URP+ShaderGraphでもレイマーチングがしたい!
概要
こんにちは。バーチャルキャストクライアント開発のnotargs(のたぐす)です。
早速ですが、みなさんはレイマーチング
という言葉を聞いたことはあるでしょうか?
レイマーチング
は、各ピクセルごとにレイを飛ばし、全てのレイを少しずつ進めていくことで物体を描画する技術の総称です。
次の画像のようなフラクタルや雲など、ポリゴンでモデリングするのが難しい物体の描画に活用されています。
この記事では、URP(Universal Render Pipeline)
とShaderGraph
を使って、できるだけコードを書かずにレイマーチングをする方法について調査してみました!
球を描画してみる
今回はライティングを自前で行いたいので、Unlit Graphを選んでおきます。
レイを定義する
まずは、ShaderGraph上でレイの起点とレイの方向を定義します。
カメラの位置を起点に、物体の今描画しようとしている座標へレイを飛ばします。
レイの位置を決めるノード
Cameraノード
とTransformノード
を使って、オブジェクト空間内でのカメラ位置を計算します。
レイの方向を決めるノード
Positionノード
とCameraノード
の差分を求めて正規化し、カメラから描画しようとしている位置までの向きを求めます。
マーチングループ
続いて、実際にレイを少しずつ進めていき、衝突判定を行う処理を書いていきます。
ShaderGraphではループが書けないため、ここだけはコードで書く必要があります。
次のようなCustom Functionノード
を用意し、TypeにはFile
を指定して以下のコードを設定します。
RayMarching.hlsl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// 位置を受け取り、物体までの距離を計算する関数 // 質点の距離関数 `length(p)` から半径を引くと、球の距離関数が定義できる float Dist(float3 p) { return length(p) - 0.5; } // 法線を計算する float3 CalcNormal(float3 p) { // 距離関数の勾配を取って正規化すると法線が計算できる float2 ep = float2(0, 0.001); return normalize( float3( Dist(p + ep.yxx) - Dist(p), Dist(p + ep.xyx) - Dist(p), Dist(p + ep.xxy) - Dist(p) ) ); } // マーチングループの本体 void RayMarching_float( float3 RayPosition, float3 RayDirection, out bool Hit, out float3 HitPosition, out float3 HitNormal) { float3 pos = RayPosition; // 各ピクセルごとに64回のループをまわす for (int i = 0; i < 64; ++i) { // 距離関数の分だけレイを進める float d = Dist(pos); pos += d * RayDirection; // 距離関数がある程度小さければ衝突していると見なす if (d < 0.001) { Hit = true; HitPosition = pos; HitNormal = CalcNormal(pos); return; } } } |
Ray Positionピン
とRay Directionピン
に先ほど計算したレイの原点と向きを刺しておきます。
Unlit Masterノードにつなぐ
Branchノードを使い、物体と衝突していれば1、衝突していなければ0をUnlit Masterノード
のAlpha
ピンに流し込みます。
Unlit Masterノード
のSurface
をTransparent
、Blend
をAlpha
、Two Sided
を有効にしておきましょう。
物体にアタッチしてみる
ここまでで作ったシェーダーのマテリアルを作り、GameObject/3D Object/Cube
から作ったCube
にアタッチしてみます。
ようやくレイマーチングで球が描画できました!
ライティングしてみる
続いて、この描画した球をライティングしてみます。
ライトの情報を取ってくる
ライトの情報を取ってくるためのノードを作成します。
Custom Functionノード
を作成し、次のスクリプトをBodyに入力します。
1 2 3 4 5 6 7 8 |
#if SHADERGRAPH_PREVIEW Direction = half3(0.5, 0.5, 0); Color = 1; #else Light light = GetMainLight(); Direction = light.direction; Color = light.color; #endif |
Lambertでライティングする
RayMarchingで計算した法線は、Transformノード
でWorld空間
に変形しておきます。
Type
にDirection
を設定するのを忘れないようにしましょう。
法線とライトの向きのDotを取ることで、光がどの程度当たっているかが計算できます。
Maximumノード
で0以下の値を切り落とし、ライトの色と掛け合わせてUnlit Masterノード
のColorピン
に繋げます。
少し影が強すぎるので、Ambientノード
の出力を足し合わせてあげます。
他の物体との衝突判定を行う
Cubeを切り抜いているため、他の物体と衝突しているときにCubeの形がそのまま出てしまっています。
こちらはZTestを自前で行うことで少し改善ができます。
SceneDepthを計算する
Linear 01
のSceneDepthノード
にCameraノード
のFar Plane(1)
を掛け、シーンにすでに描画されているピクセルの奥行きを計算しておきます。
同様に、レイマーチングの戻り値のHit Position
から、衝突位置の奥行きを計算します。
Alphaの計算につなぎこむ
シーンの奥行きと衝突位置の奥行きを比較し、HitからAlphaを計算している処理につなぎ込んであげます。
フラクタルを描画する
次のgifのように描かれるフラクタルを、「反復関数系(Iterated function system、IFS)」と呼びます。
Wikipedia: https://ja.wikipedia.org/wiki/%E5%8F%8D%E5%BE%A9%E9%96%A2%E6%95%B0%E7%B3%BB
レイマーチングを使ってこれを描画してみましょう。
フラクタルの距離関数を書く
Dist関数を次のように書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 位置を受け取り、物体までの距離を計算する関数 float Dist(float3 p) { // 反復関数系で適当なフラクタルを描画してみる float scale = 1.0; // 適当な回数繰り返して空間を折りたたむ for (int i = 0; i < 12; ++i) { // 左右上下前後をそれぞれ対称化する p = abs(p); // 45度の平面で区切って対称化する if (p.x > p.y) p.xy = p.yx; if (p.x > p.z) p.xz = p.zx; if (p.z > p.y) p.zy = p.yz; // 時間を使って適当に位置をズラす p -= float3(0.1, 0.2, 0.15) + sin(float3(0, 1, 2) + _Time.y * float3(4, 3, 2.5) + i) * 0.05; // 空間を縮小して次のステップに進む scale *= 2.0; p *= 2.0; } // 折りたたまれた空間の中で、球を描画する return (length(p) - 0.3) / scale; } |
まとめ
ShaderGraphでもがんばればレイマーチングができることが分かりました!
処理が重すぎるため使い所はかなり難しいですが、例えば異世界感の表現など、ゲーム内のワンポイントとして活用してみてはいかがでしょうか。
- Tag
- Unity