はじめに
以前の記事で uRaymarching の URP における Depth Prepass 問題を挙げました。
Depth Prepass が有効になっている際は、Depth は事前に DepthOnly
パス(または DepthNormals
パス)で出力され、実際に色を決定する ForwardLit
パスでは ZTest Equal
かつ ZWrite Off
で描画されます。最終的に他のポリゴンで覆われてしまうようなピクセルの重い計算を省略できるメリットがあります。詳しくは公式動画をご覧ください。
通常は頂点シェーダから渡ってきた情報から自動的にデプスが計算され ZTest Equal
が成功しますが、uRaymarching では SV_Depth
セマンティクスを使ってこのデプスをいじっています。この結果、計算結果のデプスがこの事前のパスと色決定のパスで少しずれてしまう関係で、何もそのピクセルに結果的に書き込まれない形になり、クリアした結果がそのまま出力されガビガビになってしまうという状況になっていました。
これに対して lil さんより以下のコメントを貰いました。
SV_Depth問題、雑に対処するならForwardパスで
— lil (@lil_xyzw) 2022年5月30日
float cameradepth = _CameraDepthTexture[uint2(input.positionCS.xy)].r;
o.depth = abs(ray.depth - cameradepth) < 1e-4 ? cameradepth : ray.depth;
みたいな感じになるのかな……?(VR対応させるならもうちょっとちゃんと書かないといけない)
今回はこちらを試してみます。
対応
uRaymarching は uShaderTemplate というシェーダジェネレータを使っています。
まず、この 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 をチェックして再コンパイルすればデプスが一致してガビガビがなくなりました。
問題点
しかしながらカメラを近づけると次のようにまたガビガビになってしまいます。
これはデプスは近い場所ほど精度が良くなるように書き込む値が非線形になっているからで、ワールド空間で同じズレだったとしても、カメラに近い際はデプスバッファ上のズレが大きくなります(これによって高い精度を保てているわけです)。
先程固定値で 1e-4
として比較していた値よりも上回るケースが出てきて、そこで深度テストが一致せずガビガビになってしまったというわけですね。
これに対しても lil さんが考察していらっしゃいました。
近づいたときに誤差が大きくなるので、
— lil (@lil_xyzw) 2022年5月30日
o.depth = abs(ray.depth - cameradepth) < saturate(ray.depth * ray.depth) ? cameradepth : ray.depth;
が良さそうな感じがした
以下のようにコードを変更します。
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 対応はまた後ほどテストして対応します。