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




