凹みTips

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

uRaymarching の Depth Prepass 対応をした

はじめに

以前の記事で uRaymarching の URP における Depth Prepass 問題を挙げました。

tips.hecomi.com

Depth Prepass が有効になっている際は、Depth は事前に DepthOnly パス(または DepthNormals パス)で出力され、実際に色を決定する ForwardLit パスでは ZTest Equal かつ ZWrite Off で描画されます。最終的に他のポリゴンで覆われてしまうようなピクセルの重い計算を省略できるメリットがあります。詳しくは公式動画をご覧ください。

logmi.jp

通常は頂点シェーダから渡ってきた情報から自動的にデプスが計算され ZTest Equal が成功しますが、uRaymarching では SV_Depth セマンティクスを使ってこのデプスをいじっています。この結果、計算結果のデプスがこの事前のパスと色決定のパスで少しずれてしまう関係で、何もそのピクセルに結果的に書き込まれない形になり、クリアした結果がそのまま出力されガビガビになってしまうという状況になっていました。

これに対して lil さんより以下のコメントを貰いました。

今回はこちらを試してみます。

対応

uRaymarching は uShaderTemplate というシェーダジェネレータを使っています。

tips.hecomi.com

まず、この uShaderTemplate 向けに以下の行を追加しました。

@if CheckDepthPrepass : false
    #define CHECK_DEPTH_PREPASS
@endif

これにより、Conditions セクションに Check Depth Prepass を追加できます。

殆どの方は uShaderTemplate とは...?だと思いますが、ここでの目的としては CHECK_DEPTH_PREPASS というマクロの有無を見て、チェックが必要なときは追加の計算を行いたく、そのためのフラグの ON/OFF をエディタに追加したいというものです(本当は組み込みのマクロが提供されていると良いのですが見当たらず...)。

そして ForwardLit パスでこのマクロを見るように以下のように書き換えます。

FragOutput Frag(Varyings input)
{
    ...
    RaymarchInfo ray;
    INITIALIZE_RAYMARCH_INFO(ray, input, _Loop, _MinDistance);
    Raymarch(ray);
    ...

    FragOutput o;
    o.color = color;
#ifdef CHECK_DEPTH_PREPASS
    float2 uv = input.positionSS.xy / input.positionSS.w;
    float depth = SAMPLE_DEPTH_TEXTURE(
        _CameraDepthTexture,
        sampler_CameraDepthTexture,
        uv);
    o.depth = abs(ray.depth - depth) < 1e-4 ? depth : ray.depth;
#else
    o.depth = ray.depth;
#endif

    return o;
}

マクロが定義されている場合は、スクリーンスペースの UV を求めて _CameraDepthTexture から事前パスで書き込まれたデプスを取得、レイマーチングのデプスとそれを比べて差が小さい場合は現在書き込まれているデプスをそのまま採用、という流れです。これで Check Depth Prepass をチェックして再コンパイルすればデプスが一致してガビガビがなくなりました。

問題点

しかしながらカメラを近づけると次のようにまたガビガビになってしまいます。

これはデプスは近い場所ほど精度が良くなるように書き込む値が非線形になっているからで、ワールド空間で同じズレだったとしても、カメラに近い際はデプスバッファ上のズレが大きくなります(これによって高い精度を保てているわけです)。

developer.nvidia.com

先程固定値で 1e-4 として比較していた値よりも上回るケースが出てきて、そこで深度テストが一致せずガビガビになってしまったというわけですね。

これに対しても lil さんが考察していらっしゃいました。

以下のようにコードを変更します。

FragOutput Frag(Varyings input)
{
    ...
#ifdef CHECK_DEPTH_PREPASS
    ...
    float delta = saturate(ray.depth * ray.depth);
    o.depth = abs(ray.depth - depth) < delta ? depth : ray.depth;
#else
    ..
#endif
    ...
}

差分は固定値ではなく深度値の 2 乗を利用しています。これによりカメラに近いときは大きい値、遠いときは小さい値となり深度テストがクリアされるようになりました。

これで Depth Prepass を ON にしていても SSAO なども掛かるようになります(前回の記事だと Transparent 推奨だったので掛かりませんでした)。

おわりに

レイマーチングは基本的にはループが重く、結局事前のパスでそれが回っており、DepthPrepass で計算が軽くなる、ということはあまり期待できないかもしれないですが...、覚えておくのには面白いテクニックだと思います。VR 対応はまた後ほどテストして対応します。