RTXDIのminimal-sampleを理解する(1)
RTXDIとは?
GPU上かどうかにかかわらずレイトレーシングやパストレーシングを行う際の重要な課題の一つは、追跡する光線の軌跡(パスもしくはレイと呼ばれるもの)をどのように構築するかです。これはレンダラーの性能や画質などの特性に直結する問題です。たとえば、物体表面からの反射に限定すれば、最も簡単なパスの構築方法は、物体の表面から半球状ににランダムな方向を選択してパスを構築する方法があると思います。また、物体表面の反射特性に合わせて、より反射率の高い方向を高確率で選択する方法や、シーン上に存在する光源の方向にパスを構築する方法もあります。
このように、いろいろなパスの選択戦略があり、実際のレンダリングでは、これらを組み合わせて使うことがよくある思います。そして、最も理想的なパスの確率分布は、その物体表面から、観測者の方向へのRadianceに比例した確率分布といわれています。しかしこれは、一般的には解析的に解くことが極めて困難であることがほとんどです。なぜなら、物体表面の反射特性は分かっても、どの方向から強い光が差し込んでくるかはわかりません。その光も、シーン上に設定された光源からの直接光なのか、それとも何かほかの物体から反射された光なのかわかりません。
RTXDIは、光源からの直接光によって形成されるRadianceに対して、最適なパスの確率分布を形成するようにパスの選択をするためのNVIDIAのSDKです。名前の由来は、おそらくRTX Direct Illuminationです。
リポジトリ
GitHubのリポジトリがあるので、さっそくCloneしてみましょう。
以降の説明では基本的にCloneしたソースを読める状態にある前提で書いています。
https://github.com/NVIDIAGameWorks/RTXDI/
ドキュメント
RTXDIのSDKのドキュメントを見る前に、前提知識として、Resampled Importance Sampling(RIS)のアルゴリズムの基礎部分を理解した方がよいと思います。(これより先は、Resampled Importance SamplingをRISと省略します。)
“Importance Resampling for Global Illumination” by J. Talbot et al.
RTXDIのSDKには、その概要を把握するのに下記のドキュメントがありますが、これを読んで理解できる人は、この記事はここで読むのを終了していただいて、SDKのドキュメントやソースコードを直接参照した方が良いでしょう。 https://github.com/NVIDIAGameWorks/RTXDI/blob/main/doc/Integration.md
rtxdi-sampleとminimal-sample
このSDKにはサンプルプロジェクトが二つ付いています。
rtxdi-sampleは、RTXDIをパス選択の核として、RTXGIやNRDやDLSSを用いてレンダリングしています。またReGIRという、ワールド空間におけるRISも行っているので、かなり実践的なサンプルになっている一方で、初めのステップとして、RTXDIの動作を理解したい場合には不向きなサンプルです。
一方で、minimal-sampleは、設定を変更することで時間方向のRISや、BRDFに基づくサンプリングも無効にすることが出来ます。また、NRDによるデノイズも行っておらず、レンダリングは極力単純な形で留めてあります。そのため、RTXDIの核であるRISの仕組みや、その効果をわかりやすく見せてくれるサンプルになっています。本記事ではこちらのサンプルプログラムの動作を見ていきます。
minimal-sampleのスクリーンショット
端的にRTXDIの効果の一端を見るためにいくつかのスクリーンショットを用意しました。 まずは上記3枚は、RTXDIのパス選択候補を、8サンプル、16サンプルと増加させたものです。選択候補は増やしているのですが、実際にれらのサンプルでレイトレースを行った訳ではありません。レイトレースはあくまで1回のみ行います。
次は、16サンプル+BRDF2サンプルの場合です。こちらはレイトレース回数は、合計3回となります。BRDFサンプルによって、良い選択候補が見つかるサーフェースでの変化が顕著に見られます。
今回の記事では説明しませんが、Spatio-Temporalのパス選択候補を導入すると、上記のようになります。上記の16サンプルと同等の処理時間ですが、結果は圧倒的にこちらが優れています。デノイズ処理は一切入っていない状態でここまでレンダリングできれば、かなり高画質なレンダリングが期待できます。
minimal-sampleを読む前に(前提知識)
ここでサンプルプログラムのレンダリングを見る前に、簡単に触れておいた方が良い前提知識について説明します。
NVRHIとDonutフレームワーク
RTXDI SDKのほかに、minimal-sampleが依存している主なライブラリとして、DonutとNVRHIがあります。
NVRHIは、D3D12とVulkanを抽象化するためのグラフィックスAPIの抽象化レイヤーです。とはいえそれほど深い抽象化が行われているわけではありません。
Donutは、サンプルアプリケーションのフレームワークに相当する部分になります。シーンのロードやシェーダーの管理、デバッグUIの表示などを行っています。こちらもサンプル向けのフレームワークなので、シンプルに記述されています。今回のサンプルプログラムでは、それほど多数のDispatchが呼び出されるわけではないので、動作の理解に苦しむことはないかと思います。
RAB_プレフィックスについて
RTXDIのサンプルを見ると、RTXDI_プレフィックスの関数や構造体とは別に、RAB_プレフィックスの関数や構造体がたくさんあります。RABの意味はRTXDI Application Bridgeという意味で、その名の通り、RTXDIとアプリケーションの橋渡しの役目があります。
RTXDIがRISを行うときに必要になる情報は、アプリケーションのレンダラーと密接に関係しています。そのため、RTXDIがアプリケーション由来の情報と思われるものを取得する際は、RAB_プレフィックスのついた関数を呼び出します。RTXDIが呼び出している、RABプレフィックスのついた関数を実装するのは、アプリケーション側の責任となります。
しかし実際は、サンプルアプリケーションのRAB実装である、RtxdiApplicationBridge.hlslを改変する形で自身のアプリケーションに組み込む形になると思います。このようにすることで、アプリケーションごとに改変の必要な部分と不要な部分の切り分けを実現しています。
RTXDI特有のリソース
通常のG-Bufferなどに加えて、minimal-sampleでRTXDI SDKを導入したことで必要となるリソースは以下の通りです。RTXDIは、SDKの内部でリソースを確保することはありません。リソースの管理は、生成、破棄を含め、すべてアプリケーション側の管理となります。 SDK側からは必要に応じて、リソースのサイズやその中身がAPIを通じて提供されるので、アプリケーションはそれらを正しく管理しなくてはなりません。 以下のリソースは、ソースコード全体の把握では大切な要素ですが、RISのアルゴリズム部分ではあまり関わりが無いので読み飛ばしても問題ありません。
-
TaskBuffer
RTXDIは、毎フレーム直接光源情報のテーブルの更新を行っています。これはPrepareLightというGPU処理マーカーの中でComputeShaderとして行われています。この処理の入力として TaskBufferが必要となります。このバッファはPrepareLightsTask構造体の配列となっています。 このサンプルでは、シーン上でEmissiveサーフェースを持ったGeometry Instanceの個数分のバッファを確保しています。 -
LightBuffer
RTXDIがアクセスする光源の情報はすべてこのバッファに格納されます。個々の光源は、RAB_LightInfo構造体に格納されます。 このサンプルでは、Emmisiveのマテリアルが設定されたポリゴン一つ一つがEmissiveTriangleの光源としてこの配列に設定されます。 TaskBufferによって入力された情報をもとに、このバッファが構築されます。 -
GeometryInstanceToLightBuffer
Geometry Instanceごとに、そのInstanceに含まれるEmissiveTriangleの光源としてのLightBufferにおける先頭のインデックスを格納します。つまり、GeometryInstanceのインデックスからLightBufferを参照するときに使われるテーブルです。 -
NeighborOffsetBuffer
RTXDIがサイズを提供しと内容を指定します。スクリーンスペースでRISを行うときに参照するべきPixelへのオフセットになる値が格納されます。EvenとOddのフィールドがあるのでRTXDIが提供するNeighborOffsetCountの2倍の数で、RG8_SNORMのTypedBufferを確保します。 レンダリングの前に、RTXDIのFillNeighborOffsetBuffer()で取得できるバイト列をこのバッファに書き込む必要があります。 -
LightReservoirBuffer
RTXDIがサイズを提供し内容はComputeShaderで算出されます。 ReservoirBuffer一つあたりのサイズはsizeof(RTXDI_PackedReservoir) * context.GetReservoirBufferElementCount()で、RTXDIから提供されます。 これをアプリケーション側の好きな数だけ確保します。サンプルの初期値では3セット分のサイズのバッファを確保しています。 時間方向でRISを行う場合のためのバッファになります。
RTXDIのReservoirについて
(ここより以下、RTXDIもしくはRISの文脈で、“サンプル"と言っている場合は、レイトレーシングにおけるサーフェースと光源を結ぶパスを構築するためのLight Sampleを指します。サンプルアプリケーションのことでもなければ、テクスチャのサンプリングのことでもありません。)
RTXDI_Reservoir構造体はRISのReservoirとしての情報を保持します。Reservoirとは、RISをするためのサンプルの集合です。ただし、サンプルの集合の情報をすべて保持していたら、GPU上ではメモリが足りません。したがって、Reservoirは今まで生成してきたサンプルによる確率の計算と、現在そのReservoirで選択されているサンプルの情報を格納しています。具体的には、サンプルの選択確率に関する情報と、パスの接続対象なる光源のインデックス、その光源の表面における位置情報にあたるUVです。これがあれば、ワールド空間でパスを接続するべき位置(つまりは光源の表面位置)が計算でき、シェーディングを行った後にサンプルの確率密度を適用することができます。
Reservoirに関して全くイメージがわかないという場合は、まず初めに紹介した論文を軽く読んで、ReSTIRに関する論文、
“Spatiotemporal reservoir resampling for real-time ray tracing with dynamic direct lighting”, Bitterli et al. 2020
を読むとイメージできると思います。(もしこの二つを読んだならば、本記事は、この先読む必要がないでしょう)
Reservoirを操作する関数群
ここではRTXDIがReservoirを操作する関数群のなかで、最も基本的なものをザックリと説明します。
-
RTXDI_Reservoir RTXDI_Reservoir RTXDI_EmptyReservoir()
有効なサンプルが一つも格納されていない、初期化されたRTXDI_Reservoir
構造体を返します。 -
bool RTXDI_StreamSample( inout RTXDI_Reservoir reservoir, uint lightIndex, float2 uv, float random, float targetPdf, float invSourcePdf)
reservoir
- 格納するReservoirlightIndex
,uv
- 追加するLight Sampleの情報random
- Light Sampleを更新するかどうかをDraw(選択)するときに使う乱数targetPdf
- RISにおけるTarget PDFinvSourcePdf
- 追加するサンプルを生成する確率の逆数
一つのサンプルをReservoirに追加して、現在このReservoirの中で選択されているサンプルを更新します。
targetPDF
は実際は正規化されたPDFである必要はなく、単なるウエイト値で問題ありません。一方で、invSourcePdf
は、サンプルの発生確率に基づいたPDFである必要があります。関数内部では、RIS WeightがtargetPdf * invSourcePdf
で計算され、Reservoir構造体のweightSum
に加算されます。Reservoirの保持サンプル数M
もインクリメントされます。また、与えられたrandom
でサンプルの選択を行い、新たに追加されたサンプルが選択された場合はReservoir内部の選択サンプルの情報を更新します。その場合は、返り値としてtrueを返します。 -
void RTXDI_FinalizeResampling( inout RTXDI_Reservoir reservoir, float normalizationNumerator, float normalizationDenominator)
通常は、Reservoirへのサンプル追加が終わった段階で呼び出す処理で、Reservoirに蓄積されたサンプルの確率と、現在選択されているサンプルの確率から、選択されているサンプルの評価値(つまりはシェーディング結果)に乗算するべき値 (Importance Samplingにおける 1/PDF) を計算します。
normalizationNumerator
,normalizationDenominator
は蓄積されたサンプルのweightSum
を正規化するときの係数です。単独のReservoirであれば、Reservoirに蓄積されたサンプル数の逆数である、1/M
が係数として適切です。この場合、1/targetPDF
* (1/M
*weightSum
)を計算し、これをweightSum
に代入します。
したがって、この関数を呼び出す前と後では構造体メンバーのweightSum
の値の意味が変わります。呼び出す前はReservoirに蓄積されたサンプルのウエイトの合算で、呼び出した後は、選択されたサンプルの評価値に乗算するべき値となります。 -
float RTXDI_GetReservoirInvPdf(const RTXDI_Reservoir reservoir)
Sampleの評価値に乗算するべき係数(Importance Samplingにおける1/PDF)を返します。
内部の処理はweightSum
の値を返すだけです。事前にFinalizeResampling()
を呼ぶ必要があります。 -
bool RTXDI_CombineReservoirs( inout RTXDI_Reservoir reservoir, const RTXDI_Reservoir newReservoir, float random, float targetPdf)
二つのReservoirを結合します。
まず、結合前に結合される側のnewReservoir
はRTXDI_FinalizeResampling()
で正規化されている必要があります。
引数targetPdf
は、結合されるnewReservoir
で選択されているサンプルの、結合先ReservoirにおけるtargetPdf
になります。結合される側と結合先でのtargetPdfが同じ場合は、引数のtargetPdf
は、newReservoir
に保存されているサンプルのtargetPdfを指定すればよいです。
結合後は、現在どちらかのReservoirで選択されているサンプルが選択サンプルになります。これをrandom
を用いて決めます。選択サンプルが変更される場合は返り値としてtrueを返します。
minimal-sampleの中身
~Spatio-TemporalでRISを行わない場合のレンダリング~
レンダリングを理解するうえでの前提知識が整ったので、さっそく一番簡単なケースのレンダリングを見たいと思います。 Spatio-TemporalでのRISは、RTXDIの大きな特長の一つですが、今回は単純化のために無効化した状態でサンプルコードを読み、 RTXDIの最もシンプルな形を理解するこにします。このサンプルアプリケーションは、“Enable Resampling"というDebugUIが用意されているのでこれをDisableにします。しかしこれはSpatio-TemporalのRISを行うかどうかを切り替えるためのフラグで、RTXDIを完全にDisableにするためのものではありません。また、BRDF Cutoffも、簡単のため0.0が設定されていると仮定します。
レイトレーサー本体の概要
Renderer.hlslのmain()がレイトレーサー本体のシェーダーコードです。カメラからレイを飛ばして、GBuffer相当の情報を取得している部分は特に難しい部分はないと思います。サーフェースにヒットした場合は、乱数シーケンスを初期化して、RTXDI_SampleParamterにサンプリングの設定をしています。その後の主な処理の流れは以下の通りです。
- 空のReservoirに、
RTXDI_SampleLocalLights()
(後述)で計算されたReservoirを結合する RTXDI_SampleBrdf()
(後述)で計算されたReservoirを結合するRTXDI_FinalizeResampling()
でReservoirの正規化を行う- 選択パスが、RTXDI_SampleLocalLights()だったら、ShadowRayをキャストして、Visibilityをチェック
- ShadeSurfaceWithLightSample()で、Reservoirで選択されたサンプルを使ってシェーディングを行う
- 再びShadowRayをキャストしてVisibilityをチェック
Spatio-TemporalのRISが無効化されている場合は、最後の2度目のVisibilityチェックは必要ないはずです。しかし、大まかな処理の流れとしてはこのようになっています。以上を簡単に言い換えれば、1バウンスのライトサンプル(NEE)と、BRDFサンプルのMulti Importance Samplingのレイトレーサーが実装されているといえると思います。
RTXDI_SampleLocalLights()
さっそくですが、1番めの処理についてです。この関数はResamplingFunctions.hlsli
に実装されています。
この関数は、numLocalLightSamples
で指定された数だけ、サンプルを構築してReservoirに蓄積する処理を行います。このサンプルアプリケーションの中の様々な個所で行われているRISの最も基本的な形になっています。
個々のサンプルの構築
RTXSDKは事前にLight DataバッファにLocal Light (つまりは Emissive Triangle)のリストを構築しています。まず、このリストから、単純に乱数でLocal Lightを選択します。さらに乱数を2つ生成して、光源の三角形上の点を決定して、その位置に向けて、プライマリレイがヒットしたサーフェースからパスを構築します。
構築されたパスのPDFは、RTXDI_LightBrdfMisWeight()
で計算され、blendedSroucePdf
に代入されます。
blendedSourcePdfの計算
blendedSourcePdf
は、RISにおけるsourcePDFなので、実際のパス生成確率に即したものでなければなりません。
この計算を行っているのは、RTXDI_LightBrdfMisWeight()
関数です。
まず、ライトサンプルの確率は
- ライトの選択確率(単なる乱数選択なので、ライトの個数の逆数)
- ライト上の特定の方向に向けたレイを選択する確率(サーフェースから見たLocal Lightの見かけの立体角の逆数)
の乗算で計算できます。
そして、BRDFサンプルの確率は、RAB_GetSurfaceBrdfPdf()
で計算されるので、アプリケーション側の処理となりますが、
- DiffuseRayの場合、CosineWeightedのPDF
- SpecularRayの場合、GGX_VNDFのPDF
- 上記いずれかをDiffuseProbabilityで選択
したがって、
DiffuseProbablity * CosineWeightedPDF + (1 - DiffuseProbability) * GGX_VNDF_PDF
でBRDFサンプルの確率が計算できます。
1ピクセルあたりで、RISで検討されるライトサンプル数とBRDFサンプル数はDebug UIの設定で決まっていて、numLocalLightSamples
とnumBrdfSamples
に設定されます。このサンプル数を用いて、これらはバランスヒューリスティックで結合されます。これは通常のMulti Importance Samplingと同様の考え方です。
注意点なのですが、RTXDI_LightBrdfMisWeight()
関数の最後では、lightSolidAnglePdf
に設定された"ライト上の特定の方向に向けたレイを選択する確率"で除算しています。ここはRTXDIのトリッキーな部分です。あくまで、実際の"sourcePdf"は、この除算の前の値です。
しかし、RTXDIではtargetPdf
もlightSolidAnglePdf
で除算するので、計算のつじつまが合うようになっています。また、taregetPdf
は、シェーディング結果を除算しますが、シェーディング結果もlightSolidAnglePdf
で除算されるので、こちらも計算のつじつまが合う仕組みになっています。
targetPdfの計算
説明が多少前後しましたが、targetPdf
の計算についてです。targetPdf
はRISにおいて、積分可能ではないが、理想的なサンプルの確率密度です。(この値は、簡単には積分できず大きさが正規化できないので、PDFと呼ぶべきではなく、単にWeightと呼ぶべきかもしれません。)
targetPdf
はレンダリングの文脈では、サーフェースがカメラ方向に出すRadianceに比例したレイの分布になるのが一番望ましいです。言い換えれば、カメラの方に最も強く反射される光源へのレイを重点的にサンプリングする分布です。これは、光源のサーフェースでのカメラ方向への反射を計算すればわかります。しかし、光源とサーフェースがVisibleかどうかの判断は、実際にShadow Rayをトレースしなくては分かりません。しかし、これを行えば、実際にレイトレースを行ってシェーディングする処理とまったく変わらなくなり、単にレイのサンプル数を増やすことと同義です。これでは、RISの意味がなくなってしまいまいます。
targetPdf
の計算では、シェーディングの中で最も処理負荷の高いShadow Rayのテスト処理を省略した値(つまりい光源とサーフェースがVisibleかどうかの判断をせずにシェーディングした結果)が用いられます。
実際の計算は、RtxdiApplicationBridge.hlsli
のRAB_GetLightSampleTargetPdfForSurface()
に実装されています。この関数はShadeSurfaceWithLightSample()
という関数を呼び出して、シェーディングの計算を行っています。算出された値の輝度値が、そのままtargetPDF
として扱われます。また、blendedSourcePdf
の項で説明した通り、シェーディングの計算の最後で、値はlightSolidAnglePdf
で除算されます。
Reservoirにサンプルを追加する計算
上記の通り、blendedSourcePdf
とtargetPdf
の計算が完了すれば、Reservoirにサンプルを追加する処理は簡単です。
RTXDI_StreamSample()
に、blendedSourcePdf
とtargetPdf
を乱数と共に渡して、渡したサンプルが選択された場合は、現在選択中のサンプルの情報を更新します。
サンプル構築後の処理
numLocalLightSamples
の数だけサンプルを構築し、Reservoirに蓄積した後は、現在Reservoirが選択中のサンプルの情報と、Reservoirに蓄積されたRISの情報のみが残ります。ここまでで複数のサンプルを検討していますが、実際にレイトレース処理は行っていません。しかし、Reservoirには、一番選択するべきサンプルの情報が残っています。
サンプルを構築するループの直後に、RTXDI_FinalizeResampling()
を呼び出しています。ここでの正規化の係数は、1.0/numLocalLightSamples
と思われるかもしれません。しかし実際のプログラムでは、1.0/numMisSamples
で正規化されています。またReservoirのサンプル数M
も1.0に設定しています。これについては後ほど説明します。
RTXDI_SampleBrdf()
この関数は、numBrdfSamplesで指定された数だけ、サーフェースのBRDFをもとにサンプルを構築してReservoir蓄積する処理を行います。
この処理は、上記で説明したRTXDI_SampleLocalLights()
の処理と対を成す処理です。
個々のサンプルの構築
まず、RAB_GetSurfaceBrdfSample()
を呼び出して、BRDFに基づいたサンプルを構築します。そして、実際にレイトレースを行い、Local Light(Emissive Triangle)にHitするかをテストします。Hitしなかった場合は、このサンプルの処理は終了しReservoirに関する処理は行われません。(しかし、このサンプルがReservoirに蓄積されないというわけではなく、正確にはtargetPDF=0として蓄積された扱いになります。これはReservoirの結合時の処理を見ると判明します。)
一方でLocal LightにHitした場合は、targetPdf
とblendedSourcePdf
をそれぞれ計算します。計算は、RTXDI_SampleLocalLights()
と全く同じ計算になります。
サンプルの構築後の処理
ここも、RTXDI_SampleLocalLights()
と基本的に同じ計算になります。
サンプル構築のループの直後に、RTXDI_FinalizeResampling()
を呼び出しています。ここでの正規化の係数は、Shadow Rayによって棄却されたサンプルを含めるなら、1.0/numBrdfSamples
であるべきと思われるかもしれません。しかし実際のプログラムでは、1.0/numMisSamples
で除算されています。またReservoirのサンプル数M
も1.0に設定しています。これについては後ほど説明します。
Light SampleとBRDF SampleのReservoirの結合処理
再び、main()
の処理に戻ります。RTXDI_SampleLocalLights()
によって構築されたlocalReservoir
と、RTXDI_SampleBrdf()
によって構築された、brdfReservoir
を結合する処理を見ていきます。
RTXDI_CombineReservoirs()の処理
まず、RTXDI_CombineReservoirs()
を呼ぶ前に、結合される側のReservoirは、RTXDI_FinalizeResampling()
が呼ばれている約束になっています。したがって、結合される側のweightSum
は、Finalize前の変数で解釈すると1/targetPDF * 1/M * weightSum
に相当する値が設定されています。(ただし1/M
はFinalize時に引数で渡す正規化係数)
これに、構造体に格納されているM
と、引数で渡されたtargetPdf
を乗算したものが、risWeight
という変数に設定されます。逆算すれば、risWeight
は、元のweightSum
に(引数の)targetPdf / (構造体に保存されている)targetPdf
を乗算したものですから、もしも、1/M
で正規化されていて、targetPdf
が同じならば、結局のところ元のweightSum
ということになります。
しかし、RTXDI_CombineReservoirs()
の引数に渡すtargetPdf
は、結合元のReservoirで現在選択されているサンプルの、結合先のReservoirにおけるtargetPdf
なので、もしも、結合先でtargetPdf
が異なる場合は、その比がweightSum
に乗算されることになります。しかし、今回のサンプルプログラムでは、Spatio-TemporalなRISの結合を行わないので、targetPdf
は結合の前後で変化しないので、この計算について深く考える必要はありません。
計算されたrisWeight
は、結合されるReservoir全体の、結合先Reservoirにおけるウエイトに相当する値です。
後は、サンプル数M
を合算し、weightSum
にrisWeight
を加算して、選択サンプルを乱数で決定することで、Reservoirの結合が完了します。
localReservoir と brdfReservoir の結合
RTXDI_SampleLocalLights()
の項で説明したとおり、localReservoir
は、1.0/numLocalLightSamples
で除算して正規化するところを、1.0/numMisSamples
で除算したうえに、サンプル数 M
を1に設定していました。
これを、RTXDI_CombineReservoirs()
の結合される側のReservoirとして処理をすると、risWeight
は、Finalize前の変数で解釈すると以下のようになります。
1/numMisSamples * weightSum
この式をわかりやすく書き換えると、以下のようになります。
numLocalLightSamples/numMisSamples * 1/numLocalLightSamples * weightSum
つまり、localReservoir
の正規化処理と、localReservoir
とbrdfReservoir
の、それぞれのサンプル数に基づくバランスヒューリスティックによる結合を同時に処理しているわけです。
同様に、brdfReservoir
の結合時のrisWeight
は、
numBrdfSamples/numMisSamples * 1/numBrdfSamples * weightSum
と解釈できます。(ここで、brdfReservoir
の生成時に、Shadow RayがMissしてサンプルが破棄されているにも関わらずtargetPdf
がゼロのサンプルとして扱われているという解釈ができるわけです。)
最後に、両者の結合後に、RTXDI_FinalizeResampling()
を正規化係数1.0で呼び出していますが、両者の正規化はMISのウエイトによって行われているので、計算のつじつまが合うわけです。
最後のレイトレースとシェーディング処理
ついに、最終的に採用すべきサンプルが確定し、乗算すべきPDFの算出も完了しました。
あとはShadow Rayをキャストして、Visibilityを確認すればよいのですが、brdfReservoir
のサンプルはその生成過程ですでにShadow Rayを使ってVisibilityを確認しているので、もし、こちらのReservoirからサンプルが採用された場合は、この作業は不要なのでスキップするように処理が書かれています。localReservoir
側からからサンプルが選択された場合のみShadow Rayのトレースを行います。
シェーディング関数のShadeSurfaceWithLightSample()
は、RISの過程で何度も呼び出しているので説明不要ですが、ここでもsolidAndlePDF
が除算されているので、PDFとの計算のつじつまが合うわけです。
RTXDI_GetReservoirInvPdf()
は、既にFinalizeされているReservoirに対して呼び出す関数で、単にweightSum
を返します。Finalizeが行われていればそこには、PDFの逆数に相当する値が格納されているはずです。
シェーディングが終われば、TonemappingをかけてUAVに書き出すと、全体の処理が完了します。
まとめ
最後まで読んじゃった人は「にゃ~ん」ってつぶやいてほしいです。