URP+ShaderGraphでもレイマーチングがしたい!

概要

こんにちは。バーチャルキャストクライアント開発のnotargs(のたぐす)です。
早速ですが、みなさんはレイマーチングという言葉を聞いたことはあるでしょうか?

レイマーチングは、各ピクセルごとにレイを飛ばし、全てのレイを少しずつ進めていくことで物体を描画する技術の総称です。
次の画像のようなフラクタルや雲など、ポリゴンでモデリングするのが難しい物体の描画に活用されています。

この記事では、URP(Universal Render Pipeline)ShaderGraphを使って、できるだけコードを書かずにレイマーチングをする方法について調査してみました!

球を描画してみる

今回はライティングを自前で行いたいので、Unlit Graphを選んでおきます。

レイを定義する

まずは、ShaderGraph上でレイの起点とレイの方向を定義します。
カメラの位置を起点に、物体の今描画しようとしている座標へレイを飛ばします。

レイの位置を決めるノード

CameraノードTransformノードを使って、オブジェクト空間内でのカメラ位置を計算します。

レイの方向を決めるノード

PositionノードCameraノードの差分を求めて正規化し、カメラから描画しようとしている位置までの向きを求めます。

マーチングループ

続いて、実際にレイを少しずつ進めていき、衝突判定を行う処理を書いていきます。
ShaderGraphではループが書けないため、ここだけはコードで書く必要があります。

次のようなCustom Functionノードを用意し、TypeにはFileを指定して以下のコードを設定します。

RayMarching.hlsl

Ray PositionピンRay Directionピンに先ほど計算したレイの原点と向きを刺しておきます。

Unlit Masterノードにつなぐ

Branchノードを使い、物体と衝突していれば1、衝突していなければ0をUnlit MasterノードAlphaピンに流し込みます。
Unlit MasterノードSurfaceTransparentBlendAlphaTwo Sidedを有効にしておきましょう。

物体にアタッチしてみる

ここまでで作ったシェーダーのマテリアルを作り、GameObject/3D Object/Cubeから作ったCubeにアタッチしてみます。
ようやくレイマーチングで球が描画できました!

ライティングしてみる

続いて、この描画した球をライティングしてみます。

ライトの情報を取ってくる

ライトの情報を取ってくるためのノードを作成します。
Custom Functionノードを作成し、次のスクリプトをBodyに入力します。

Lambertでライティングする

RayMarchingで計算した法線は、TransformノードWorld空間に変形しておきます。
TypeDirectionを設定するのを忘れないようにしましょう。

法線とライトの向きのDotを取ることで、光がどの程度当たっているかが計算できます。
Maximumノードで0以下の値を切り落とし、ライトの色と掛け合わせてUnlit MasterノードColorピンに繋げます。

ライトが反映できました!

少し影が強すぎるので、Ambientノードの出力を足し合わせてあげます。

他の物体との衝突判定を行う

Cubeを切り抜いているため、他の物体と衝突しているときにCubeの形がそのまま出てしまっています。
こちらはZTestを自前で行うことで少し改善ができます。

SceneDepthを計算する

Linear 01SceneDepthノード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関数を次のように書き換えます。

フラクタルが描画できました!

まとめ

ここまでのグラフの全体像はこんな感じです。

ShaderGraphでもがんばればレイマーチングができることが分かりました!
処理が重すぎるため使い所はかなり難しいですが、例えば異世界感の表現など、ゲーム内のワンポイントとして活用してみてはいかがでしょうか。