DirectXの話 第124回
スプライトを使用したボケフィルタの比較
今回は被写界深度をより現実的にするボケフィルタをスプライトで実装してみました。
しかし、今回の目的はボケフィルタの実装ではなく、2種類の描画方法でどちらが高速かを試してみただけです。
そのため、ボケフィルタの実装としてはかなりお粗末なので、コードを利用したりする場合は注意してください。
注意すべき点についてはあとで解説します。
ボケは英語では"Bokeh"となります。
語源は日本語の“暈ける”で、“可愛い”とかと同様に日本語から英語になった単語らしいです。
ボケとツッコミの方のボケではなく、写真のボケを指します。
写真のボケは被写界深度としてCGをやってる人にはおなじみでしょう。
昔の被写界深度は最終画像をガウスブラーなどでぼかして、最終画像とブラー画像を深度から求めたアルファ値でブレンドしていました。
これでもそれっぽく見えるのですが、実際の写真と比較すると高輝度部分が大きく異なることがわかると思います。
このようなボケはそもそもレンズの絞り形状に合わせて暈けるので、高輝度部分では絞り形状がはっきりと出ます。
上のスクリーンショットのような五角形や円などの形で夜のネオンなどが暈けているのを見たこともあるでしょう。
では、このような形状を出すにはどうしたらいいでしょう?
簡単な方法としては映像のぼかし方をガウスブラーではなく、絞り形状に合わせたピクセルをフェッチすることで対応が出来ます。
ただし、この方法ではかなりの数のピクセルをフェッチしなければそれっぽい形状になりません。
フェッチが重いハードでは相当厳しいです。
そこで最近はスプライトを利用して描画する手法が使われるようになってきました。
各ピクセルの錯乱円の大きさに合わせたスプライトを大量に描画することで対応します。
錯乱円とは英語では"Circle of Confusion (CoC)" と呼ばれるもので、絞りの形状の大きさのことを言います。
ピントがずれている部分ほど錯乱円は大きくなります。
なお、今回のサンプルの錯乱円の計算式は今給黎さんの著書『DirectX 9 シェーダプログラミングブック』から持ってきています。
詳しくはそちらをお読みください。
もしも画面全てが暈けている場合、800*600の画像サイズでは48万個のスプライトを描画することになります。
流石にそれでは多すぎるので、今回は400*300のピクセルに対してスプライトを描画するようにしています。
これでも12万個のスプライトですので、馬鹿には出来ません。
もちろん、ピントが合っている部分にはスプライトを描画する必要はありません。
つまり、実際に描画されるスプライトは12万個のうちの一部ということになります。
このスプライトをどのように生成するか?
CPUでスプライトを生成するなどナンセンスです。
もしそのように対応するなら、400*300の画像をCPU側で取得し、そのCoCからピントが合っているかどうか調べ、ピントが合っていない場所にスプライトの頂点を生成しなければなりません。
そんな単純なことはGPUにやらせればいいのです!
やり方は簡単です。
頂点シェーダに12万個のポイントを渡し、ここで各ピクセルのCoCを取得してスプライトを描画するかどうか決定します。
スプライトを描画する頂点についてはジオメトリシェーダで4頂点のスプライトを生成し、そうでないものは何もせずにジオメトリシェーダを抜けます。
あとはピクセルシェーダで描画するだけです。
ね、簡単でしょ?
しかし、今年のGDCにおけるAMDの発表で、ちょっとした話が出てきました。
スプライトを大量に描画する場合、ジオメトリシェーダを使うより頂点シェーダのみで対処した方が高速だと。
どういうことかというと、頂点シェーダに12万*6個の頂点を入力します。当然、頂点6個でスプライト1つ分です(TriangleList描画のため)。
DirectX11では入力頂点の通し番号を取得することが出来るので、その番号からその頂点がどこのピクセルのスプライトで使用されるかわかります。
そのピクセルのCoCを取得し、暈けているならそれに合わせたサイズのスプライトになるように頂点を計算し、暈けていないなら全ての頂点を同じ座標にしてしまいます。
全ての頂点が同一座標の場合、ラスタライズの段階で無視されてピクセルシェーダには入ってきません。
なので、無駄なピクセルシェーダは動作せず、ジオメトリシェーダを使ったときと同じ結果を得られます。
この結果はサンプルを実行してみてください。
"VS Only"の項目をtrueにすると頂点シェーダのみを使用します。デフォルトではfalseとなっています。
うちの環境ですと、スプライトの描画数が多い場合は頂点シェーダのみの方が高速に動作していました。
ただし、スプライトの数が少ない場合はジオメトリシェーダを使った方が速かったです。
かなりの量のスプライトを描画する場合は効果があるわけですが、その場合はピクセルシェーダもかなり重くなるので注意が必要です。
さて、今回のサンプルの問題点ですが、実装としてはお粗末な物になっているという点です。
被写界深度を単純に実装してしまうと、ピントが合っている部分と合っていない部分の境界でおかしな色のブレンドが発生してしまうことがあります。
色のにじみというやつですが、今回のサンプルにもそのような状況が発生しています。
これは、各ピクセルのボケ具合を考慮せずに縮小バッファを使用しているためです。
綺麗な実装をする場合、出来れば暈けている部分と暈けていない部分を分けて、暈けている部分の画像のみでボケ画像を作るべきです。
この手法はトライエースの『スターオーシャン3』でも使われていました。
しかし、そもそもスプライトを大量に描画すること自体が重いので、出来ればより軽い手法で実装したいと思うのが普通でしょう。
軽い実装でそれらしい映像を作る方法としては、MJP氏のこちらの実装が個人的には良いのではないかと思います。
簡単に解説すると、閾値以内の輝度の部分は普通にガウスブラーっぽくぼかします。
閾値以上の輝度の部分についてはその情報を頂点情報としてUAVに出力します。
被写界深度を有効にした画像を描画した後、出力した頂点情報からスプライトを生成して描画します。
この方法の場合、高輝度部分以外にはスプライトの描画を行いません。
光学的には正しくないのですが、絞り形状が目立つのは高輝度部分のみという状況を上手く利用した手法だと思います。
ただ、ブラー画像がどの程度高速に描画できるのか、UAVへのアクセスは軽いのかなどの疑問もあります。
しかもUAVを使用するため、DirectX9では実装が難しいです。
ちなみに、今回のサンプルの実装はDirectX9でも可能です。ジオメトリシェーダを使わないバージョンについては、ですが。
では、サンプルは以下から取得してください。
マウスでカメラ操作、各レンズパラメータでボケの調整が可能です。
今回はボケをわかりやすくするためにライトを強めに当てています。
次回はMJP氏がすでに実装していますが、Tile-based Forward Renderingをやってみたいなぁ、とか思ってます。