DirectXの話 第123回
再構築フィルタを用いたモーションブラー
今回はI3D 2012で発表されていた ”A Reconstruction Filter for Plausible Motion Blur” をやってみました。
比較的簡単な実装でそれなりの効果を得られますが、残念ながら弱点も存在します。
モーションブラーを実現する方法としてはいくつかの手法が存在しています。
PS2の頃によく使われたのがフィードバックブラーを用いる方法です。
この方法の欠点は描画された画像分しかブラーがかからない点です。
モーションブラーを実装する大きな理由としては、30FPSやそれ以下の速度でもフレームとフレームの間を補完することでそれなりの滑らかさを獲得しようというものです。
しかし、フィードバックブラーではフレーム感を補完することは出来ません。
ちょっとした演出であれば問題ないのですが、やりすぎると不快感を催す場合もあります。
私の場合、『ワンダと巨像』でカメラが大きく動いたときに少々酔いそうになったのを記憶しています。
別の方法として、ブラーを掛けたテクスチャに切り替えるという手法が存在します。
よく使われるのが車のタイヤで、『Forza Motorsport 3』でも使われていたと記憶しています。
この手法はモデルの範囲内でしかブラーがかからず、しかも事前にテクスチャを用意する必要があるなど汎用性に欠けます。
現在、最もよく使われているのが速度バッファを用いたピクセル単位のブラーです。
しかしここにもいくつかの問題が存在しています。
大きな問題として、速度バッファをどのように埋めるか、というものがあります。
よく見かけるのは Deferred Rendering をする場合に、最初の G-Buffer を描画する際にMRTで描画してしまう手法です。
この方法でモーションブラーを実装してしまうと、元のモデルの輪郭がくっきりと出てしまいます。
今回のサンプルにはこの手法も実装されていますが、以下のようになってしまいます。
ピクセルシェーダは同一ピクセルの速度からブラーを行うため、描画が行われていない部分はブラーが行われなくなってしまいます。
これを防ぐ方法として、オブジェクトを速度方向に膨らませる手法もよく使われます。
このサイトでも(Ver.1.0の頃ですが)2回ほどやっていますが、どちらもこの方法を用いています。
本来であればこの手法が一番いい結果をもたらすのですが、3つの問題があります。
1つめはジオメトリ描画パスを1回余分に行わなければいけないという点です。
頂点シェーダによって頂点の引き伸ばしが行われるため、法線、深度、マテリアル情報と言った G-Buffer に描画されるものと一緒には描画できません。
2つめは引き伸ばしのために縮退ポリゴンを利用しなければならない点です。
DirectX10以降であればエッジ情報から必要な部分にポリゴンを追加することが可能ですが、だからといって軽いわけでもありません。
3つめはアルファ抜きオブジェクトなどはその形状に沿った膨らませ方が難しいという点です。
オブジェクトの形状や移動方向によってはそのまま引き伸ばしてアルファ抜きすればいいかもしれません。
しかしそれでうまくいくかと言われると、やっぱり難しいだろうな、と思います。
他にもDirectX10以上ならジオメトリシェーダを使ってラインオブジェクトを描画する手法もありますが、DirectX9世代では使えません。
ってな訳で今回の手法が考案されたわけで、前置きが長くなりましたが簡単に解説を行います。
詳しいことは Paper を読んでください。<手抜き
この手法はまず、速度バッファを G-Buffer とともに描画した後にタイルごとの最大速度を求めます。
このバッファを TileMaxBuffer と呼びます。
元の画面解像度を (w, h) とすると、TileMaxBuffer の解像度は (w / k, h / k) となります。
k はタイル1つの大きさです。
タイルにはそのタイル内での最大速度が埋め込まれます。これがそのタイルの代表的な速度となります。
次にこの TileMaxBuffer の各ピクセルにおいて、(x±1, y±1) の範囲の最大速度を求めます。
こうして求めたバッファを NeighborMaxBuffer と呼びます。
ブラーを描画する際に使用されるのはこのバッファです。
さて、このバッファは何を意味するのでしょうか?
G-Buffer とともに速度バッファを描画した場合、その問題点はモデルが描画されたピクセル以外には速度が埋め込まれない点です。
上の画像で言うなら青い背景部分には速度が埋め込まれていないため、本来ならロボットの肩部分が残像として残っているはずなのに残らなくなってしまっています。
しかし、NeighborMaxBuffer によってその近辺の最大速度を取得することが出来ます。
近隣の最大速度の方向にたどっていくことによって、本来そこに残っているであろう色を取得できる可能性が出てくる、と言うわけです。
これにより、同一シーンにおいて以下のような結果となります。
若干エッジ部分が目立つとは言え、以前のものに比べてだいぶ良くなっています。
コードの解説は行いませんが、Paper にある擬似コードをほぼそのまま実装しただけです。
それほど難しくはないと思いますが、Paper では速度がピクセル単位であること、深度値は奥行きが -Z であることに注意してください。
また、今回はジッタのためのノイズが適当なランダム値にしていますが、Perlinノイズのような連続性のあるノイズを利用した方が綺麗に見えると思います。
今回のサンプルではループ数が少ない状態でジッタを ON にするとあまり見栄えがよろしくありません。
さて、最初に欠点もあると書きましたが、高速な回転運動に対してはブロックノイズが発生することがあります。
このようなノイズの発生は TileMaxBuffer、及び NeighborMaxBuffer の求め方に起因していると考えています。
TileMaxBuffer を求める際、そのタイル内の最大速度を取得するようにしています。
しかし、その速度がそのタイル内で支配的な速度かどうかは保障されません。
例えば、カメラが左右に大きく移動しているにも関わらず、そのタイルに1ピクセルのパーティクルが描画され、これが上下方向により速い速度を持っていたらどうなるでしょう?
そのタイルでは本来支配的な速度が左右方向であるにも関わらず、TileMaxBuffer には上下の速度が与えられてしまいます。
他の部分はカメラの移動に合わせて左右方向にブラーされるのですが、このパーティクルが存在するタイル、及び近隣のタイルだけは上下方向にブラーがかかります。
これがブロックノイズの原因と思われます。
もちろん、NeighborMaxBuffer でも同様で、下図のような TileMaxBuffer から NeighborMaxBuffer が作成された場合を想像してみてください。
結果は十分に予想できますよね?
速度が十分に遅ければここまで酷くブロッキーにはならないのですが、速度が速かったり露出時間が長かったりすると厳しいです。
シーン全体に対してリニアな速度が与えられている場合は有効な技術ですが、回転していたり複数のオブジェクトが縦横無尽に飛び交っていたりするシーンでは使いにくい技術です。
回避策として、NeighborMaxBuffer に TileMaxBuffer の値を埋め込んでおき、両方のベクトルから求めたカラーをブレンドする、なんて方法もありかなとは思いました。
他にも、TileMaxBuffer と NeighborMaxBuffer に上下方向と左右方向にそれぞれ近い2つのベクトルを代表値として保持し、やっぱり前述と同様にブレンドってのもありかもしれません。
その辺りは実際に試してみないとどう見えるかわかりませんが、速度面では不利になること請け合いです。
と言うわけでサンプルは以下から。
起動直後は通常のモーションブラーですが、Reconstruction を true にすると再構築フィルタがONになります。
回転のブロックノイズをチェックしたい人は Sample113.cpp の567行目付近をチェックしてみてください。
また、今回は .lib ファイルを同梱していませんが、前回のサンプルと変わっていませんのでそちらから取得してください。
追記:
SIGGRAPH 2012にてこのブラーを開発したVicarious Visionsの方々が実装について発表しています。
こちらのページの"Scalable High Quality Motion Blur and Ambient Occlusion"です。
この資料によると、タイルによるアーティファクトを防ぐため、VnとVcを交互に用いてサンプリングするとあります。
VnはNeighborMaxBufferの速度ベクトルで、Vcは元の速度バッファのベクトルです(シェーダコード中ではVxとなっています)。
と言うわけで、交互に用いるミックス手法を取り入れたサンプルに切り替えてみました。
ダウンロードは下から。
結果ですが、だいぶマシになってます。
完全に、とは言わないまでも、ブロックサイズとサンプル回数を調整してやれば問題ない程度までは持って行けると思います。
明らかにブロッキーだった部分が、よく見ればブロッキーだね、という程度までには落とし込めていると思います。
デバッグメニューのMixの項目をtrueにすればミックス処理が有効になりますので、試してみてください。
なお、前述の発表資料によると、現世代コンソールはブロックのサイズが10ピクセル、DX11対応でも18ピクセルだそうです。
私のサンプルは20ピクセルとなっていますが、10ピクセルぐらいだとかなり穏当な結果になりました。
レースゲームのようなかなりの高速移動をするゲームでなければ10ピクセルくらいで十分かと思います。