DirectXの話 第137回

Fourier Opacity Mapping

DirectX11世代のゲーム(PS4などの次世代機含む)では様々な技術の採用が行われることになるかと思います。

もちろん、それらのすべてが素晴らしい速度で素晴らしい映像を作り出すというわけではないでしょう。

しかし、先端技術を取り入れていく会社のゲームはきっとそれらの技術を利用してくるものと思われます。

今回紹介するのはやはり次世代では必要になりそうな、パーティクルのシャドウマッピング手法の一つである、"Fourier Opacity Mapping (FOM)"です。

I3D 2010でNVIDIAが発表した技術で、NVIDIAのサイトにもサンプルが存在しています。

論文が置いてあったサイトが消えてしまっているようですが、CiteSeerXのこちらのリンクからPDFをダウンロードすることが可能です。

まず、今回の技術の話をする前に煙のようにパーティクルで表現されるものの影について考えてみます。

パーティクルで表現されるものはゲーム中では色々な種類があります。

特に日本のゲームの場合、ヒットエフェクト、魔法の効果などでよく使われますが、これらはたいてい自己発光しているもので影を落とさないものでしょう。

まあ、ヒットエフェクトが光ってるってどういうことだよ、って思わなくもないですが(笑

影を落としそうなパーティクルエフェクトと言えば、前述の煙や湯気、砂埃、パーティクルエフェクトとは言えないかもしれませんが雲なんかもそれでしょう。

現実世界では、これらはビルボードではなく、粒子、まさしくパーティクルなわけです。

現実世界の光は目に見えないはずの空気の粒子ですら吸収(Absorption)、散乱(Scattering)が発生します。

それよりも粗い粒子である煙などではこれらの効果が大きく発生するのも当たり前で、それ故に人間の目で見える状態になるということになります。

残念なことに、現在のCGではこれらの粒子を完全にシミュレートし、光の吸収、散乱も完全にシミュレートするようなことはリアルタイムには難しいです。

たばこの煙を1ピクセルの粒子で表現してそれをリアルタイムで描画可能なPS2(笑)など存在しません。

そのため複数の半透明なビルボードを重ね合わせ、煙っぽく見せるという手段を用いるのが一般的です。

これらが落とす影というのは、基本的には光源からレイを飛ばし、それらが煙を突き抜けてシャドウレシーバに届いた際の光の量として求めることが可能です。

煙のないところを飛んできたレイは減衰せずにシャドウレシーバに到達することでしょう(実際には空気による減衰が存在する)。

しかし、煙の中を飛んできたレイは煙の濃さに応じた吸収や散乱が発生し、光は大きく減衰することでしょう。

ちなみに、散乱は光を減衰させるだけでなく増幅される可能性もあります。

散乱した光が別の光と同じ方向に飛んでいく場合はある方向の光が増幅されたと見ることが可能です。

虫眼鏡で光を集中させるようなものと言えるかもしれませんが、こちらの場合は散乱ではなく屈折ですね。

なんにせよ、この散乱による減衰、増幅も考慮するのは非常に難しいので、今回の技術では吸収のみを取り扱います。

さて、それでは本題に入りましょう。

今回紹介するFOM以外にもOpacity Mappingと呼ばれる技術はいくつかありますが、基本的には光源からの距離dに対するα値をどう表現するかという部分に問題が集中しています。

以下の図をご覧ください。

この図はライトビューからパーティクルを描画した際の、1ピクセル分の深度0.0~1.0でビルボードとα値がどのような関係を持っているかを適当に示したものです。

図中、オレンジの線がビルボードで、このα値は一律0.5とします。

α値をそのまま光の吸収率と考えると、Daの深度では光の強さが元の0.5倍に、Dbでは0.25倍に、Dcでは0.125倍になることがわかります。

普通にα値を描画しただけでは、最終結果として(1 - α1) * (1 - α2) * (1 - α3) * … * (1 - αn)の結果しか求められません。

この方法でシャドウを落とせるのはパーティクルより奥にある物体に影を落とす場合のみです。

パーティクルへのセルフシャドウや、パーティクル内のオブジェクトには影を落とすことは出来ません。

パーティクルの影を表現する上で重要なのは、ある深度dに対して如何に適切なα値を返せるかどうかという点です。

これを実現する簡単な技術として"Opacity Shadow Mapping"があります。

この技術は本当にわかりやすいですが、単純にライトビューの深度方向に複数枚のレンダリングターゲットを作成、それぞれの深度でのα値をそれぞれのレンダリングターゲットに保存する方法です。

例えば深度方向に2分割してMRT0とMRT1へ描画すると仮定します。

この場合、深度0.0~0.5のパーティクルはMRT0に描画され、0.5~1.0のパーティクルはMRT1に描画されます。

最終的には影を落とされる物体の深度から必要な分のテクスチャフェッチを行い(深度0.0~0.5ならMRT0のみ、深度0.5~1.0ならMRT0とMRT1)、それら全てを積算すればOKです。

この手法の問題はMRTへの描画よりテクスチャフェッチ回数でしょう。

画像品質を上げる場合、MRTのスライス数が重要になってきます。0.0~1.0の深度を細かく分割すればするほどクオリティが上がりますが、その分テクスチャフェッチ回数が増えてしまいます。

そこで考えられたのが今回紹介するFOMです。

あるピクセルを通るレイについて考えるなら、吸収のみなので、深度dに対するα値aの関数と見ることが可能です。

a = f(d)

このfという関数が求められるなら、深度からα値を求めることが出来るわけです。

察しのいい人ならわかると思いますが、この関数を求めるにはフーリエ級数を用いれば良いのです。

正確とは言えませんが、係数を増やせば増やすほど関数の精度は高くなります。

係数を求めるためのテクスチャフェッチ回数は深度によらず固定になってしまいますが、細かく分割されたMRTをフェッチするよりはマシなんじゃないでしょうか。

なお、計算式の導出方法はここでは述べません。前述の論文を読んでいただければ詳細に導出されているのでわかりやすいかと思います。

しかもFOMは現在のリアルタイムCG描画と相性が良いという点が挙げられます。

ライトビューからビルボードを描画した際、そのビルボードから求められる係数と別のビルボードから求めた係数とを複雑な計算で合成しないといけない、となるとDirectX11とは相性が悪くなります。

しかし、この係数同士の計算は加算合成でOKです。こちらも論文の導出式を見ていただければわかります。

FOMを描画するテクスチャは係数を考慮して用意する必要があります。

今回のサンプルではk=3としているため、必要な係数は7つになります(a0~3, b1~3)。

7つの係数なので、4チャンネルのテクスチャが2枚必要になります。

NVIDIAが公開しているサンプルはk=7で4チャンネルのテクスチャが4枚使われています。

もちろん、これ以上の精度を求めるならより多くのテクスチャと計算量が必要になるでしょう。さすがにそのレベルになると現実的ではなくなってくるように感じます。

また、使用するテクスチャは16Fを利用するのが良いかと思います。8bitでは精度的に問題が大きくなってしまうでしょう。

コードの解説は行いません。

導出式をそのまま利用指定だけですので。

ただ、1点だけ注意を。

FOMを求める際、-log( 1.0 - alpha ) を求めています。

対数ですので、alphaが1.0以上の場合は計算結果がおかしくなります。

(1.0 - alpha) の結果は正の微少値以上になるようにしておくのが良いでしょう。

サンプルでは1e-3未満にはならないようにしています。

では、サンプルは以下からDLしてください。

Download:Sample128.zip

デフォルトではモデルの影は通常のシャドウマップを、パーティクルはFOMを用いるようにしています。

FOM Alpha はFOMを生成する際にビルボードテクスチャから取得したα値に対して積算する値です。このα値はパーティクルをビュー空間で描画する際には利用していません。

パーティクルの量が多いと光の吸収が大きくなりすぎてしまって、光が当たっている面の少ししか光が届いていない状態になってしまいました。

これを緩和するための措置と見ていただければ良いかと思います。

もう1点の緩和措置として FOM Power がありますが、こちらはシャドウレシーバを描画する際にFOMから求めた光の減衰値をどの程度強くするかの値です。

FOM Only はモデルの描画もFOMを用いてみるという、ちょっとした余興みたいな機能をON/OFFします。

Model Alpha がFOM描画時のモデルのα値で、実際のモデル描画時にはこの値は用いられません。

もちろん、α値ありでFOMを描画する関係で、影が薄くなったり重なってる部分が微妙になったりしています。

また、この値を大きくするとFOMの精度があまりよろしくないこともわかるかと思います。

実際、ライトビューの奥側の影が手前に出てきてしまうことがあります。これはモデルだけではなく、パーティクルにしても言えます。

係数を増やしてより高い精度を利用すればこの問題は解消出来るとは思うのですが、どの程度の係数を用いれば何とかなるのかは残念ながらわかりません。

係数を増やして色々試してみるべきかもしれませんね。

今回のサンプルではやっていませんが、もしも色つきの煙で影にも色を付けたいという場合はどうすれば良いでしょうか?

この方法としては、FOMをカラー分だけ用意する以外の方法を私は思いつきません。

他にもFOMを実装されている海外の方もいらっしゃいますが、そちらでもカラーのFOMはカラーの分だけ用意していました。

つまり、今回のk=3なら2*3色で6枚必要になります。

NVIDIAのようにk=7なら4*3色で12枚。DirectX11では対応出来ませんね。11.1なら確かMRTが64枚まで使えるはずなので対応出来ると思いますが。

次回ですが、もうすこしパーティクルシャドウをやっていこうかな、と思っています。

まあ、GDCのあれですね。