LPV(Light Propagation Volume) メモ

 UE4のリアルタイムGIは重量級のSVOGI(SparseVoxelOctreeGI)ではなく、処理の軽いLPV(LightPropagationVolume)に変更され実装されているようです。
(UE4を採用しているFableLegendsのGI実装→Blog - Latest News from Lionhead : Dynamic Global Illumination in Fable Legends)
 CryEngine,UE4の両方に実装されているため、リアルタイムGI手法のスタンダードになりそうです。


LPVは元々Crytekにより開発された技術です。まず以下の資料を参考にしていきます。
http://www.crytek.com/download/Light_Propagation_Volumes.pdf

アルゴリズムの手順

f:id:IARIKE:20140510012703p:plain

1. RSM(ReflectiveShadowMap)の生成

 GIを適用したい光源(例えば太陽光=DirectionalLight)をカメラとみなし、そこからRSMレンダリングを行います。
光源のカメラから見える(=レンダリングされる)ピクセルは、当然光源に照らされます。
光源に照らされたピクセルは、そこから半球方向へ均一に光を反射します。

 GIの計算で求めるのは、それらのピクセルから放射された反射光が、他のオブジェクトを照らす度合いです。
従って、それらのピクセル1つずつを、仮想的なライト(VPL:VirtualPointLight)と考えることが出来ます。

※RSM(ReflectiveShadowMap)とは?
 名前の示す通り、ShadowMapから派生したものです。ShadowMapは光源から深度をレンダリングしたものですが、
RSMでは光束、法線、深度の3種類の値をレンダリングします。RSMの各ピクセルをポイントライトと考えたのがVPLです。

2.VPLの挿入(Injection)

 格子状に区切った領域に、1で求めたVPLを挿入します。(図では4x4の2Dグリッドですが、実際には4x4x4の3Dグリッドです。)
 挿入するためには各VPLがグリッド上のどのセルに位置しているかを知る必要があります。
1で生成した深度から、ワールド座標を計算しセルのインデックスを求めます。
 この3Dグリッド構造のことをRadiance Volume(放射輝度ボリューム)といいます。

※RadianceVolume(放射輝度ボリューム)とは?
 GIを適用したい空間を一定の間隔(例えば32x32x32分割)で区切り、その場所でのGI情報を各セルに格納したものです。
 各セルには、全球の放射輝度を球面調和関数(SphericalHarmonics)の係数に圧縮したものが入ります。SHは後でやります。

放射輝度ってなに?
放射輝度 - Wikipediaによると

放射輝度の定義は単位立体角あたり、単位投影面積あたりの放射束。

放射輝度ボリュームの場合は光源の面積は無いので、単純にとある1方向へ光が放射される量と考えられます。

3.放射輝度ボリュームの伝搬(Propagation)

 2で挿入したVPLは言わば種のようなもので、それだけでは挿入されたセルの近くしか照らしません。
そこで、伝搬処理を繰り返し行い、放射輝度を全てのセルに伝搬させていきます。

 2でVPLをインジェクションした直後の放射輝度ボリューム(RV0)を最初の入力として、次の放射輝度ボリューム(RV1)を計算します。
次に、求めた放射輝度ボリューム(RV1)を入力として、更にその次の放射輝度ボリューム(RV2)を計算します。
図ではこの処理を3回繰り返しています。

4.レンダリング

 3で計算したそれぞれの放射輝度ボリューム(RV0,RV1,RV2...)を合計したものを使ってGIのレンダリング(間接光の追加)を行います。
図では球が壁からの反射を受けている様子を示しています。

放射輝度ボリュームの球面調和関数での表現

 図を見るとわかりますが、各セルには単なる数値やベクトルではなく、放射輝度の球体上での分布が入っています。
球面座標系で方向(θ、φあるいは法線ベクトル)を指定して、その方向の放射輝度を得られるような、データです。
(緯度経度を指定して地球の標高値を知る、ようなイメージです。標高値が放射輝度の代わりです。)

 球体上の分布といえばキューブマップで表す事ができますが、各セルにキューブマップを格納するわけにはいかないので、圧縮してなんとかRGBAに入らないか考えたのでしょう。
(放射輝度ボリュームのサイズは32x32x32なので、32768個のキューブマップテクスチャになり、現実的ではありません。)
そこで出てくるのが球面調和関数(SphericalHarmonics)で、この関数を使うと少ない係数で球体上の値の分布を近似表現出来ます。
球面調和関数 - Wikipedia
t-pot『テイラー、フーリエ、球面調和関数』

 LPVで用いられている球面調和関数(ここからSH)は二次までで、係数は4つになります。4つの数値ならRGBAテクスチャにちょうど入ります。
(Wikipediaの図だと、2段目まで。計4つの形を4つの係数で混ぜあわせて近似してる…というイメージでいいのかな)
つまりたった4つの係数で球面上の放射輝度の分布を近似(圧縮)しているわけです。


 実際にSHを使用する方法を見ていきます。圧縮(SH係数を求める)方法と伸張(特定の方向の値を取得)方法です。
これはhttp://blog.blackhc.net/2010/07/light-propagation-volumes/にあるpdfの4.2,4.3に、両方書いてあります。
(実はソースも全部Zipに入って配布されてます。形式は7zipなので、zipから7zに拡張子を変える必要がありました。)

圧縮(SH係数を求める)

係数を{\vec{s}} = float4 { (s_0,s_1,s_2,s_3)}としたとき

{ \displaystyle
s_0 = \frac{\sqrt{\pi}}{2} = 0.8862269255
 }

{ \displaystyle
s_1 = -\sqrt{\frac{\pi}{3}}N_y = -1.0233267079N_y
 }

{ \displaystyle
s_2 = \sqrt{\frac{\pi}{3}}N_z = 1.0233267079N_z
 }

{ \displaystyle
s_3 = -\sqrt{\frac{\pi}{3}}N_x = -1.0233267079N_x
 }

に、圧縮したい値(ここでは放射輝度{R}を掛けたものです。

ここで{\vec{N} = (N_x,N_y,N_z)}放射輝度の方向、{R}はその方向の放射輝度です。
例えば様々な方向{\vec{N_i}}を向いた放射輝度{R_i}が10個ある場合、係数の合計は

{ \displaystyle
 \vec{c} = \Sigma_{i=1}^{10}\vec{s_i}Ri
 }

で求められます。この値が放射輝度ボリュームに格納されます。

伸張(値の取得)

知りたい放射輝度の方向を{\vec{N}(x,y,z)}、結果の放射輝度{R}とした時

{\vec{S}} = float4{(S_0,S_1,S_2,S_3)}

{ \displaystyle
S_0 = \frac{1}{2\sqrt{\pi}} = 0.2820947918
}

{ \displaystyle
S_1 = -\frac{\sqrt{3}}{2\sqrt{\pi}}y = -0.4886025119y
}

{ \displaystyle
S_2 = \frac{\sqrt{3}}{2\sqrt{\pi}}z = 0.4886025119z
}

{ \displaystyle
S_3 = -\frac{\sqrt{3}}{2\sqrt{\pi}}z = -0.4886025119x
}

でSHの{\vec{N}}方向に対するベクトルを求め、
係数を{ \vec{c} = (c_0,c_1,c_2,c_3)}としたとき

{ R = S_0c_0+S_1c_1+S_2c_2+S_3c_3}
{ = \vec{S}\cdot\vec{c} }


 で得られます。
 放射輝度スカラー値なので、RGBそれぞれにSHの4係数必要になります。
従って合計12個のチャンネルが必要で、RGBAのレンダーターゲットが3枚必要になります。

放射輝度ボリュームの伝搬

 インジェクション直後の一番最初のRadianceVolumeをRV0とした時、伝搬処理を行ったRV1を求めます。
RV1のピクセル(=RadianceVolumeにおける1つのセル)を計算する時…

f:id:IARIKE:20140510032033p:plain

1.現在のセルに隣接する6つの放射輝度分布(SH係数)をテクスチャ(RV0)から読み込みます

2.現在のセル(図の中心のセル)には6つの面があります。(当然です)

3.6つの面それぞれに放射輝度を集めます。

4.例えば、まず左隣りのセルから集めるとします(図の左上)

5.6つの面の法線を考えた時、左側の面は法線が右を向いているので、左側のセルから放射輝度を受け取ることはありません。

6.従って左隣のセルから放射輝度を受け取るのは、右、上、下、正面、背面の5面分になります。(図だと正面と背面が書いてませんが、斜線で塗ってある面です)

7.当然これは6つの隣接するセル全てに言えることなので、放射輝度を集める処理は6*5=30回行えばいいことになります。

8.放射輝度を集める式はさっきのブログの
http://blog.blackhc.net/wp-content/uploads/2010/07/lpv-annotations.pdf
の6.2にあるのですが、

{
\frac{\Delta\omega}{4\pi}I(\omega_c)
}で、

{\Delta\omega}は立体角、それを球の面積で割ったものに、{I(\omega_c)=\omega_c}方向の放射輝度を掛けてるわけです。
(立体角については図の右のSolidAngleを見て下さい。球に面を投影した時の面積です。)
{\omega_c}方向は隣接セルの中心から面へ向かうベクトルを正規化したものです。

Crytekの論文だけでは立体角は分からなかったのですが、さっきのブログの人が親切に計算してくれて値が載っています。
正面向きの面は0.400669685、側面の4面に関しては全部同じ(回転させると一緒なので当然)で0.42343135443673907668になってます。
また正規化した後のベクトル{\omega_c}も計算してくれています。(これは難しい計算ではないですが。)
{I(\omega_c)}は1で読み込んだSH係数と、正規化した面への方向のベクトルからSHを展開して得られる放射輝度になります。


9.全ての面の放射輝度が求められたら、再投影(Reprojection)を行います。
 6面の方向それぞれの向き(面法線の逆)に、それぞれ求められた放射輝度を使ってSH係数を計算し、
 全て足し合わせ、そのセルのSH係数(放射輝度)とします。

VPLインジェクション時のメモ

 面は完全なランバート反射をすると考えるので、考慮すべきは面の法線{\vec{n}}とライトベクトル{\vec{L}}内積だけです。
またDirectionalLightからレンダリングする際、距離は任意に設定出来る(放射輝度ボリュームが完全に含まれるように)ため、
放射輝度ボリュームの1セルに対するRSM1ピクセルの大きさは一定ではありません。
(近いと1セルに多くのピクセルが入り明るくなりすぎ、遠くからだと逆に1セルに入るピクセルが少ないので暗くなります。従ってこれを調整する必要があります。)
 例のブログに載っている例では、カメラ(ライトから見た)画角を90度にしたとき

output.surfelArea = 4 * posWorld.w * posWorld.w / RSMSize.x / RSMSize.y;

 とあります。これはレンダリング平面が1m先にあると考えた時、画角90度では2m*2mになるため4が現れ、
 それをRSMの解像度で割ると1ピクセル当たりの面積がわかります。さらにposWorld.wの自乗で距離による面積の増加を示しています。
 また放射輝度ボリュームの1セルのサイズが大きくなるとそこに入るVPLも自然と増えるので、セルのサイズも考慮する必要があります。

まとめ

 手順が面倒な上、数学的にも難しくて大変です。
実装もCrytekの実装と上のブログの人の処理は細かい数値が違うようで、どれが正解かはわかりません。

手書きの図汚くてすいません。でも自分用メモだからいいよね!!