レイトレ日記 2022/07/08

今日は実装用のメモをとっておこうと思います。
自分さえわかればいいので,詳しい説明は省略します。

1. [Bitterli 2020]

[Bitterli 2020]では,アルゴリズム5にSpatiotemporal reuseを使用したRISアルゴリズムが載っています。

※図は[Bitterli 2020]より引用
で,基本的にはこれをそのまま実装すれば良いです。
まずは画面サイズ分のピクセルを格納するための配列を用意して,これをリザーバーとして使用します。
あとは,アルゴリズム5にあるように

  • 初期候補の生成(Generate initial candidates)
  • 初期候補に対する可視性を評価(Evaluate visibility for initial candidates)
  • 時間的再利用(Temporal reuse)
  • 空間的再利用(Spatial reuse)
  • ピクセルカラーを計算(Compute pixel color)

の順で処理していきます。

まず,初期候補の生成ですが,アルゴリズム3に従ってリザーバーに格納していきます。

※図は[Bitterli 2020]より引用
\(x_i\)は\(i\)番目のピクセルの位置座標,\(p(x_i)\)は,ソースの確率密度関数(source PDF)で,自分的な理解では\(p(x_i)\)はBSDFの確率密度関数になると思います(間違っていたら指摘して下さい)。\({\hat p}_q(x_i)\)は,被積分関数\(f_q\)に対応するターゲットの確率密度関数(target PDF)を意味します。本文中にtarget PDFは\({\hat p}(x) = \rho(x) L_e(x) G(x)\)という記述があります。\(\rho\)はBSDFで,\(L_e\)は放射輝度,\(G\)は幾何項を意味します。
アルゴリズム3に登場する\(r.y\)や\(r.{\rm w}_{\rm sum}\)などはリザーバーのメンバー変数を意味します。その定義はアルゴリズム2に記載されています。

※図は[Bitterli 2020]より引用
\({\mathbb S}[i]\)は本文中に説明がなさそうなのですが,\(i\)番目のサンプルと,\({\rm weight}({\mathbb S}[i])\)は\(i\)番目のサンプルの重みと解釈しておけば大体あっているのではないかと思われます。

アルゴリズム5に戻って,次の処理は初期候補の可視性の評価についてです。これはサンプル\(y\)を使ってシャドウレイをトレースすれば求まると思います。サンプルの定義は[Bitterli 2020]では言及されていませんが,[Ouyang 2021]では次のように示されているので,これを参考に実装すればよさそうです。

※図は[Ouyang 2021]より引用
アルゴリズム5の次の処理は,Temporal reuseです。
pickTemporalNeighbor()についてはセクション5に記載があり,現在のピクセルの周囲30pixelの半径で,低ディスクレパンシー数列から5個のランダムな点をサンプリングしているとあります。HaltonやらHammersleyやら,そういった類の数列を使ってサンプリングする実装にすればよさそうです。
combineReservoirs()についてはアルゴリズム4に記載があります。

※図は,[Bitterli 2020]より引用
計算する際は,現在のピクセル位置を前のフレームに投影するために速度バッファからモーションベクトルを引っ張ってきて計算すればよいそうです。
バイアスドなアルゴリズムでは,形状やマテリアルが大きく異なる隣接ピクセルを再利用するとバイアスが大きくなるため,カメラ距離と現在ピクセルと隣接ピクセルの法線の間の角度を比較して,閾値を超えたら棄却するということをやっているそうです。

アルゴリズム5の続いての処理は,Spatial reuseです。
pickSpatilaNeighbors()も,低ディスクレパンシー数列を使ってランダムな点をサンプリングしておけば良さそうです。
combineReserrvoirs()は先ほど説明したので,同じ処理を実行すればよいです。

アルゴリズム5の最後の処理は,ピクセルカラーの計算です。
これは被積分関数にリザーバーのサンプル点を渡して,式(6)を使ってRISの重みを求めて乗算すれば良いようです。
アルゴリズム3に具体的な式が載っています。

あと、細かい所としては Generate initial candidates, Evaluate visibility for initial candidates, Temporal reuseのループ数が同じなので,1つのシェーダにまとめてしまってもよさそうです。
Spatial reuseはシェーダ内でループすると,処理時間の関係でGPUハング(TDRに引っかかる)する恐れもあるので,CPUでループした方が実装上は安全かもしれません。

2. [Ouyang 2021]

[Bitterli 2020]では,直接照明に対して適用するものでした。間接照明に対してもReSTIRを適用しようとするのが,[Ouyang 2021]です。
ReSTIRがグローバルなライト空間に初期サンプルを配置するのに対して,ReSTIR GIはシェーディング点周辺の方向のローカルな球の空間上に初期サンプルを配置します。レイの原点に向かって散乱する光の量でRISの重みを決定します。空間的,時間的な両方でこれらの点をリサンプリングすることで,シーンにおける間接照明を近似する分布からの重みづけサンプルを生成することができ,大幅な誤差低減につなげます。
オリジナルのReSTIRとReSTIR GIの違いですが,図2に示されています。オリジナルが(a)と(b)で,ReSTIR GIが(c)と(d)になります。

※図は,[Ouyang 2021]より引用
オリジナルのアルゴリズムはシーンにおけるライト上でランダムに生成した点から開始されます。リサンプリング後に,寄与しないオリジナルのサンプルは棄却されます。有益なサンプルが空間的,時間的に共有され,その確率的な寄与に基づいて使用されます。
ReSTIR GIでは,Visible pointと呼ばれる可視点からランダムな方向にレイを飛ばし,ヒットしたところを初期サンプルとします。空間的リサンプリングと時間的なリサンプリングは同様の方法で適用されます。(c)と(d)を見ると分かりますが,オリジナルでは寄与を与えるのはライトにヒットするものだけですが,ReSTIR GIでは,2-Bounceするサンプルも寄与として取れる可能性があるため,意味がある間接照明を与える方向を見つけることが可能になります。

[Bitterli 2020]では,リザーバーを更新するUPDATE()のみが定義されていましたが,アルゴリズム1に示すように,別のリザーバーをマージする関数が追加されています。

※図は,[Ouyang 2021]より引用
図4に示すようにReSTIR GIは3ステップのアルゴリズムです。

※図は,[Ouyang 2021]より引用
各フレームでは各ピクセルに対して以下のステップを実行します。

● Initial Sampling
visible pointからランダムな方向にレイをトレースします。そして,最も近い交差点をスクリーン空間の初期サンプルバッファに記録します。交差点における位置座標,法線,放射輝度,Next Event Estimation(NEE)で使用する乱数,また同様にピクセルの位置座標と法線も記録します(Sample構造体を参照)。

● Temporal reuse
初期サンプルバッファからのサンプルを使用して,Temporal Reservoir Bufferを更新します。現在フレームで作成されたものと
,既存のバッファの間でランダムに選択することによって更新します。

● Spatial reuse
Spatial Reservoirを更新するために近接ピクセルからランダムにtemporal reservoirを選択します。バイアスを消すために,現在ピクセルの深度と法線を比較することによって類似する幾何的特徴を持つ近接ピクセルを選択します。

まず最初の初期サンプルですが,アルゴリズム2に示されています。

※図は,[Ouyang 2021]より引用
可視点から始めるので,G-Bufferから位置と法線を引っ張り出します。次に,ランダムな方向\(\omega_i\)にレイを飛ばして,交差位置とその位置の法線,確率密度,出射放射輝度などを求めて,Sample構造体のコンストラクタを呼び出してInitialSampleBufferに格納します。

次に,Temporal reuseですが,アルゴリズム3に従って実装します。

※図は,[Ouyang 2021]より引用
source PDFとtarget PDFさえ求められれば,サクッと実装できるはずです。target PDFは式(10)を使って求めればよいです。実装のときに混乱するので,まとめておくと
\(p_q(\omega_i)\) ⇒ 各ピクセル\(q\)における可視点\(x_V\)に対応するBSDFの確率密度関数
\({\hat p}_q(\omega_i)\) ⇒ 各ピクセル\(q\)における\(L_o(x_s, -\omega_i)\) (式(10))
添え字のqが付いていないものは,そのサンプル位置におけるsourcePDFとtargetPDFを計算をすればよいかと思います。

UPDATE()関数はアルゴリズム1に示されているので,問題ないでしょう。これでTemporal Reservoir Bufferから取得したリザーバー\(R\)が更新できたので,Temporal Reservoir Bufferに格納して値更新します。

最後に,Spatial reuseのステップですが,アルゴリズム4に従って実装します。

※図は,[Ouyang 2021]より引用
隣接ピクセル\(q_n\)の選択についてですが,特に何も書いていなさそうなので,[Bitterli 2020]を参考に低ディスクレパンシー数列を使ってランダムに選べばよいかと思います。セクション5に,半径等の設定については記述があり,適用的に変化させます。初期半径は画像解像度の10%で,Spatial Reuseの際に別ピクセルのサンプルが再利用可能でSpatil Reservoirに提供される場合は半径は変えず,そうでない場合は,半径を半分にしていき,3pixelの下限値を下回らないように維持します。
アルゴリズム内に出てくる幾何類似度のチェックですが,サーフェイス法線が\(25^{\circ}\)以内,正規化された深度の差分が\(0.05\)以内であれば類似と判定する[Bitterli 2020]と同じ方法をとっているそうなので,これを参考に実装すれば良さそうです。
Calculate \(|J_{q_n \rightarrow q}|\)は式(11)を使って計算するだけで,位置座標と法線ベクトルがあれば計算することが出来ます(図6参照)。

※図は,[Ouyang 2021]より引用
アルゴリズム4中に出てくるMERGE()関数はアルゴリズム1に記載されているので,これをみて実装すれば良いです。
アルゴリズム4ではわかりやすいように,17行目から19行目のfor文を分けてくれていますが,Qのメモリ領域を取るのがシェーダ実装上でめんどくさそうな気もするので,15行目あたりに18・19行目の処理を移動して,16行目の初期化を3行目あたりにもってきて,なるべく変数確保をせずに,効率的に実装した方がよさそうな感じです。

基本的なReSTIR GIの実装は以上ですが,[Ouyang 2021]ではSample構造体が大きくなりやすいので,サーフェイス法線を4byteに圧縮,放射輝度にはhalfを使うなどして,48byteにしてメモリ帯域を減らすなどの工夫をしているそうです。
また,すべてのピクセルで長い経路をサンプリングすると,パフォーマンスに大きな影響を与える可能性があるため,25%のピクセルにおいてのみマルチバウンスパスを追跡しているそうです。実装的には画面を\(64 \times 32\)ピクセルのタイルに分割して,タイル単位でロシアンルーレットを適用し,ロシアンルーレットで不合格になったものはシングルバウンスパスを使用して描画し,サンプル点における直接照明を計算します。ロシアンルーレットで合格になったものは,マルチバウンスパスになりますが,ロシアンルーレットの確率を使用して重みづけするそうです。

…というわけで,めちゃくちゃざっくりですが,実装のメモでした。
あと分からないところは,論文のサンプルコードなどを頼りに実装していけばよいかと思います。

参考文献

  • [Bitterli 2020] Benedikt Bitterli, Chris Wyman, Matt Pharr, Peter Shirley, Aaron Lefohon, Wojciech Jarosz, “Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting”, ACM Transactions on Graphics, Vol.39, No.4, 2020.
  • [Ouyang 2021] Y.Ouyang, S.Liu, M.Kettunen, M.Pharr, J.Pataleoni, “ReSTIR GI: Path Resampling for Real-Time Path Tracing”, High-Performance Graphics 2021.