DirectXの話 第185

SSGI with Deinterleaved Rendering

23/05/16 up

SSGI with Visibility Bitmask

前回の第184回ではVisibility Bitmaskを利用したSSAOを実装しましたが、今回はペーパーにもあるSSGIも含めて実装してみました。
再度になりますが、ペーパーはこちらです。

https://arxiv.org/abs/2301.11376

実装はナイーブな実装で、ペーパーの擬似コードをほぼそのまま利用しています。

しかし、このまま実装するとパフォーマンス的に問題があったため、パフォーマンス向上のためにDeinterleaved Renderingを行ってみました。

Deinterleaved Rendering

Deinterleaved RenderingはSSAOなどのスクリーンスペース技術でキャッシュを効率的に動作させるために用いられる技術です。
現代のハードウェアはキャッシュを最大限利用することでパフォーマンスが出るように設計されているため、キャッシュ効率は非常に重要なパフォーマンス指標となります。

SSAOやSSGIはスクリーンスペースでレイトレーシングを行う技術(いわゆるレイマーチング)ですが、レイが長いとGPUスレッドがスクリーンテクスチャに対してランダムアクセスすることになります。
また、この手の技術はデノイズ前提であるため、デノイズがうまく動作するようなノイジーな結果を求められます。
そのため、ピクセル単位で別々の方向にレイを飛ばす必要がありさらにランダムなアクセスが加速することになります。

キャッシュ効率を考える場合、メモリアクセスの局所性が重要になります。
つまり、広い範囲のランダムアクセスを避け、狭い範囲の整列したアクセスすることがキャッシュ効率を向上させます。

SSAOやSSGIも狭い範囲をレイマーチすることでのアクセスでキャッシュ効率を向上させることができます。
しかし、特にSSGIは狭い範囲でのアクセスにしてしまうとGIの効果が薄くなってしまいます。

これを解決するためによく用いられるのがミップマップを利用したコーントレーシングです。
近場の、狭い範囲では高解像度アクセスが行われますが、対象ピクセルから離れるほど低解像度アクセスすることになるのでキャッシュ効率が向上することでしょう。

しかし、Visibility Bitmaskはコーントレーシングと相性が良さそうな印象がないため、今回はDeinterleave Renderingにしてみました。
Deinterleave Renderingはフルスクリーンの画面を複数画面に分割し、それぞれで処理を行う手法ですが、分割方法がいわゆるタイル分割ではなく、1ピクセルずつ交互に抜き出していくことになります。
以下の図はGDC2013のNVIDIA社発表資料より抜粋したものになります。

タイル分割で処理すると、キャッシュの局所性は保たれますが広い範囲のレイマーチは行えません。
しかしDeinterleaved Renderingの場合は交互にピクセルを分割するため、最終的に分割後のスクリーンはそれぞれが元画像の縮小に近い結果となります。
実際、今回のサンプルでDeinterleaveしたワールド法線の画像は以下のようになります。

NVIDIA社資料では2x2のDeinterleaveですが、私のサンプルでは4x4のDeinterleaveとなります。
また、SSGIに必要なワールド法線、深度、ライティング結果の3枚をDeinterleaveしています。
サンプルではNVIDIA社のようにテクスチャアレイを利用せず、フルスクリーンサイズの2Dテクスチャに分割して書き込んでいます。
この場合、レイマーチ時に境界部分で別のDeinterleaveインデックスを参照しないように注意しなければなりません。

実装

サンプルは以前と同じVisibility Bufferサンプルに追加しています。

https://github.com/Monsho/VisibilityBuffer

ssgi.c.hlslが対象のシェーダコードです。
前回までのSSAOとは別のシェーダコードとなっていますので注意してください。
SSGIを有効にするには、GUIのSSAO Typeから"SSGI with VB"を選択してください。
デフォルト設定ではSSGIの効果が薄いので、GI Intensity、Max Pixel、World Radiusを最大にすると効果が出ます。
それでもわかりにくいようであれば、Ambientをなしにするか、DebugのDisplay ModeでGIを選択してください。

Deinterleaved Renderingではピクセル位置が少し面倒になります。
深度から座標を再構築する際にスクリーンのUV座標を用いますが、この際にDeinterleaveする前の元ピクセルからのUV座標が必要になります。
これはレイマーチしたのちに取得する座標も同様です。

こちらはDeinterleaveがOn/Offの場合の座標、UV計算部分です。
pixPosはDeinterleave後のピクセル座標、outPosは出力時のReinterleave(Deinterleaveしたものを元に戻す)ピクセル座標です。
同様に、pixUVpixPosから、outUVoutPosから求められるUV値です。
座標の再構築はoutUVを利用することになります。

他のパラメータも解説しておきます。

rectはレイマーチのサンプリングを行う範囲を示しています。この範囲を超えるとレイマーチが打ち切られます。
通常では0~1の範囲ですが、Deinterleaveの場合は左右上下端のピクセル中心となります。

baseUVはレイマーチの際にpixUVからoutUVを再構築する際に利用します。
計算処理は以下のようになっています。

Deinterleaveは4x4ですので、この際のUV座標は0.25刻みでインデックスが変わります。
このUV座標を0~1に拡張した場合、各Deinterleaveインデックスのピクセル中心というのは元のUVでいうと、4x4ブロックの中心位置ということになります。
それを考慮してbaseUVを求めています。
少しわかりにくいので2x2の場合で図示してみました。

この問題は最初に実装したDeinterleaved Renderingの結果がどうにもおかしく、色々計算式を見直しているうちに気づきました。
通常、DeinterleaveインデックスごとのSSAO/SSGIにはそれほど大きな違いは出ないのですが、この計算式をミスるとかなり大きな違いが発生します。
今回はSSGI計算処理の中でReinterleaveしているのですが、Deinterleaveしたまま結果を出力するデバッグ機能を用意しておくと問題の発生に気づきやすいかもしれません。
少なくとも、私は結果をDeinterleaveしたまま出力した結果をチェックしておかしいことに気づきました。

実際のレイマーチのコードは前回示したもの(バグが有ったので修正を行っていますが)と大きな違いはありません。
ステップサイズがDeinterleaveの場合は1/4になること、レイマーチ範囲が0~1の即値ではなくrect参照になっていること、スクリーン空間座標からビュー空間座標に変換する際に、テクスチャサンプリングするUVと再構築のUVで違いがあることくらいです。
もちろんGI計算も追加されているのですが、こちらはペーパーの擬似コードほぼそのままです。

結果

通常の処理とDeinterleaved Renderingはチェックボックスで変更できるようになっています。
AO/GI表示にしてチェックボックスをOn/Offしてみると映像的な差はわかるかと思います。
映像的には確かに違っているのですが、大きな映像的なエラーはDeinterleaved Renderingでも発生していません。
元々ノイジーな結果ですので、そこまで問題にはなっていないようです。
しかし、高周波なシーンでは差が発生するかもしれません。
可能であれば、実装時にはDeinterleaveのOn/Off機能を追加してエラーが発生しないか様々な場所でチェックするほうが良いでしょう。

パフォーマンスについてはかなり大きな差が出ます。
Max Pixel、World Radiusを最大にした場合のパフォーマンスの差異は以下のようになりました。

NsightでのGPU Traceの比較です。
1stが通常、2ndがDeinterleaved Renderingです。
L1キャッシュヒット率が大きく改善しているのがわかります。
処理時間(Duration)も倍になっています。
各Throughputも全体的に改善しており、キャッシュヒット率向上が良い結果をもたらしていることがわかりやすいです。

しかし、レイの長さを短くしてみるとパフォーマンスは逆転します。
差は大きくないですが、Deinterleaveするほうがわずかにパフォーマンスが悪くなります。
Deinterleaveパス自体に0.3msほどかかっているので、狭い範囲でSSAO/SSGIを行う場合はDeinterleaveしない方が有利なようです。

また、サンプルではoutPosをベースにランダム値を決定していますが、よりキャッシュヒットしやすくするためにはDeinterleaveインデックスごとにランダム値を決定する方法もありです。
この場合さらにキャッシュヒット率が改善し、パフォーマンスも大幅に上昇します。

このテストコードはコメントアウトしているため、テストする場合はコメントを切り替える必要があります。
ssgi.c.hlslの226行目と227行目のコメントを反転させてください。

ただし、パフォーマンスは上がりますが、ノイズの品質は悪くなります。
時間方向デノイズをキツめに適用することでごまかせますが、ゴースティングなどの原因にもなりますので一長一短ですね。

最後に

Deinterleaved Renderingによってキャッシュ効率が向上することが今回のサンプルで確認できました。
場合によってはキャッシュ効率が上がってもパフォーマンスが悪化する場合もあるようですが、悪化は比較的軽微なので、Deinterleaveパスを含めてコストが見合うようであれば非常に強力な手法と言えます。

しかし、Deinterleaveパスのターゲットが作業に必要なバッファ分だけ作成しなければならないため、例えばSSRのようにラフネス・メタリックパラメータも必要という状態になってくるとメモリ量的にも不利な部分が出てきます。
もちろん解像度にも関係してきますが、単純に半解像度にして処理するより品質の高い結果を高パフォーマンスで実現できる可能性があります。
一部の技術に対して、ということにはなりますが、結構効果的な手法ではないでしょうか。