凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で距離関数の記述だけでレイマーチングができる uRaymarching の UniversalRP 対応してみた

はじめに

uRaymarchingUnity 上で簡単にレイマーチングするオブジェクトを生成することの出来るアセットです。詳細については以前の記事をご参照ください。

tips.hecomi.com

tips.hecomi.com

これまではレガシーなパイプライン(Forward / Deferred)の対応みでしたが、Scriptable Render Pipeline の登場以後 UniversalRP や HDRP の要望をチラホラ頂いていたので、まずは手始めに UniversalRP 対応を行ってみました。本エントリでは今回のアップデートで追加された UniversalRP テンプレートの紹介と、実装で行ったことや解決した問題などのノウハウについて共有したいと思います。

デモ

12 ポリゴンのキューブで描画されています。UniversalRP 下で従来と同じように Lit / Unlit が動きます。

f:id:hecomi:20200111161911p:plain

f:id:hecomi:20200111161941p:plain

ダウンロード

github.com

執筆時の最新バージョンは v2.0.1 です。

使い方

テンプレート

Shader > uShaderTemplate > Generator でジェネレータを生成した後、インスペクタから Shader Template を以下に設定:

  • Universal > UniversalRP > Lit
    • ライティング有り(Universal Render Pipeline/Lit と同じ)
  • Universal > UniversalRP > Unlit
    • ライティング無し(Universal Render Pipeline/Unlit と同じ)

UI

f:id:hecomi:20200111132011p:plain

Lit / Unlit テンプレートともに同じ項目になります。

Conditions

次の項目があります。

  • Shadow Caster
    • ShadowCaster パスの生成を行うかどうか
    • チェックを入れると影が出ます
  • World Space
    • チェック無し時はポリゴン中心を原点としてレイマーチングします
    • チェックを入れるとワールド座標で行いそれをクリッピングするようになります
  • Object Scale
    • チェックを入れるとオブジェクトをスケールするとレイマーチングした世界も拡大します
  • Check If Inside Object
    • チェックを入れるとポリゴン内部に入った際に Near Clip 位置からレイを開始します
    • CullingNone にする必要がある
    • 内部にいるか判定負荷は少しあるので使わないときはオフにしてください
  • Ray Stops At Depth Texture
    • 通常はループ回数が MAX になった時かレイが Far Clip まで到達した場合に終了しますが、チェックを入れた場合はレイがデプステクスチャ位置に到達すると終了します
    • ZWriteOff な半透明オブジェクトで使用してください
  • Ray Starts From Depth Texture
    • デプスプリパスが使われている際、実際のレンダーターゲットへの描画時にレイの開始位置をデプスプリパスで計算した最終位置を使います
    • 詳細は後述します

Variables

  • Render Type
    • Tags ブロックの RenderType の設定です
  • Render Queue
    • Tags ブロックの Queue の設定です
  • LOD
    • LOD の数値です
  • Object Space
    • CUBENONE かどちらかを選択してください
    • CUBE はキューブポリゴンで奥側をクリッピングします
    • NONE はキューブポリゴンを超えて奥まで描画します
CUBE

f:id:hecomi:20190127185823g:plain

NONE

f:id:hecomi:20190127185906g:plain

問題点

時間ベースの動きが非実行時にずれる

UniversalRP では ゲームを実行しないとパス毎に独立した _Time が使われている?のか実際の形状と影がずれる現象が生じます。

f:id:hecomi:20200111151828p:plain

引き続き調査しますが、実行すれば正常になりますのでしばらくはそちらでご対応ください。。

フルスクリーンモードはない

レガシーパイプラインでは RaymarchingRenderer コンポーネントを使うと CommandBuffer を利用したフルスクリーンのレイマーチングシーンを作成できましたが、ライティングがうまくいかない問題などが有り、現在は冒頭のリンクの記事で解説したように、大きいキューブを作成・カリングオフ・カメラ内判定フラグオンで代替してもらうように薦めています。

VR は未確認

一応コード上は動くように書いているつもりですが未だ確認していないです。。時間が出来たときに調査しますが、もし先行してバグなど見つけられましたらご連絡いただけると助かります。

実装メモ

以下今回の実装に関するメモです。

コード修正

以前の記事で UniversalRP の調査を行いましたが、各関数の変化(UnityObjectToClipPos() がなくなって TransformObjectToHClip() を代わりに使うなど)に対応したのと、ビルトインのシェーダを見てみると positionWS のように「変数名 + 空間名」となっていることが多いので、新ルールに合わせるよう対応を行いました。

tips.hecomi.com

また拡張子も .cginc から .hlsl に変更します。ポイントライト影がなくなったり、ShadowCaster の中で UpdateDepthTexture 対応の分岐を書かないとならない(該当コード)...といった直感的でない挙動がなくなったりなどで、全体的にはよりシンプルになりました。

DepthPrepass に関して

f:id:hecomi:20200111180522p:plain

カスケードシャドウマップを使わない場合は Render Main Shadowmap でライトから見たデプスを生成し、その深度値を Render Opaques 時にカメラから見たデプスと比較して遮蔽されているか否かを判断、遮蔽部はライティングの影響を受けないようにします。

f:id:hecomi:20200111190040p:plain

一方、カスケードシャドウマップ利用時には Render Main Shadowmap でライトから見たデプスを(分割数に応じて複数)生成し、次に Depth Prepass というステージでカメラから見たデプスのみを先に生成、そして Resolve Shadows でスクリーンスペースでシャドウ部分だけを先に計算したテクスチャを生成、そのテクスチャを Render Opaques 時に使って影を描画する、という流れになっています。

f:id:hecomi:20200111192156p:plain

そして、この Depth Prepass を通る場合は、不透明オブジェクト描画時にデプステクスチャが _CameraDepthTexture という変数で使えるようになります。

なお、URP アセットで設定できる Depth Textureチェックボックスをオンにすると、不透明オブジェクト描画"後"に、デプステクスチャが使えるようになります(半透明時でのソフトパーティクル表現やポスプロ用)。

DepthPrepass の結果の利用

さて上述の DepthPrepass を通る場合、ここで使われる DepthOnly パスと、ライティングを行う ForwardLit 両方でレイマーチングが行われてしまいます。レイマーチングするオブジェクトのコストの殆どはこのループで消費されるので、ほぼ 2 倍のコストが掛かってしまう形になります。そこで DepthOnly パスを通った際に、ForwardLit パスはその結果を使うオプションとして Ray Starts From Depth Texture 条件を Conditions の中に作成しました。ここにチェックを入れると、次のようなコードを通ることになります。

#if defined(RAY_STARTS_FROM_DEPTH_TEXTURE)

TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);

inline void InitRaymarchWithCameraDepthTexture(inout RaymarchInfo ray, float3 positionWS, float4 positionSS)
{
    // デプステクスチャの UV
    float2 uv = positionSS.xy / positionSS.w;

    // 描画先ピクセルのデプステクスチャの値を取得
    float depth = SAMPLE_DEPTH_TEXTURE(
        _CameraDepthTexture, 
        sampler_CameraDepthTexture, 
        uv);

    // デプステクスチャの値をデコード
    depth = LinearEyeDepth(depth, _ZBufferParams);

    // カメラからの距離に変換
    float dist = depth / dot(ray.rayDir, GetCameraForward());

    // デプステクスチャに対応するワールド座標を算出
    ray.startPos = GetCameraPosition() + ray.rayDir * dist;
}

#endif

余談ですが、デプス取得のコードも UniversalRP ではレガシーと比べて変わっていますのでご参考まで。

これにより ForwardLit のレイマーチングはループなしで終わるという流れになります。ただし、この場合は Post Process ブロックで ray.loop にアクセスしてなにかしたい場合、常に 1 が降ってきてしまうので、簡易 AO を行いたいユースケース等は対応できなくなってしまう点にご留意下さい。

おわりに

UniversalRP 対応でも結構疲れました...。HDRP はグッと難易度が上がると思うので、目標は低めに...、取り敢えず今年中に対応を目指して進めていきたいと思います。