26/04/19 up
今回はChatGPT Codexさんと一緒にReSTIR GIを実装した話になります。
上の画像は暗いですが、ReSTIR GIの結果のみ、デノイズなしの状態を可視化したものです。
また、最適化は今後の課題としています。
実装はこちら。
ReSTIRは"Reservoir-based SpatioTemporal Importance Resampling"の略で、時空間を利用したインポータンスサンプリング的な手法です。名前の通り、Reservoirを利用しているという点と、インポータンス”リ”サンプリングするという点が特徴です。
最初の論文は多分NVIDIAさんが出した直接ライティング用のものだと思います。
その後、Global IlluminationやPath Tracingにも応用できるようになっていきました。
なお、今回の実装ではこちらのGIの論文と、それを元にNVIDIAさんが実装したRTXDIを参考にしています。
https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing
https://github.com/NVIDIA-RTX/RTXDI
ReSTIRの数学的な意味合いなんかは以下に記事があります。(英語)
https://agraphicsguynotes.com/posts/understanding_the_math_behind_restir_gi/
モンテカルロGIはサンプリング数を増やして最終的に収束させていく手法で、1フレームに1サンプリングのみだとしても、時間をかければ最終的な絵に収束するのですが、ReSTIR GIの場合は短時間でサンプリング数を増やすことはできるのですがノイズ自体は時間をかけてもなくならないのが特徴ではないでしょうか。
そのため、必ずデノイザーが必要となります。デノイザーなしでは利用できないと考えてください。
ReSTIR GIの実装ではレイトレーシング部分についてはモンテカルロGIと違いはありませんが、その結果をどのように格納するかという点で大きな違いがあります。
普通に考えればRadianceの結果(から計算したライティング結果)をブレンドしていくことになると思うのですが、ReSTIRでは確率的にサンプリング結果を格納するかしないかを決定し、格納する場合はRadianceなどのサンプリング結果を置き換えて、格納されるにしてもされないにしてもウェイト値は更新していくという感じです。
確率的にこの色が、このくらいのウェイト値で入ってくるという情報を格納するので、確率次第で明るくなったり暗くなったりします。
また、このように時空間的にウェイト値を更新していくと新しい結果を受け取りにくくなってしまうことがあります。
この問題に対応するため、定期的に格納する結果をリセットして新しい結果を受け入れるようにします。
リセットすれば当然積み重ねたものが消えてしまうので、どんなに時間をかけてもノイズがなくなりません。
デノイザーが絶対必要になるのはこれらの基本的な仕様によるところです。
今回の実装では論文もあればRTXDIなどの参考となる実装もあるので、自分だけで実装するより生成AIを利用したほうが速いのでは?と思いましたので、ちょっと前に契約したChatGPT Codexを利用してみようと。
Claudeとどっちにするかという点で悩んだ部分はあるのですが、使い物にならないならClaudeも試してみようというくらいの気持ちで選びました。
Codexを使う方法はいくつかあるかとは思うのですが、今回はウェブブラウザからそのまま利用する形を採用しました。
リポジトリでReSTIR実装用のブランチを作成し、そのブランチでCodexさんに作業してもらいます。
まずはReSTIR GIの実装を計画して!という感じで計画立てさせて、その計画に問題なさそうなら承認してそのまま実装を続けてもらいました。
承認前に問題があったのはほんの一部で、シェーダファイル名はこんな名前にしてとかの小さな要望もいくつかありましたが、概ね計画通りにやればいいという感じで承認していきました。
実装が完了するとプルリクを出せる状態になるので、簡単に確認したあとにプルリクを作り、ローカルブランチに取り込んでビルド、実行するという流れです。
いちいちプルリク出して取り込んで…というのは面倒な部分もありますが、都度実装を確認できるし、実装が完了するまではローカルPCの資源を使わないのでのんびりゲームができますw
RiderだとChatGPTアカウントを登録してCodexを利用することもできるようなのですが、これについては後で試してみようとは思っています。
使ってみての感想というか、少なくとも現時点での結論ですが、きちんと監修してやらないと正しい実装をしてくれないです。
そして、一度実装するとその実装が間違っていても見直そうとせず、場当たり的に対応しようとするクセがあるように感じました。
例えば、最初の実装ではMの値を制限とかしてもウェイト合計が非常に大きな値になってしまうことがあり、最終的にInfやNanが入ってくることがありました。
ウェイト値の正規化が正常に行われればそうそう起こらないはずなのですが、正規化がうまく行ってなかったりとかGI計算に入れるべきではない空のピクセルの結果を統合してしまったりとかが原因でした。
実はこの辺の問題についてはGeminiも併用しました。
Gemini自体には実装を求めず意見のみを求め、意見が妥当と判断した場合にCodexに新しい実装計画を立ててもらって実装してもらうという流れです。
実装自体は1つのAIに任せても大丈夫な印象ですが、その実装の拠り所となる理論とかは複数のAIの意見を利用したほうが良いかもしれません。
実装時にだいぶ楽だったのはベースとなるシェーダの実装や描画パスの追加、リソース管理など、自分でやるとコピペと修正を行う面倒な作業をやってもらえることです。
一部で不具合があったり無駄があったりはするのですが、それを考慮してもだいぶ楽でした。
自分の作業はこのように実装された部分の確認、修正が必要なら修正、理論的に間違ってるっぽい部分の修正とパラメータとかの調整という感じでした。
しかし、現状ではCodexは実行時不具合までは確認できません。
PIXの結果を見ながらどのパスで問題が発生しているか確認、場合によってはシェーダコードを修正して不具合の元となる情報をバッファに書き出すなどして確認とかの作業が必要ですが、Codexさんはやってくれないので自分でやることになります。
そうすると、もういい!俺がやる!ってなりがちです。
というか、不具合調査モードに入った場合、不具合調査→特定してCodexに指示→修正確認は面倒くさすぎです。
この場合は調査から修正までの流れは自分で全部やってますし、そのほうが早い上に精神衛生上も良いと思いました。
今回の実装はReSTIR GIと、デノイザーとしてSVGFを実装しています。
ReSTIR GIは二次反射は対応していませんが、屋外であればさほど問題ないかなと思っています。
DDGIを併用することで二次反射以降も対応できそうな気がしていますので、こちらもそのうち対応してみようかと思っています。
この場合、DDGIの更新頻度を下げないとパフォーマンス的に厳しいとは思うので、そこまでやるならRTXGIの改造も必要になるかもしれないです。
また、実装はDiffuseのみで、Specularには対応していません。
Specularに対応するのであればDiffuseとは分けてデノイズするとかしないといけないらしく、
SVGFは基本的なTemporalとSpatialのフィルタに加えて、PrePassを実装しています。
PrePassでは分散を利用した外れ値の調整と、簡易なバイラテラルフィルタを適用しています。
ReSTIRの結果はどうしてもノイジーになってしまうので、SVGFの時間方向フィルタがうまく適用されないDisocclusionに対してはノイジーな結果が短時間発生するのですが、PrePassを実装することでだいぶ軽減しているように見えます。
ファイアフライノイズも完全になくなっているわけではないですが、ある程度軽減しています。
クランプ用係数を大きくするとちょくちょくファイアフライノイズっぽいものが目立つようになるので効果はあるはずです。
実装して思ったことですが、空間的再利用には注意が必要ではないかという点です。
空間的再利用を行うとデノイザーの結果にモヤモヤしたアーティファクトがのることがあります。
しかし、だからといって適用しないと暗めになる部分もあったりして収束が甘くなるようにも感じます。
このあたりは私のReSTIR GIの実装がイマイチなのかもしれませんが、SVGF側が問題なのかもしれず、実アプリで利用するのであればもっと調整したりとかしないとダメかなと思います。
空間的再利用は検証するピクセル数を増やしてもあまり効果がないように感じます。
まったく利用しない場合と1ピクセルだけ利用する場合では確実に効果が出ることが分かるのですが、1ピクセルと4ピクセルだと正直見分けがつきません。
デノイズ前の結果を見ると少し違うことが分かるのですが、デノイズした結果については見分けがつかないです。
空間的再利用もパフォーマンス的にただではないので、最終結果に満足できるなら空間的再利用をしないのも検討してみたほうがいいかもしれません。
他にも再利用するピクセルが近いところばかりを選択すると明るい結果が局所的に増幅され、モヤモヤしたアーティファクトが目立つようになります。
遠すぎると空間的再利用がほぼ行われないのでそれはそれで問題です。
適度な距離で再利用する事が重要なようです。
サンプリングするピクセル位置をJitterで調整しているのですが、この部分ももっと高品質のノイズを利用するべきかもしれないです。
ヤコビアンの利用もデフォルトでは無効にしています。
ヤコビアンを利用すると壁際のライティング結果が過度に評価されてしまう場合があるようで、実際に有効無効で差が出る部分が出ることを確認しています。
しかし、見る人が見比べれば分かるという程度であることと、空をサンプリングした結果を再利用する際にヤコビアンを考慮したウェイト値が過度に大きくなったり小さくなったりする場面が見受けられました。
その結果としてファイアフライノイズが出ることがあり、得られる利点と欠点を考慮して無効にしています。
もちろんフラグで有効にできるので、サンプルを試す人はON/OFFの結果を比べてみてください。
多分、最終結果としてはほとんど違いがわからず、それでいて場所によって稀にファイアフライノイズが出てくるという結果になると思います。
結果は以下のようになっています。
上はGIなしでスカイライトが適用されている状態、下はReSTIR GIです。
トンネルの中はわかりやすいのではないかと思います。
GIなしでは一様な明るさになっていますが、ReSTIR GIではトンネルの入口・出口付近は明るめ、中間部は少し暗めになっています。
色が出ているのがわかりやすい部分を拡大したものが下の画像です。
左から、GIなし、DDGI、ReSTIR GIです。
GIなしは当然赤い庇が影響を与えていません。
DDGIは広範囲に赤い色が出ていますが、下方向を向いている周辺全体で影響が出ていて少しのっぺりとした印象を受けます。
ReSTIR GIは庇近くに強く赤い色が出ていて、遠い部分では影響が小さくなっています。
ReSTIR GIは単純に実装するだけなら比較的容易なのですが、調整は結構大変だと感じました。
また、デノイザーの品質がかなり重要です。
そしてDisocclusionやゴーストへの対応も必要になるので、今回の実装だけでは不十分でしょう。
何よりレイトレーシングはほぼ必須なので、ハードウェアによっては使えない、使えるけどパフォーマンス的に無理という場面が出てきます。
当然、TLASに含まれないメッシュやマテリアル対応も難しいので、ReSTIR GI自体を実装するだけですぐに使えるわけでもないです。
このように弱点の多い技術ではありますが、それを言うならどんなリアルタイムGI技術も一長一短ではあります。
それぞれの技術の特性を考慮して自身のエンジンに組み込むことが重要です。