こんにちわ。Pocolです。
今日は,タイルシェーディングについての実装メモを書くことにします。
カテゴリー: Direct3D
Direct3D関係のお話
サヨナラGLSL…
こんにちわ,Pocolです。
久しぶりにa3dの更新作業をやっています。
今日は,ついにGLSLから脱却する対応を入れました。
…といっても大したことはしていなくて,DXCからSpir-Vを生成するようにしただけです。
ただ,普通のHLSLの記述だと対応できないそうなので,マクロをかまして対応するという感じにしました。
マクロの定義は下記の通り。
#ifdef __spirv__ #define A3D_LOCATION(index) [[vk::location(index)]] #define A3D_RESOURCE(var, reg, index) [[vk::binding(index)]] var #define A3D_FLIP_Y(var) var.y = -var.y #else #define A3D_LOCATION(index) #define A3D_RESOURCE(var, reg, index) var : register(reg) #define A3D_FLIP_Y(var) #endif
Vulkanの座標系はDirectXと比べて上下逆になる座標系なので,フリップ用のマクロも定義してあります。
実際のシェーダは下記のように書きます。
/////////////////////////////////////////////////////////////////////////////////////////////////// // VSOutput structure /////////////////////////////////////////////////////////////////////////////////////////////////// struct VSOutput { A3D_LOCATION(0) float4 Position : SV_POSITION; A3D_LOCATION(1) float2 TexCoord : TEXCOORD; }; /////////////////////////////////////////////////////////////////////////////////////////////////// // PSOutput structure /////////////////////////////////////////////////////////////////////////////////////////////////// struct PSOutput { A3D_LOCATION(0) float4 Color : SV_TARGET0; }; //------------------------------------------------------------------------------------------------- // Samplers and Textures //------------------------------------------------------------------------------------------------- A3D_RESOURCE(SamplerState ColorSmp, s0, 1); A3D_RESOURCE(Texture2D ColorMap, t0, 2); //------------------------------------------------------------------------------------------------- // ピクセルシェーダメインエントリーポイントです. //------------------------------------------------------------------------------------------------- PSOutput main(VSOutput input) { PSOutput output = (PSOutput)0; output.Color = ColorMap.Sample( ColorSmp, input.TexCoord ); return output; }
あとは,dxc.exeで -spirv のオプションを付けてコンパイルするだけです。
DirectX用とVulkan用のシェーダバイナリの両方が1つのシェーダコードから生成できます。
これで,GLSLともようやくサヨナラです。
GPU最適化備忘録
こんにちわ,Pocolです。
今日はGPU最適化のための自分用のメモを書いておこうと思います。
自分がある程度理解できればいいことを前提に書いているので,「そもそも間違っている」とか「全然正しくない」とか「こうした方がもっといい」とかあれば,ご指摘ください。
P3 Method
基本的には,Louis Bavoil氏が書いている「The Peak-Performance-Percentage Analysis Method for Optimizing Any GPU Workload」に従ってボトルネックを探し,改善を行っていきます。GDCでも講演があるので,興味がある方はそちらの動画を見ると良いと思います。
この手法は次のステップで最適化を行います。
(1) “Top SOL%”の値をチェックする。
– (A) 80%より大きいの場合: SOLユニットから処理を取り除くことを試みる。
– (B) 60%より小さいの場合: top SOL%の値が増加するように試みる。
– 上記以外の場合: (A)と(B)の両方を行う。
(2) Top SOLユニットがSMの場合(あるいは,SOL%の項目が近い場合)
“SM Throughput for Active Cycles”の値をチェックする。
– (C) 80%より大きい場合: 命令群を適宜スキップしてみたり、特定の計算をルックアップテーブルに移行することを検討してみる。
– (D) 60%より小さい場合: SM占有率(実行中のアクティブワープ数)の増加を試みる & SM発行ストールサイクルの数を減らすことを試みる
– 上記以外の場合: (C)と(D)の両方を行う
(3) その他がTop SOLユニットの場合、そのユニットに送られる作業量を減らすことを試みる。
ここまで出てきた用語は次の意味です。
- SOL:Speed Of Light。最大スループットを意味する。
- SM:Streaming Multiprocessor。共有メモリやコンスタンスとキャッシュ,テクスチャキャッシュ,複数のスカラープロセッサなどから構成されるもの。ものすごく雑に言えばUnified Shaderのこと。
- Throughput。ハードウェアが単位時間あたりに処理できるデータ量のこと。あるいは,その数値を使ってデータ処理能力やデータ転送速度を表す。
さて,最適化する上で肝心なのは「Top SOLをどうやって調べるか?」ということになります。これはNVIDIA Nsight Graphicsを使って調べることができます。
まず,Nsightを立ち上げる前にパフォーマンスカウンターなどを取得できるように設定を変更しておきます。NVIDIAコントロールパネルを開きます。Windowsのタスクバー右下にある^をクリックして,NVIDIA設定を右クリックし,NIVIDIAコントロールパネルを選択します。選択してもコントロールパネルが開かない場合は,何らかの異常が発生している恐れがあるのでグラフィックスドライバーを再インストールして改善するか試してみてください。コントロールパネルを開いたら,メニューの「デスクトップ」から「開発者設定を有効にする」にチェックを入れておきます。これでカウンター等の値がとれるようになるはずです。
続いて計測を行います。Nsight Graphicsを立ち上げFrame Profilerとしてアプリケーションを立ち上げます。立ち上げ方はConnect to processのウィンドウで,Activityの項目をFrame Profilerに設定して,Application Executableにアプリケーション実行パスを指定,Working Directoryに作業ディレクトリを設定してください。自動的にアタッチする場合はAutomatically ConnectにYesを,Steamなどのようにいったん別exeをかましてから立ち上げる場合はNoに設定しておき,Target PlatformでAttachタブを選択してから,プロセスにアタッチするという方法を取ります。
アプリケーションが立ち上がったら,左上にオーバーレイが表示されるので,F11を押して測定したいフレームをキャプチャーします。
キャプチャーすると,画面右上にRange Profilerというものが表示されるので,調べたいドローコール部分をRange Profilerでクリックします。クリックすると,少し時間をおいてから次のようにRange InfoやPipeline Overviewなどが表示されます。
あとは,Pipeline Overviewに表示されるSM / L2 / VRAM / CROP / RAS の値を見て上記のステップに沿って改善を行います。
(A) Top SOL% > 80% の場合
GPU上で非常に効率的に動作していることが分かります。高速化するためには,このTop SOLユニットを処理から削除し,他のユニットのボトルネックを探していきます。例えば SM が Top SOL ユニットの場合は,命令群をスキップしてみたりなど。もう一つの例としては構造化バッファのロードを定数バッファに移して高速化するなどです。
(B) Top SOL% < 60% の場合
トップ SOL%の値が60%未満場合、トップ SOL ユニットと SOL% が低い他のすべての GPU ユニットは、使用率が低い(アイドルサイクル)、非効率的に動作している(ストールサイクル)、または与えられた作業負荷の仕様により高速パスに当たらないということを意味します。このような状況の例としては、以下のようなものがあります。
- アプリケーションが一部CPUに制限される
- Wait For IdleコマンドやGraphic←→ComputeスイッチでGPUパイプラインを何度も消耗している
- テクスチャオブジェクトからのTEXフェッチは、フォーマット、次元、またはフィルタモードによって、設計上、スループットが低下して実行されます(GTX 1080のこれらの合成ベンチマークを参照)。例えば、3Dテクスチャをトライリニアフィルタリングでサンプリングする場合、50%のTEX SOL%が期待されます。
- TEXやL2ユニットのキャッシュヒット率が低い、VRAMアクセスがまばらでVRAM SOL%が低い、GPU VRAMではなくシステムメモリからVB/IB/CB/TEXをフェッチするなど、メモリサブシステムの非効率性。
- 入力アセンブリの32ビットインデックスバッファのフェッチ(16ビットインデックスに比べ半減)
この場合、トップSOL%の値を使用して、非効率な処理を削減することによってこの処理負荷で達成できる最大利得の上限を導き出すことができます。
(C) SM Throughput For Active Cycles > 80% の場合
SM がトップ SOL ユニットで、「SM Throughput For Active Cycles」が 80%より大きい場合、現在の処理負荷は主に SM スケジューラの発行率によって制限されているため、SM の占有率を上げてもパフォーマンスは大きく向上しません(少なくとも、処理負荷は 5%以上増加しない)。
この場合,次のステップは、SMスケジューラの帯域幅を飽和させているのはどのような命令かを把握することです。典型的なのは算術命令(FP32や整数演算)ですが、テクスチャフェッチや共有メモリアクセスなどのメモリ命令もあり得えます。また、SMがトップSOLユニットで、SM Throughput for Active Cyclesが80%以上のワークロードでは、TEXユニットもSMのSOL%に近い値でなければ、TEX命令(SRVおよびUAVアクセス)が性能を制限している可能性はないでしょう。
(D) SM Throughput For Active Cycles < 60% の場合
このGTC 2013の14分の講演のスライド15にあるように、あるワープ命令が発行できない場合(オペランドの準備ができていない、あるいは実行に必要なパイプラインサブユニットの準備ができていないため、これをワープストールと呼びます)、SM命令スケジューラは、別のアクティブワープに切り替えてレイテンシーを隠蔽しようとします。そこで、SMスケジューラがSMアクティブサイクルあたりにより多くの命令を発行できるようにするには、2つの方法があります。
(1)SMの占有率(スケジューラが切り替え可能なアクティブなワープの数)を上げる
(2)SM発行ストール待ち時間を短縮する(ワープがより少ないサイクルでストール状態に留まるようにする)。
アプローチ1:SM占有率を増加する
SMがトップSOLユニット(またはそれに近い)、「アクティブサイクルのSMスループット」60%未満であれば、SMの占有率を上げるとパフォーマンスが向上するはずですが、まず何が制限になっているかを把握する必要があります。
ピクセルシェーダとコンピュートシェーダで最も一般的な SM 占有率のリミッタは、シェーダが使用するスレッドごとの ハードウェアレジスタの数です。
ハードウェアのレジスタ数が最大理論占有率(アクティブサイクルあたりのアクティブワープ数)に与える影響は、CUDA Occupancy Calculatorで確認できます。
Maxwell、Pascal、Volta GPU では、レジスタの他に、以下のリソースでも SM 占有率を制限することができます。
グラフィックスシェーダについて:
- 頂点シェーダー出力アトリビュートの合計サイズ。
- ピクセルシェーダーの入力アトリビュートの合計サイズ
- HS、DSまたはGSの入力および出力アトリビュートの合計サイズ。
- ピクセルシェーダの場合、ピクセルワープのアウトオブオーダー完了(通常、動的ループや早期終了ブランチなどの動的制御フローに起因する)。CS のスレッドグループは任意の順序で完了できるため、CS にはこの問題があまりないことに注意してください。Gareth Thomas & Alex Dunnによる「Practical DirectX 12」についてのGDC 2016講演のスライド39をご覧ください。
コンピュートシェーダについて:
- スレッドグループのワープがSM上でall-or-none方式で起動するため、スレッドグループのサイズはSMの占有率に直接影響します(つまり、スレッドグループのすべてのワープが必要なリソースを利用でき、一緒に起動するか、または何も起動しないかです)。スレッドグループのサイズが大きくなると、共有メモリやレジスタファイルなどのリソースの量子化が粗くなります。アルゴリズムによっては純粋に大きなスレッドグループを必要とする場合もありますが、それ以外の場合は、開発者はできるだけスレッドグループのサイズを64スレッドまたは32スレッドに制限するようにすべきです。これは、64 または 32 スレッドサイズのスレッドグループが、シェーダプログラムに最適なレジスタターゲットを選択する際に、シェーダコンパイラに最も柔軟性を与えるからです。
- さらに、レジスタ使用量が多く (>= 64)、SM (HLSL の GroupMemoryBarrierWithGroupSync()) におけるスレッドグループバリアのストール時間が長いシェーダでは、スレッドグループのサイズを 32 に下げれば、64 と比較してスピードアップする可能性もあります。64 レジスタのガイダンスにより、SM あたり最大 32 スレッドグループの制限(CUDA Occupancy Calculator のアーキテクチャ依存の「Thread Blocks / Multiprocessor」制限)が、SM 占有率の主要制限要因にならないようにします。
- CUDA Occupancy Calculatorに様々な数値を入力することで、スレッドグループごとに割り当てられる共有メモリの総バイト数もSMの占有率に直接影響を与えることが分かります。
- 最後に、短いシェーダ(例えば、TEX命令と2つの算術命令)を持つDispatchコールのシーケンスでは、SMの占有率は、上流ユニットのスレッドグループの起動率によって制限される場合があります。この場合、連続したDispatchコールをマージすることが有効な場合があります。
CUDA Nsightのドキュメントページ「Achieved Occupancy」には、Compute ShadersのSM占有率制限の候補がいくつか挙げられています。
- スレッドグループ内の処理負荷が偏っている。
- 起動したスレッドグループが少なすぎる。これは、GPU Wait For Idles の間に SM を完全に占有するのに十分なワープを起動しないグラフィックシェーダの問題にもなりえます。
注:スレッドグループのサイズを小さくするには、実際には2つのアプローチがあります。
- アプローチ1:スレッドグループのサイズをN倍に下げ、同時にグリッドの起動次元をN倍にする。
- アプローチ2:N>=2個のスレッドの作業を1個のスレッドに統合する。これにより、マージされたN個のスレッド間でレジスタを介して共通データを共有したり、アトミック演算(HLSLのInterlockedMinなど)を用いて共有メモリではなくレジスタで削減を実行したりすることができます。さらに、このアプローチには、N個のマージされたスレッド間でスレッドグループの統一操作を自動的に償却する利点もあります。しかし、この方法によるレジスタの肥大化の可能性には注意する必要があります。
注:もし、あるワークロードの SM 占有率が、主にフルスクリーン・ピクセルシェーダとコンピュー トシェーダのどちらでレジスタカウントが制限されているかを知りたい場合は、次のようにしてください。
- スクラバーで追加し… “Program Ranges” を実行し、調査したいシェーダプログラムの範囲を見つけます。Program Range を右クリックし、Range Profiler を起動し、Pipeline Overview Summary の “SM Occupancy” 値を確認します。
- スクラバーの「Time(ms)」行をクリックして、Range内のいくつかのレンダーコールを選択します。タブを API Inspector に切り替え、ワークロードのサイクルのほとんどを占めるシェーダステージ(PS または CS)を選択し、シェーダ名の横にある「Stats」リンクをクリックします。
- 「Shaders」Nsightウィンドウが表示され(下図14参照)、「Regs」列にシェーダのハードウェアレジスタ数が表示されます。シェーダの統計情報が入力されるまで数秒待つ必要があることに注意してください。
- CUDA Occupancy Calculator グラフを使用して、このレジスタカウントに関連する Max Theoretical Occupancy を調べ、このシェーダの Range Profiler が報告する実際の 「SM Occupancy」 と比較します。
- もし、達成した占有率が最大占有率よりずっと低ければ、SMの占有率はスレッドごとのレジスタの量だけでなく、他の何かによって制限されていることが分かります。
あるシェーダに割り当てられるレジスタの総数を減らすには、DX シェーダのアセンブリを見て、シェーダの各ブランチで使用されるレジスタの数を調べればよいです。ハードウェアは、最もレジスタを必要とするブランチに対してレジスタを割り当てる必要があり、そのブランチをスキップするワープは、最適ではない SM 占有率で実行されます。
フルスクリーンパス(ピクセルまたはコンピュートシェーダ)の場合、この問題に対処する典型的な方法は、ピクセルを異なる領域に分類するプリパスを実行し、各領域に対して異なるシェーダの並べ替えを実行することです。
- コンピュートシェーダについては、このSIGGRAPH 2016のプレゼンテーションでは、シェーダの順列ごとにスレッドブロックの数を変えながら、画面上の異なるタイルに特殊なコンピュートシェーダを適用するためにDispatchIndirectコールを使用したソリューションが説明されています。
“Deferred Lighting in Uncharted 4” – Ramy El Garawany (Naughty Dog). - ピクセルシェーダについては、異なる特殊化アプローチを使用することができます。フルスクリーンのステンシルバッファをプリパスで満たし、ピクセルを分類することができます。それから、ピクセルシェーダ実行の前に起こるステンシルテストに依存し(これは我々のドライバによって自動的に行われるべきです)、現在のシェーダの順列が触れていないピクセルを破棄するステンシルテストを使用することによって、複数の描画コールを効率的に実行することができます。このGDC 2013のプレゼンテーションでは、このアプローチでMSAA遅延レンダリングを最適化します。”The Rendering Technologies of Crysis 3″ – Tiago Sousa, Carsten Wenzel, Chris Raine (Crytek).
最後に、あるコンピュートシェーダのSM占有率制限をよりよく理解するために、我々のCUDA Occupancy Calculatorスプレッドシートを利用することができます。使用するには、GPU の CUDA Compute Capability とシェーダのリソース使用量(スレッドグループサイズ、Shader View からのレジスタ数、共有メモリサイズ(バイト))を記入するだけです。
アプローチ2:SM発行ストール待ち時間を減らす
SM占有率を上げる以外の方法でSM Throughput For Active Cyclesを上げるには、SM issue-stall cyclesの回数を減らすことです。これは、命令発行サイクル間のSMアクティブサイクルで、オペランドの1つがレディでない、または命令を実行する必要があるデータパス上のリソース競合が原因で、ワープ命令がストールしている間です。
PerfWorksのメトリクスsmsp__warp_cycles_per_issue_stall_{reason}は、ワープが{reason}で停止した命令イシュー間の平均サイクルを示しています。PerfWorksメトリックsmsp__warp_stall_{reason}_pctは、サイクルごとにその理由で停止したアクティブなワープの%です。これらのメトリックは、Range ProfilerのUser Metricsセクション、およびSM Overview Summaryセクションで、降順にソートされて公開されています。ワープが停止する理由としては、以下のようなものが考えられます。
- “smsp__warp_stall_long_scoreboard_pct” PerfWorks メトリクスは、L1TEX (local, global, surface, tex) オペレーションのスコアボード依存を待つためにストールしたアクティブワープのパーセンテージを示します。
- “smsp__warp_stall_barrier_pct” PerfWorks指標は、スレッドグループのバリアで兄弟ワープを待つためにストールしたアクティブワープのパーセンテージを示します。この場合、スレッドグループのサイズを下げるとパフォーマンスが向上する場合があります。これは、各スレッドが複数の入力要素を処理するようにすることで実現できる場合があります。
“sm__issue_active_per_active_cycle_sol_pct” が 80% より低く、 “smsp__warp_stall_long_scoreboard_pct” がワープストールの理由のトップなら、そのシェーダは TEX-latency limited であることが分かっているはずです。これは、失速サイクルのほとんどが、テクスチャフェッチ結果との依存関係 から来ていることを意味します。この場合は…
- シェーダのコンパイル時に反復回数がわかるループがある場合(ループ回数ごとに異なるシェーダの並べ替えを使用する場合もある)、HLSL の [unroll] ループ属性を使用して FXC にループを完全に展開させるようにしてみてください。
- シェーダが完全にアンロールできない動的ループ(たとえば、レイマーチ ングループ)を実行している場合は、テクスチャフェッチ命令をバッチして、TEX 依存のストールの数を減らしてください(HLSL レベルで、 独立したテクスチャフェッチを 2~4 のback-to-back命令のバッチに まとめることによって)。
- シェーダが与えられたテクスチャのピクセルごとにすべての MSAA サブサンプルを反復している場合、そのテクスチャの TEX 命令の 1 バッチで、すべてのサブサンプルを一緒にフェッチします。MSAA サブサンプルは VRAM 内で隣り合わせに保存されるため、一緒にフェッチすれば TEX ヒット率は最大になります。
- テクスチャの負荷が、ほとんどの場合、真になると予想される条件に基づいている場合(例えば、if (idx < maxidx) loadData(idx))、負荷を強制して座標をクランプすることを検討する(loadData(min(idx,maxidx-1)))。
- TEXキャッシュとL2キャッシュのヒット率を向上させることで、TEXレイテンシーを減少させてみてください。TEX および L2 ヒット率は、サンプリングパターンを微調整して隣接ピクセル/スレッドがより多くの隣接テクセルをフェッチするようにし、該当する場合はミップマップを使用し、さらにテクスチャのサイズを小さくしてよりコンパクトなテクスチャフォーマットを使用することによって向上させることが可能です。
- 実行されるTEX命令の数を減らしてみてください(おそらく、TEX命令述語としてコンパイルされる、テクスチャ命令ごとのブランチを使用します、例としてFXAA 3.11 HLSLを参照してください、例.”if(!doneN) lumaEndN = FxaaLuma(…);”).
トップSOLユニットがSMではない場合
どのユニットかを確認して,下記に従って対処を行います。
(1)トップSOLユニットが,TEX, L2, あるいは VRAMの場合
トップ SOL ユニットが SM ではなく、メモリサブシステム ユニット(TEX-L1、L2、および VRAM)の 1 つである場合、性能低下の根本原因は、GPUフレンドリーではないアクセスパターン(通常、ワープ内の隣接スレッドが遠く離れたメモリにアクセスする)により発生した TEX または L2 キャッシュのスラッシングである可能性があります。この場合、最上位制限ユニットは TEX または L2 であるかもしれませんが、根本的な原因は SM によって実行されるシェーダにある可能性があるため、最上位 SOL ユニットが SM の場合を使用して SM のパフォーマンスをトリアージ方法で決定する価値があります。
トップ SOL ユニットが VRAM であり、その SOL% 値が悪くない(60% 以上)場合、このワークロードは VRAM スループットが制限されており、別のパスでマージすることでフレームが高速化されるはずです。典型的な例は、ガンマ補正パスと他のポストプロセッシングパスをマージすることです。
(2)トップSOLユニットがCROPあるいはZROPの場合
CROP がトップ SOL ユニットである場合、より小さいレンダーターゲット形式(例:RGBA16F の代わりに R11G11B10F)を使用してみたり、Multiple Render Targets を使用している場合、レンダーターゲットの数を減らしてみたりすることができます。また、ピクセルシェーダでより積極的にピクセルをキルすることは価値があるかもしれません(たとえば、特定の透明効果では、不透明度が1%未満のピクセルを破棄する)。透明レンダリングを最適化するための可能な戦略については、「透明(または半透明)レンダリング」を参照してください。
ZROP が Top SOL ユニットである場合、より小さい深度フォーマット(例:シャドウマップ の D24X8 の代わりに D16、または D32S8 の代わりに D24S8)を使用し、さらに不透明オブジェクトを前から後ろへの順番に描いて、ZCULL(粗視化深度テスト)が ZROP とピクセルシェーダを起動する前に多くのピクセルが廃棄できるようにするとよいでしょう。
(3)トップSOLユニットがPDの場合
前述したように、PDは入力された頂点のインデックスを収集するために、インデックスバッファのロードを行います。PDがトップSOLユニットである場合、32ビットではなく16ビットのインデックスバッファを使用してみることができます。それでも PD の SOL%が増加しない場合は、頂点の再利用とローカリティのためにジオメトリを最適化することを試してみることができます。
(4)トップSOLユニットがVAFの場合
この場合、頂点シェーダーの入力アトリビュートの数を減らすと効果的です。
また、位置ストリームを他のアトリビュートから分離することは、z-only またはシャドウマップレンダリングに有効な場合があります。
頂点アトリビュートを減らす
前述した,P3 Methodでも出てきますが,頂点アトリビュートを減らすと高速になるケースがあります。プラットフォームによってはドキュメントに「XXX以下にすると速くなります」みたいな記述が載っていたり,実際にどのぐらい数でどういう結果が得られたのかをグラフ化してくれていたりします。
ここで重要なのは,なるべくパッキングして数を減らすということです。例えば,下記のようにします。
//変更前 struct VSOutput { float4 Position : SV_POSITION; float2 TexCoord0 : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; float2 TexCoord2 : TEXCOORD2; float2 TexCoord3 : TEXCOORD3; }; // 変更後 struct VSOutput { float4 Position : SV_POSITION; float4 TexCoord01 : TEXCOORD01; // TEXCOORD0 and TEXCOORD1 float4 TexCoord23 : TEXCOORD23; // TEXCOORD2 and TEXCOORD3 };
データ容量は変わっていないですが,これだけで高速化します。過去の経験ではモデル描画のシェーダに対して適用したところ全体で1[ms]程度の高速化が出来た記憶があります。とにかくピクセルシェーダで使っていないアトリビュートがあったら,削る。削れないときはまとめることによって数を減らすということを考えると良いです。
サイズを減らす
計算等に利用するテクスチャサイズを減らすことによってテクスチャキャッシュに載りやすくし,高速化するテクニックがあります。例えば,Deinterleaved RenderingやCheckerboard Renderingといったものがこれに当たります。特に縮小バッファにして描画品質があまりにも落ちすぎるのを避けたい場合などに使うと良いです。
テクスチャフェッチをバッチする
NVIDIAのBlog記事にもありますが,レイマーチループのような動的ループ内でテクスチャフェッチをバッチかすることによってTEXのレイテンシーを隠すことができます。
典型的なSSRレイマーチングループのHLSLは次のような感じです。
float MinHitT = 1.0; float RayT = Jitter * Step + Step; [loop] for ( int i = 0; i < NumSteps; i++ ) { float3 RayUVZ = RayStartUVZ + RaySpanUVZ * RayT; float SampleDepth = Texture.SampleLevel( Sampler, RayUVZ.xy, GetMipLevel(i) ).r; float HitT = GetRayHitT(RayT, RayUVZ, SampleDepth, Tolerance); [branch] if (HitT < 1.0) { MinHitT = HitT; break; } RayT += Step; }
ループ内で,隣り合わせで2回テクスチャフェッチするように処理を置き換えます。
float MinHitT = 1.0; float RayT = Jitter * Step + Step; [loop] for ( int i = 0; i < NumSteps; i += 2 ) { float RayT_0 = RayT; float RayT_1 = RayT + Step; float3 RayUVZ_0 = RayStartUVZ + RaySpanUVZ * RayT_0; float3 RayUVZ_1 = RayStartUVZ + RaySpanUVZ * RayT_1; // batch texture instructions to better hide their latencies float SampleDepth_0 = Texture.SampleLevel( Sampler, RayUVZ_0.xy, GetMipLevel(i+0) ).r; float SampleDepth_1 = Texture.SampleLevel( Sampler, RayUVZ_1.xy, GetMipLevel(i+1) ).r; float HitT_0 = GetRayHitT(RayT_0, RayUVZ_0, SampleDepth_0, Tolerance); float HitT_1 = GetRayHitT(RayT_1, RayUVZ_1, SampleDepth_1, Tolerance); [branch] if (HitT_0 < 1.0 || HitT_1 < 1.0) { MinHitT = min(HitT_0, HitT_1); break; } RayT += Step * 2.0; }
ブログ記事では,GTX1080で0.54[ms]だったものが0.42[ms]に高速化したと記載されています。また2回ではなく4回にすることで0.54[ms]から0.33[ms]になるという記述もあります。レイマーチ処理は基本的に重くなりやすいので,アルゴリズム的に最適化出来ない場合は,こうしたバッチ化により高速化するとよさそうです。
Errata : Direct3D12ゲームグラフィック実践ガイド
大変申し訳ございません。
下記の通り、誤記がありましたので謹んでお詫びして訂正いたします。
P.245 中央部の数式
【誤】
\begin{eqnarray}
x &=& r \sin \theta \cos \theta \\
y &=& r \cos \theta \\
z &=& r \sin \theta \sin \theta
\end{eqnarray}
【正】
\begin{eqnarray}
x &=& r \sin \theta \sin \phi \\
y &=& r \cos \theta \\
z &=& r \sin \theta \cos \phi
\end{eqnarray}
P.341 リスト10.16
【誤】
auto invW = 1.0f / float(w - 1); auto invH = 1.0f / float(h - 1);
【正】
auto invW = 1.0f / float(w); auto invH = 1.0f / float(h);
サンプルプログラム Chapter.10 IESProfile.cpp
【誤】
auto idx = (w - 1) * y + x;
【正】
auto idx = w * y + x;
ビジビリティカリング メモ(3)
こんちわ、Pocolです。
前回までで,ビジビリティカリングの説明が終わりました。
こんなに素晴らしいカリング手法があるのなら,積極的に使いたいと思いました。
で,ふと思いついたのが,ビジビリティカリングをシャドウキャスターのカリングに使おうという単純なアイデアです。
参考文献
[3] Oliver Mattausch, Jiri Bittner, Ari Silvennoinen, Daniel Scherzer, and Micheal Wimmer, “Efficient Online Visibility for Shadow Maps”, GPU Pro 3, pp.233-242, CRC Express, 2012.
[4] Jiri Bittner, Oliver Mattausch, Airi Silvennoinen, Micheal Wimmer, “Shadow Caster Culling for Efficient Shadow Mapping”, I3D’11: Symposium on Interactive 3D Graphics and Games, February 2011, pp.81-88.
実装アイデア
実装アイデアはいたって単純です。
[3]の文献で使用されているハードウェアオクルージョンカリングの代わりにカリング手法をビジビリティカリングに置き換えるだけのものです。
まず,以下を前提とします。
・カメラビューからみた深度バッファがある。
・シャドウマップ行列がすでに求まっている。
[3]の手法をかなり雑に説明すると,
欲しい情報は「カメラビューから見た時のピクセルにシャドウが落ちるかどうか?」であるので,カメラビューのピクセルにシャドウを落とさないものは,そもそも寄与しないからカリングしてよくね?というアイデアです。
実装のためのメモ
ここからは自分なりの実装前の実装するためのメモです。
[3]の手法を実装するために,マスクが必要なります。
まずは,Pre-Zなりで作成されたカメラビューから見た深度バッファを用いて,現在フレームのワールド空間位置座標に変換し点群を求めます。
次に,この求まったワールド空間位置座標にシャドウマップ行列を適用し,ライトビュー空間位置座標に変換します。
これでライトビュー空間からみたときのラスタライズ位置が求まるはずです。
あとはUAV等に,このラスタイズ位置に「カメラから見える」としてフラグを書き込みます。
これでマスクが完成です。
次に,False Negative Passと同じ要領でライトビューからOBBを描画し,マスクバッファ上のフラグが立っている場所にシャドウをキャストするものだけをビジビリティバッファ上に「見える」としてマークします。
これで「カメラビューに寄与するシャドウキャスターのみ」のビジビリティバッファが完成します。
あとはインダイレクトドロー情報を生成して,ExecuteIndirect()で描画すればOKのはず…。
オプションとして,Mainパスに当たる部分のように前フレームのシャドウマップバッファを用いて,粗くカリングしておくという手も考えられます。…あんまり高速化期待できないかもしれませんが。
ビジビリティカリング メモ(2)
前回の続きです。
今回はFalse Negative Passの説明。
False Negative Pass
Main Passでは前フレームの深度バッファをダウンサンプルして,現在フレームの深度バッファをでっちあげカリング処理を行いました。
当然ながら,適当にでっちあげた深度バッファだと誤判定される可能性があります。
そこで,False Negative Passで誤判定されたものを取りこぼしが無いようにちゃんと判定しようというわけです。
このパスでは,フル解像度の深度バッファを用いて描画を行います。
文献[1]には,理論的にはコンサバティブラスタライゼーションを用いれば,この第2オクルージョンパスでも1/4解像度の深度バッファが使えると書いてあるのですが,いくつかの問題があったためフル解像度を使っているようです。
ビジビリティバッファのクリア
False Negative Passの最初の処理はビジビリティバッファのクリアです。
これはメインパスと同じようにゼロクリアすれば良いようです。
ビジビリティバッファを埋める
メインオクル―ジョンパスのように,Early-DepthStencilテストを通過したピクセルをビジビリティバッファ中にVisibleとしてピクセルシェーダで各インスタンスをマークします。問題のあるオブジェクトすべては最初のパスでどっちみち描画されるので,カメラのニア平面背後にあるバウンディングボックスに対してクランプするコードの実行は不要になります。
インダイレクトドロー情報を生成する
インダイレクトドロー情報はメインパス中で生成されます。
このときには可視できるインスタンスドロー情報のみが生成されているので,遮蔽されたインスタンスに対する処理は不要になります。
インダイレクトにFlase Negativeを描画する
メインパスと同じように描画します。
ここまでは,文献[2]でも説明されているので,特に新しいことはありません。
ここから先はアイデア段階のものです。
…それは次回に説明します。
ビジビリティカリング メモ(1)
新年明けましておめでとうございます。
本年もProjectAsuraをよろしくお願い致します。
さて,今日はビジビリティバッファを用いたカリングを行うための自分用の実装メモを残しておこうと思います。
あくまでも自分用なので間違っているかもしれません。また実装結果ではなく,これから実装するためのメモなので全然当てにならない可能性もあるので注意してください。
参考文献
[1] Hawar Doghramachi and Jean-Normand Bucchi, “Deferred+: Next-Gen Culling and Rendering for Dawn Engine”, GPU Pro, pp.77-104, Black Cat Publishing 2017.
[2] 三嶋 仁, “最新タイトルのグラフィックス最適化事例”, https://cedil.cesa.or.jp/cedil_sessions/view/1897, CEDEC 2017.
概要
ここでは,[1]の文献をベースに纏めて置きます。
基本的に2パスで処理を行います。まず1パス目がMain Passと呼ばれるものです。
前フレームの深度バッファを1/4解像度にダウンサンプリングして,現フレームのビュー射影行列を用いて,現在フレームにおおまかに一致する深度バッファを作成しインダイレクトドローを用いて可視と判定されたメッシュのみを描画します。
2パス目はFalse Negative Passと呼ばれるもので,フル解像度でMain PassでOccludeと判定されたOBBを描画し,インダイレクトドローを用いて,可視と判定されたメッシュのみを描画します。
Main Passの処理概要は次です。
- ① 深度バッファのダウンサンプリングとリプロジェクション
- ② ビジビリティバッファのクリア
- ③ ビジビリティバッファを埋める
- ④ インダイレクトドロー情報を生成する
- ⑤ 可視メッシュをインダイレクトに描画する
False Negative Passの処理概要は次になります。
- ① ビジビリティバッファのクリア
- ② ビジビリティバッファを埋める
- ③ インダイレクトドロー情報を生成する
- ④ 可視メッシュをインダイレクトに描画する
以下のデータが多分必要。
- 前フレームの深度バッファ
- 雑な現フレームの深度バッファ(Computeから深度書き込みできないらしいので,Color → Depthの変換が必要らしい…)
- ビジビリティバッファ(uint32_tのUAV)
- インスタンス変換用のバッファ(たぶん,ワールド行列。これをつかってGPU上でOBBにする)
- メッシュ情報
- インダイレクトドロー引数用のバッファ(可視メッシュ描画用)
- 可視インスタンスのインデックスバッファ
- Occludeされたインスタンスのインデックスバッファ
上記のうち,[1]によると…
<CPU更新>
- メッシュ情報
- インスタンス変換用のバッファ
<GPU更新>
- ビジビリティバッファ
- 可視インスタンスのインデックスバッファ
- Occludeされたインスタンスのインデックスバッファ
- インダイレクトドロー引数用のバッファの
とのこと。
実装詳細
Main Pass
深度バッファのダウンサンプリングとリプロジェクション
前フレームの深度バッファを用いて4×4ピクセルの最大値をコンサバティブにとることにより1/4解像度にダウンサンプリングし,前のフレームのワールド位置座標を復元します。
復元した,ワールド位置座標に現フレームのビュー射影行列を掛けて,w除算することにより現フレームの雑な深度情報を生成しておきます。大きな深度値を避けるためにカメラよりも後ろの深度値を出力することを抑制します。
疑似コードは次のようになります。
// 4要素のうちの最大値を求める. float Max4(float4 value) { return max( max( max(value.x, value.y), value.z ), value.w ); } [numthreads(REPROJECT_THREAD_GROUP_SIZE, REPROJECT_THREAD_GROUP_SIZEk, 1)] void main(uint3 dispatchThreadID : SV_DispatchThreadID) { if ((dispatchThreadID.x < (uint(SCREEN_WIDTH)/4)) && (dispatchThreadID.y < (uint(SCREEN_HEIGHT)/4))) { const float2 screenSize = float2(SCREEN_WIDTH/4.0f, SCREEN_HEIGHT/4.0f); float2 texCoords = (float2(dispatchThreadID.xy) + float2(0.5f, 0.5f)) / screenSize; const float offsetX = 1.0f/SCREEN_WIDTH; const float offsetY = 1.0f/SCREEN_HEIGHT; // 深度のダウンサンプル float4 depthValues00 = depthMap.GatherRed(depthMapSampler, texCoords + float2(-offsetX, -offsetY)); float depth = Max4(depthValues00); float4 depthValues10 = depthMap.GatherRed(depthMapSampler, texCoords + float2(offsetX, -offsetY)); depth = max( Max4(depthValues10), depth); float4 depthValues01 = depthMap.GatherRed(depthMapSampler, texCoords + float2(-offsetX, offsetY)); depth = max( Max4(depthValues01), depth); float4 depthvalues11 = depthMap.GatherRed(depthMapSampler, texCoords + flaot2(offsetX, offsetY)); depth = max( Max4(depthValues11), depth); // ダウンサンプルした深度から前フレームのワールド空間位置座標を再構築する. float4 lastProjPosition = float4(texCoord, depth, 1.0f); lastProjPosition.xy = (lastProjPosition.xy * 2.0f) - 1.0f; lastProjPosition.y = -lastProjPosition.y; float4 position = mul(cameraCB.lastInvViewProjMatrix, lastProjPosition); position /= position.w; // 現フレームの射影空間位置座標を計算する. float4 projPosition = mul(cameraCB.viewProjMatrix, position); projPosition.xyz /= projPosition.w; projPosition.y = -projPosition.y; projPosition.xy = (porjPosition.xy * 0.5f) + 0.5f; int2 outputPos = int2(saturate(projPosition.xy) * screenSize); // カメラ背後の大きな深度値を避ける. float depthF = (projPosition.w < 0.0f) ? depth : projPosition.z; // atomic max操作のための深度変換. // バインドされたカラーバッファはゼロで初期化されるため,最初に深度を反転し,atomic max操作を行い, // 最終的な深度バッファにコピーする際に深度を戻す. uint invDepth = asuint(saturate(1.0f - depthF)); // 新しい位置にリプロジェクションされた深度を書き込む. InterlockedMax(depthTexture[outputPos], invDepth); // リプロジェクションによる穴あきを処理するために現在位置にリプロジェクションされた深度を書き込む. InterlockedMax(depthTexture[dispatchThreaID.xy], invDepth); } }
ビジビリティバッファのクリア
ゼロクリアする。
DirectX12のUAVクリアAPIか,コンピュートシェーダでゼロクリアする。
ビジビリティバッファを埋める
前フレームの深度バッファから作成された現フレームの深度バッファをバインドして,ピクセルシェーダで[earlydepthstencil]付きでOBBを描画し,可視メッシュのフラグを立てる。
疑似コードは次のようになります。
// 頂点シェーダ VS_Output main(uint vertexId : SV_VertexID, uint instanceID : SV_InstanceID) { VS_Output output; output.occludedID = instanceID; // 単位キューブの位置を生成する. float3 position = float3(((vertexID && 0x4) == 0) ? -1.0f : 1.0f, ((vertexID && 0x2) == 0) ? -1.0f : 1.0f, ((vertexID && 0x1) == 0) ? -1.0f : 1.0f); matrix instanceMatrix = instanceBuffer[output.occludedID]; float4 positionWS = mul(instanceMatrix, float4(position, 1.0f)); output.position = mul(cameraCB.viewProjMatrix, positionWS); // カメラがバウンディングボックスの中の場合,オブジェクト自身が可視であっても完全に遮蔽される. // そのため,バウンディングボックスの頂点がニア平面の後ろであるものは,そのようなオブジェクトカリングを避けるために // ニア平面の前にクランプされる. output.position = (output.position.w < 0.0f) ? float4(clamp(output.position.xy, float(-0.999f).xx, float(0.999f).xx), 0.0001f, 1.0f) : output.position; return output; } // ピクセルシェーダ [earlydepthstencil] void main(VS_Output input) { visBuffer[input.occludedID] = 1; }
インダイレクトドロー引数の生成
コンピュートシェーダ上でインダイレクトドロー引数を生成する。
疑似コードは次のようになります。
#define THREAD_GROUP_SIZE 64 groupshared uint visibileInstanceIndexCounter; [numthread(THREAD_GROUP_SIZE, 1, 1)] void main ( uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex, uint3 dispatchThreadID : SV_DispatchThreadID ) { if (groupIndex == 0) { visibleInstanceIndexCounter = 0; } GroupMemoryBarrierWithGroupSync(); MeshInfo meshInfo = meshInfoBuffer[groupID.x]; for(uint i=0; i<meshInfo.numInstances; i+=THREAD_GROUP_SIZE) { uint elementIndex = groupIndex + i; if (elementIndex < meshInfo.numInstances) { uint instanceIndex = mesh.instanceOffset + elementIndex; // 可視の場合. if (visBuffer[instanceIndex] > 0) { uint index; InterlockedAdd(visibleInstanceIndexCounter, 1, index); visibleInstanceIndexBuffer[meshInfo.instanceOffset + indexs + NUM_FILL_PASS_TYPE] = instanceIndex; } else { uint index; InterlockedAdd(drawIndirectBuffer[0].instanceCount, 1, index); occludedInstanceIndexBuffer[index] = instanceIndex; } } } GroupMemoryBarrierWithGroupSync(); if (groupIndex == 0) { if (visibleInstanceIndexCount > 0) { // 可視メッシュのカウンターをインクリメントする. uint cmdIndex; InterlockedAdd(visibleInstanceIndexBuffer[meshInfo.meshType], 1, cmdIndex); cmdIndex += meshInfo.meshTypeOffset + 1; // G-Bufferに可視メッシュを描画する. DrawIndirectCmd cmd; cmd.instanceOffset = meshInfo.instanceOffset; cmd.MaterialId = meshInfo.materialId; cmd.indexCountPerInstance = meshInfo.numIndices; cmd.instanceCount = visibleMeshInstanceIndexCounter; cmd.startIndexLocation = meshInfo.firstIndex; cmd.baseVertexLocation = 0; cmd.startInstanceLocation = 0; drawIndirectBuffer[cmdIndex] = cmd; } } }
可視メッシュを描画する
メッシュタイプごとにExecuteIndirectコマンドを発行して,描画します。
疑似コードはつぎのような感じ。
VS_Output main(VS_Input input, uint instanceID : SV_InstanceID) { VS_Output output; uint instanceIndex = instanceInfoCB.instanceOffset + instanceID+ instanceIndex = visibleInstanceIndexBuffer[instanceIndex + NUM_MESH_TYPES]; matrix transformMatrix = instanceBuffer[instanceIndex].transformMatrix; ... }
長くなったので,続きのFalse Negative Passは次回書きます。
Pre-Integrated SkinのLUTを作ってみた。
こんにちわ。
Pocolです。
会社でキャラの肌が調整しずらいからどうにかしてくれ!
…と,言われてしまったので,とりあえずPre-Integrated Skinを調べてみようと思いました。
LUTテーブル作るところまでは実装してみました。
出来上がったLUTのテクスチャはこちら。
で,これを作るソースコードは以下のような感じ。
#define STBI_MSC_SECURE_CRT #define STB_IMAGE_WRITE_IMPLEMENTATION //----------------------------------------------------------------------------- // Includes //----------------------------------------------------------------------------- #include <cstdio> #include <stb_image_write.h> #include <vector> #include <asdxMath.h> //----------------------------------------------------------------------------- // リニアからSRGBに変換. //----------------------------------------------------------------------------- inline float ToSRGB(float value) { return (value <= 0.0031308) ? 12.92f * value : (1.0f + 0.055f) * pow(abs(value), 1.0f / 2.4f) - 0.055f; } //----------------------------------------------------------------------------- // ガウス分布. //----------------------------------------------------------------------------- inline float Gaussian(float v, float r) { return 1.0f / sqrtf(2.0f * asdx::F_PI * v) * exp(-(r * r) / (2.0f * v)); } //----------------------------------------------------------------------------- // 散乱計算 //----------------------------------------------------------------------------- inline asdx::Vector3 Scatter(float r) { // GPU Pro 360 Guide to Rendering, "5. Pre-Integrated Skin Shading", Appendix A. return Gaussian(0.0064f * 1.414f, r) * asdx::Vector3(0.233f, 0.455f, 0.649f) + Gaussian(0.0484f * 1.414f, r) * asdx::Vector3(0.100f, 0.336f, 0.344f) + Gaussian(0.1870f * 1.414f, r) * asdx::Vector3(0.118f, 0.198f, 0.000f) + Gaussian(0.5670f * 1.414f, r) * asdx::Vector3(0.113f, 0.007f, 0.007f) + Gaussian(1.9900f * 1.414f, r) * asdx::Vector3(0.358f, 0.004f, 0.00001f) + Gaussian(7.4100f * 1.414f, r) * asdx::Vector3(0.078f, 0.00001f, 0.00001f); } //------------------------------------------------------------------------------- // Diffusionプロファイルを求める. //------------------------------------------------------------------------------- asdx::Vector3 IntegrateDiffuseScatterOnRing(float cosTheta, float skinRadius, int sampleCount) { auto theta = acosf(cosTheta); asdx::Vector3 totalWeight(0.0f, 0.0f, 0.0f); asdx::Vector3 totalLight (0.0f, 0.0f, 0.0f); const auto inc = asdx::F_PI / float(sampleCount); auto a = -asdx::F_PIDIV2; while(a <= asdx::F_PIDIV2) { auto sampleAngle = theta + a; auto diffuse = asdx::Saturate(cosf(sampleAngle)); auto sampleDist = abs(2.0f * skinRadius * sinf(a * 0.5f)); auto weights = Scatter(sampleDist); totalWeight += weights; totalLight += weights * diffuse; a += inc; } return asdx::Vector3( totalLight.x / totalWeight.x, totalLight.y / totalWeight.y, totalLight.z / totalWeight.z); } //----------------------------------------------------------------------------- // ルックアップテーブル書き出し. //----------------------------------------------------------------------------- bool WriteLUT(const char* path, int w, int h, int s) { std::vector<uint8_t> pixels; pixels.resize(w * h * 3); float stepR = 1.0f / float(h); float stepT = 2.0f / float(w); for(auto j=0; j<h; ++j) { for(auto i=0; i<w; ++i) { auto radius = float(j + 0.5f) * stepR; auto curvature = 1.0f / radius; auto cosTheta = -1.0f + float(i) * stepT; auto val = IntegrateDiffuseScatterOnRing(cosTheta, curvature, s); // 書き出しを考慮して, 上下逆にして正しく出力されるようにする. auto idx = ((h - 1 - j) * w * 3) + (i * 3); pixels[idx + 0] = static_cast<uint8_t>(ToSRGB(val.x) * 255.0f + 0.5f); pixels[idx + 1] = static_cast<uint8_t>(ToSRGB(val.y) * 255.0f + 0.5f); pixels[idx + 2] = static_cast<uint8_t>(ToSRGB(val.z) * 255.0f + 0.5f); } } auto ret = stbi_write_tga(path, w, h, 3, pixels.data()) != 0; pixels.clear(); return ret; } //----------------------------------------------------------------------------- // メインエントリーポイントです. //----------------------------------------------------------------------------- int main(int argc, char** argv) { std::string path = "preintegrated_skin_lut.tga"; int w = 256; int h = 256; int s = 4096; for(auto i=0; i<argc; ++i) { if (_stricmp(argv[i], "-w") == 0) { i++; auto iw = atoi(argv[i]); w = asdx::Clamp(iw, 1, 8192); } else if (_stricmp(argv[i], "-h") == 0) { i++; auto ih = atoi(argv[i]); h = asdx::Clamp(ih, 1, 8192); } else if (_stricmp(argv[i], "-s") == 0) { i++; auto is = atoi(argv[i]); s = asdx::Max(is, 1); } } return WriteLUT(path.c_str(), w, h, s) ? 0 : -1; }
ちょっとググって調べた感じだと,https://j1jeong.wordpress.com/2018/06/20/pre-intergrated-skin-shading-in-colorspace/というBlog記事をみて,検証もせずにとりあえず鵜呑みで,sRGBで書き出してみました。
シェーダで使う場合は補正入れてリニアになるようにしてから使ってください。
NaNをなくす
良くライティング計算結果がNaNになってポストエフェクトでバグるというのが頻発していて困っていたのですが,
ようやく最近回避方法を知りました。
StackOverflowとかみていたら,
step(value, value) * value;
みたいにすると,NaNである場合には0になり,NaN以外の場合にはvalueで返すことが出来るらしい…。
と書いてあったのですが,よくよく考えてみるとstep(value, value)のところは確かにゼロとなるので問題ないなさそうに思えるのですが,NaNとの四則演算結果はNaNになるような気がします。実際に試してみたのですが,やっぱり駄目でした。
あとは,Googleのfilamentに
#define MEDIUMP_FLT_MAX 65504.0 #define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)
みたいなのが書いてあったので,下記のような実装を試してみました。
static const float HALF_MAX = 65504.0; clamp(value, 0.0f, HALF_MAX);
結果として,NaNが発生しなくなりバグが解消されるようになりました。
自分なりになぜNaNが発生しなくなるのかを考えてみたのですが,NaNの性質として !=以外の演算は常にfalse, !=演算は常にtrueという性質があるようです。よって,clamp()メソッドを使うと比較演算が実行されるので,この結果としてNaNをはじいて指定範囲内に収まると考えれば非常に納得がいきます。ここではclamp()を試しましたが,同様の理屈が通るのであればmax()などでも代用できるはずです。
さて,特に物理ベースレンダリングに移行する際に,GGXモデルなどを使うことが多いと思うのですが,定式化されているものが除算を含む形で定義されているので,実装する際に除算をしなくてはならないということが発生します。
GGXの計算する際に,分母がゼロになることにより,ゼロ除算が発生し,計算結果がNaNになる可能性があります。
また,pow()関数を使用している場合は,MSDNのドキュメント にも書いてあるように第1引数が0未満の場合はNaNになり,第1引数・第2引数がともにゼロの場合はハードウェア依存によりNaNが発生する可能性があります。
昔からよくある方法としてゼロ除算を発生させないために,例えば1e-7fなどのような小さな値を分母側に足しておき,ゼロにならないようにオフセットを掛けるという回避方法もあるのですが,この方法は除算には適用できるのだけれども,pow()関数には適用できません。
GGXを用いたライティング計算の場合は,ゼロ除算が発生する可能性があるため最初はこのオフセットを足し込むというやり方を取っていたのですが,これをやってしまうと計算結果が変わってしまい,ハイライトが鈍くなるなど見た目上に影響を及ぼすことが分かりました。
PBRやっているはずなのに,何故かなまった感じの画が出る場合には,下駄をはかせてハイライトを潰してしまっていないかどうか確認するのを強くお勧めします。
下駄をはかせている箇所を下駄を取って普通に除算し,最後に上記のようなNaNをはじく処理をいれることで,ハイライトがなまったり,ライティング以降のレンダリングパスでバグるという問題を解決することが出来ます。また,pow()関数を用いる場合にも適用でき,NaNをなくすることができるので,NaNで困っている方はぜひ試してみてください。
※もっとより良い方法ご存知であれば,是非教えてください!
DirectX Raytracing
GDC2018でDirectX Raytracingが発表されたようです。
実験段階のSDKが公開されています。
http://forums.directxtech.com/index.php?topic=5860.0
今日帰ったら,本の執筆の息抜きとしてちょっと触ってみようかなと思います。