DirectXの話 第139回

Draw Indirectを用いたスプライトボケフィルタ  13/05/26 up

ちょっとバグありなんですが、とりあえず速度検証がメインだってことで公開します。

速度的にそんなに悪くは無いと思うのですが、全面的に良いとも言えない部分があり、設定や使い方次第かな、と思います。

以前、スプライトを用いた被写界深度(Depth Of Field : DOF)について速度検証をしてみました。

その際に検証したのはジオメトリシェーダでスプライトを生成するのと頂点シェーダで生成するのとではどちらが速いかというものでした。

結果としては頂点シェーダで生成した方が速かったのですが、他のGPUで計測してみたところ、GeForce系では全体的にジオメトリシェーダの方が速かったですね。

まあ、それはまた別のお話なのですが、今回は今までにもいくつか使ってみたDraw Indirect系命令を使用してみました。

前回のスプライトDOFの実装は以下のようなものでした。

1.深度バッファを半解像度にダウンサンプリング

2.各ピクセルのCircle of Confusion (CoC) を計算

3.CoCがピントズレのサイズであればそのピクセルにスプライトを生成

若干違いますが、おおむねこんな感じでした。

スプライトの描画は、描画キック自体は(半解像度の)ピクセル数分だけ行い、頂点シェーダ、もしくはジオメトリシェーダ内でCoCの値から描画の有無を決定していました。

ジオメトリシェーダの場合は頂点生成を止め、頂点シェーダの場合はポリゴンサイズが0になるように調節しました。

キック回数自体は1回ですが、頂点シェーダが回る回数は多めになっています。

これがどの程度問題になるのかイマイチ確認出来ていませんが、頂点シェーダが回る回数が少ない方が良いのは間違いないでしょう。

今回やってみたのはその頂点シェーダが回る回数を減らすための施策です。

DirectX11で実装されたDraw Indirect系の命令は、描画するインスタンスや頂点の数をGPUに計算させ、これをそのまま描画キック出来るという代物です。

わかりやすいのは第132回で実装したインスタンスのオクルージョンカリングでしょう。

第134回ではMarching Cubeのイテレーションにもしようしています。

つまり、CoCを計算する際に、ピントズレしているピクセルについてはスプライトを生成、生成されたスプライトの数だけDrawInstancedIndirect()命令で描画するというものです。

今回の処理の流れは以下のようになります。

1.1回のCompute Shaderで以下の処理を行う

A.4ピクセルの深度値、カラー値をフェッチ(半解像度でスプライトを生成するため)

B.最も輝度の高いピクセルについてCoCを計算

C.ピントズレしているようならスプライトを生成し、StructuredBufferに積み込む(後ボケ、前ボケで別のバッファ)

2.ボケ生成用バッファにスプライトを描画(後ボケ、前ボケはそれぞれ別バッファ)

3.ボケバッファとカラーバッファを合成する

これだけです。

一応、今回は後ボケと前ボケを分け、ピンボケの境界部分におけるカラーブリーディングを避けています。

まあ、カラーブリーディングを避けなかったとしてもボケ生成用バッファへの描画(2番の処理)は必要になります。

スプライトを単純に半透明で描画してしまうと、スプライトの生成順序によって描画結果がまちまちになってしまいます。

これを避けるためには一旦別バッファに描画しておくのが一番かと思います。

今回、1-Bのように最も高輝度な部分にスプライトが生成されるようにしました。

これはあまりよろしくないんじゃないかと思うのですが、シーンによってはこうしないと上手くいかない事例が出てきます。

今回のシーンのようにミップマップを使用していない法線マップを使用する場合、1,2ピクセルの高輝度部分が出てきてしまったりします。

そういうおかしなピクセルが出ないようにミップマップを使うべきなのでしょうが、そういう絵面が欲しい場合もあります。

この場合に、例えば深度が最も浅い部分でCoCを計算してしまうと、高輝度部分を押しのけて低輝度カラーによってスプライトが描画されてしまうことがあります。

こうなると高輝度部分の1ピクセルだけが妙に浮いて見えてしまったりします。

計算式の問題なので上手くやれば何とかなるかもしれませんが、イマイチいい手が思い浮かばなかったので今回のような手法になりました。

また、今回は背景データとしてSponza(Cryteckのじゃないやつ)を利用しています。

このデータの元からなのかコンバート時の問題なのかわからないのですが、頂点座標の誤差が大きかったりポリゴンが貼られていない場所があったりします。

このような場所ではクリアカラーが見えてしまっていて、ここにスプライトが生成されてしまうととても酷い結果になります。

今回のクリアカラーが黒なため、黒っぽいスプライトが目立って出てきてしまう部分があり、それらを見るととても残念な気持ちになります。

もちろん、このようなポリゴンの隙間はゲーム開発では避けるべき問題です。

見つかったらデザイナさんに直してもらうのが基本ですが、期間的な問題や、製品発売後に発覚したなどの要因で直せないこともあります。

こういうときにスプライトベースのDOFは、普通のガウスブラーなどと比べてイマイチな結果になりやすいと思います。

スプライトベースDOFを使用する場合はデザインデータに細心の注意を払ってください。

今回の重要な話。

今まで私が作成したDraw Indirect系サンプルは、StructuredBufferへデータを出力する際に、構造体配列の何番にデータを入れれば良いのか、という情報をDraw Indirectで使用するバッファから求めていました。

このRWByteAddressBufferをInterlockedAdd()命令でインクリメントしていたのですが、今回のサンプルで、この命令は遅いと言うことがよくわかりました。

Atomicな命令なので当たり前なんでしょうけど、この命令を使用している分には、Draw Indirect命令でスプライトベースDOFをやるのは無理でしょう。

しかしご安心を。RWStructuredBufferにはIncrementCounter()という命令があります。

RWStructuredBufferを生成する際に、カウンタを使用するためのフラグを与えてやると前述の命令が使用出来るようになります。

こちらは十分に速く、確実にStructuredBufferのカウントをインクリメント出来ます。

スプライトの生成が終わったら、StructuredBufferのカウントをDraw Indirectに使用するバッファにコピーします。

コピー方法はID3D11DeviceContext::CopyStructureCount()を利用するか、別のCompute Shaderでコピーする手段があります。

今回のサンプルは後者を使いました。

結果としては相当速くなったな、と思います。

と言うわけで今回のサンプルは以下から。

Download:Sample130.zip

確認出来ている不具合ですが、HDAOを半解像度で処理すると、何故か色んな所に四角く黒いスプライトが描画されてしまいます。

あんなもの描画してないんですがね…

もしかしたらGPUのドライバの不具合かもしれませんが、定かではありません。

速度の問題はそのほとんどがピクセルシェーダです。

Compute ShaderはInterlockedAdd()を使用せず、テクスチャフェッチを最小限にしておけばそこまで重くないです。

残念ながらピクセルシェーダは重めで、特にDOFがきつい状況でスプライトの最大サイズを大きくすると結構困ったことになります。

ピクセルシェーダの処理速度を減らすのであれば、やはりガウスブラーなどを用いたDOFに高輝度部分だけスプライト、という対応の方がいいんじゃないでしょうか。

PS4とかだったら十分な速度で動くかもしれません。

ガウスブラー+スプライトとかも速度検証してみたいですね。