穴日記

どうだ明るくなったろう

Precomputed Radiance Trasnferのメモ

マイクロソフト(当時)のSloan氏が発表したPrecomputed Radiance Trasnfer(事前計算済み放射輝度伝達)の簡易的なメモ。

リアルタイムに相互反射や無限遠からの環境マップによるImage Based Lightingを考慮したライティングを行うのはかなり大変である。これを解決する手法。

さて、IBLをリアルタイムに行うのはかなり大変である。あるピクセルの色を決定するには、そのピクセルに対応するシーン中のある点の色、すなわち放射輝度を決定しないといけない。これは、その点における以下のような積分を解く必要がある。(なおシーンは簡単のため全て完全拡散面としておく(PRTは完全拡散面以外も扱える))

L_o(x, \omega) = \frac{\rho(x)}{\pi} \int_{\Omega} L_i(x, s) \cos{\theta} d\omega(s)

ここで、\rhoは各点における反射率であり、その点における色や見え方を決定する。さて、右辺の積分を解くことによってL_oの値がもとまり、これは最終的にレンダリングされる画像の各ピクセル値に対応する。しかし、右辺の積分を解くと一口に言っても大変である。相互反射などはどうすればいいのか、といった問題や、そもそもどう積分を解くのか(モンテカルロ法を使うのか、使わないのか)といった問題がたくさんある。

今回は「リアルタイム」に「IBLの影響」を解きたいという状況だと仮定して、どんどん近似を入れていくことで問題を解くことにする。まず、相互反射を考慮しないことにする。すなわち、各点は全て光源から直接照明される以上に、ほかの影響は受けないと考える。(後でこの制限は取り払う)すると、上の式は以下のようになる。

L_o(x, \omega) = \frac{\rho(x)}{\pi} \int_{\Omega} L_e(s) V_x(s) \cos{\theta} d\omega(s)

ここで、L_eはある方向からくる放射輝度の関数であり、これが今回の光源すなわちImage Based Lightである。\cos{\theta}はその位置における法線と方向sの成す角度によるcosである。V_x(s)というのは可視関数で、位置xにおいて、方向sを見たとき、光源(環境マップ)が可視か、それとも別のオブジェクトに遮蔽されて不可視か、を表したものである。可視なら1、不可視なら0になる。
Image Based Lightingというのは、通常点光源、面光源などを扱うコンピュータグラフィックスにおいて、光源を以下のような環境マップによる画像ベースのものにする、というものである。


(Light Probe Image Gallery(http://www.pauldebevec.com/Probes/)より)

この画像の各ピクセルはある方向からくる放射輝度値を表現しており、上のL_eはこの値を得るためのものである。環境マップは、レンダリングしているシーンから十分に遠くに存在する巨大な球に張り付けられたテクスチャであると考えることもでき、それゆえにシーン中の位置に依存しない関数になっている。

ここで球面調和関数が出てくる。球面調和関数は完全直交関数系であり、球面上の任意の関数g(\theta, \phi) を展開できる、とウィキペディアにある。上で出てきた関数L_eV_xも、cos{\theta}も全て方向にのみ依存する関数である。よって、これらは全て球面調和関数として展開できる。この時、何次の項まで展開するかを指定することが出来、次数が多くなればそれだけ精度も増すが、データ量や計算負荷も大きくなる、トレードオフになる。

PRTにおいては、まず、レンダリングしたいデータの各頂点(等)においてV_xcos{\theta}を求める。これは離散的な結果になる場合がある。次に、これらの関数の積を求め、これを球面調和関数で表現し、頂点ごとに別途保存しておく。また、入力として与えたい光源である環境マップについても、球面調和関数として表現する。(これはランタイムで行ってもよい)

レンダリング時には、各頂点において、V_xcos{\theta}の積たる球面調和関数と光源である環境マップの球面調和関数を復元し、全方向について積分してやれば、それはそのまま\int_{\Omega} L_e(s) V_x(s) \cos{\theta} d\omega(s) 積分したことになり、望む結果が得られる。さらに、ここからが球面調和関数の優れたところだが、実際には具体的な関数を復元する必要も、全方向について積分する必要もなく、各係数について内積をとるとそれが最終的に求めたい積分の結果になる。これらの内積の処理は高速に行うことが出来る。光源である環境マップが変化しても、環境マップの球面調和関数としての表現さえ存在すれば、単なる内積によってリライティングができ、しかもそれはちゃんと可視判定を考慮したものとなっているため、高品質なシェーディングが期待できる。

相互反射

さて、以上の話は直接光に限定した話だったが、PRTは間接光を考慮したものに拡張することが出来る。ある点において、環境マップが可視の方向からくる光は、そのまま環境マップの値だが、不可視の方向からくる光は別の場所で一度反射した光、すなわち間接光である。まず、各頂点においてV_xcos{\theta}を計算し、球面調和関数を求める。次に、各頂点から直接光の来ない方向、すなわち不可視の方向にレイをとばし、別のオブジェクトとの交点yを求める。

yが求まったら、その点の位置において同様にして球面調和関数を求める。この球面調和関数と環境マップによって計算される値とは、つまりyからxへと反射される光に他ならない。そこで、yにおける球面調和関数の値について、yにおける反射率と積分の際のその方向への立体角の分をスケーリングしてxにおける球面調和関数に加算してやる。すると、いざレンダリングするときに、L_o = M_x \cdot L_eという関係だったものが、L_o = (M_x + K_y M_y + ... ) \cdot L_eというものになり、xにおいて直接反射する光と、別の点yなどを経由して反射する光の両方が考慮された結果が得られる。
(ここでL_eM_xなどは球面調和関数の係数として表現された各項で、K_yはスケーリング係数)

シャドウマップのメモ

シャドウマップの自分用メモ。

シャドウマップによるアルゴリズムは無数にあるが、基本的には光源側からシーンをレンダリング、光源からの深度値をシャドウマップと呼ばれるデータ構造に保存し、その値を視点側からシーンをレンダリングするときに参照しながらシャドウ領域を特定していく。
(ここでの深度とは光源や視点からの距離を何らかの座標系・空間において線形、あるいは非線形に表したものだとする)


視点側からシーンをレンダリングするとき、ある点について、シャドウ領域か否かを判定したい。
シャドウ領域ということは、光源とその点の間に別の物体(遮蔽物)が存在するということであり、非シャドウ領域(普通にシェーディングされる領域)ということは、光源とその点の間に別の物体が何もないということになる。
さて、レンダリングしたい点について、光源側から見たときの深度というものが計算できる。一方、その点について、シャドウマップ上でどの位置に対応するのかというのもわかるので、シャドウマップ上における深度も求まる。もし、レンダリングしたい点が非シャドウ領域なら光源からその点が直接みえているため、シャドウマップ上に保存される深度値と、改めて計算しなおした光源側からの深度値は一致、ないしは非常に近いものになるはずである。しかし、レンダリングしたい点がシャドウ領域なら光源とその点の間に別の物体が存在し、その物体の方が光源にちかいため、シャドウマップ上に保存される深度値は別物体のものになる。この深度値は改めて計算しなおした光源側からの深度値より小さくなる。こうして、レンダリング対象の点について、シャドウ領域か否かが判定できる。


シャドウマップを作成するときは、光源を普通の視点と見立ててレンダリングを行う。たとえば、光源がスポットライトなら、それは普通のカメラと同じなので透視投影変換を行うし、光源が平行光源なら、それは平行投影変換を行う。
より具体的には、シーンの各頂点に対してモデル座標系からワールド座標系へと普通に変換してから、光源のビュープロジェクション行列をかけてやる。光源のビュープロジェクション行列は光源の位置や種類、向きなどによって求まる。
光源のビュー行列を作用させることで、光源の位置を原点とした座標系にシーンが設定される。プロジェクション行列(透視投影や平行投影)によって、平面上に投影される。この平面がシャドウマップに対応する。具体的に保存する値としては、非線形なZ値だったり、単純に光源からの距離などを保存する。前者は非線形、後者は線形な値が深度値として記録されることになる。


レンダリングするときは、まず対象の点について、光源からの距離を求める。シャドウマップに保存されている値をどう計算したかによって、この計算方法は変わり、シャドウマップに保存された値が線形な距離であれば光源からの距離は単純に求めることが出来る。
次に対象の点が、シャドウマップ上でどの位置に対応するかを求める。これも、上と同様の光源のビュープロジェクション行列をかけてやれば容易に求まる。ただし、これで求まる平面というのはデバイス座標系であるので、テクスチャ座標系にあわせるために若干追加の処理が必要な場合もある。たとえば、前者はXYが-1.0から1.0の範囲なのに対して、後者は0.0から1.0の範囲だったりするのでシフト、スケーリングが必要になる。シャドウマップ上でどの位置に対応するかがわかればテクスチャを参照して深度値を取得、あらかじめ求めてあった光源からの距離と比較し、シャドウ領域か否かを判定する。

法線の変換の話

オブジェクトの頂点をモデル座標系からワールド座標系に移すときに法線も一緒に変換する必要がある。
たとえば、シェーディングするときは法線が必要になるが、たいていライトはワールド座標系に存在しているため、法線もワールド座標系に移す必要がある。
他にも、法線をいろいろ利用するときは頂点と同じ座標系にあった方が都合が良い。


頂点について、モデル座標系からワールド座標系に移すときには何らかの変換行列を使って変換する。
しかし、頂点に対して作用する行列と同じものを法線に対して作用させてもうまくいかない。
まず、平行移動成分を除去しなければならない。法線は向きのみの量なので平行移動は関係ないからである。
回転移動成分についてはそのまま使うことが出来る。
拡大縮小などのスケーリング成分については、たとえば各頂点をX軸方向に3倍にする場合、法線はX軸方向に1/3倍にする必要がある。(最終的に正規化して長さは1にする)


これらの各要素を考慮しつつ法線を適切に変換するための行列を求めるには、頂点の変換行列の逆転置行列を求めればよい、ということが知られている。
まず、与えられた変換行列について、平行移動成分を除去する。これは4x4行列の左上3x3の成分を抜き出せばよい。
次に、この左上の成分の行列{M}について、特異値分解を考える。
すると、{M = R_1 * S * R_2 }という形に分解できる。ここで、特異値分解の性質より{R_1, R_2}は正規直交行列になる。
正規直交行列ということは標準基底の回転によって得られるということであり、これらの行列は回転行列として表現できるということになる。(本当は鏡像も考えなければならない気がするが、拡大縮小行列にまとめることができるので得られる結果は変わらない)
また、特異値分解の性質よりSは対角行列になる。対角行列ということはSは拡大縮小行列として表現できるということになる。
つまり、任意の3x3変換行列は回転行列と拡大縮小行列の積に分解できるということになる。


今回求めたい、法線に対する変換行列というのは、元の変換が回転の場合はそのまま同じ回転を作用させ、元の変換が拡大縮小の場合は逆数の分だけ拡大縮小するものである。
つまり回転行列についてはそれをそのまま作用させ、拡大縮小行列についてはその逆行列を作用させたいということになる。
今、{M = R_1 * S * R_2}なので、法線にたいして作用させるべき行列は{M_w = R_1 * S^{-1} * R_2}である。
回転行列の転置行列は自身の逆行列と等しくなることと、拡大縮小行列の転置行列は自身と等しくなることから、
{M_w = ({R_1}^{-1})^T *  ({S}^{-1})^T *  ({R_2}^{-1})^T}となる。
ここで、行列に対する転置はまとめることができる。ただし掛け算の順番は変わる。

{M_w = ({R_2}^{-1} *  {S}^{-1} *  {R_1}^{-1})^T}

同様に、逆行列もまとめることができる。やはり掛け算の順番は変わる。

{M_w = (({R_1} *  {S} *  {R_2})^{-1})^T}

よって、{M_w = (M^{-1})^T}となるので、法線に対する変換行列は元の行列の逆転置行列となる。