下の画像をクリックすると、通常のランバート照明からOren-Nayer照明の効果が次第に強くなるアニメーションが見られます。
今回は、粘土やコンクリート、漆喰に適したライティングである、Oren-Nayar 照明モデルを扱います。
今回は、Michal Valientさんのサイトが元になっています(ShaderX2でも発表されるようです)。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
hlsl.fx | シェーダの入ったエフェクトファイル |
main.h | アプリケーションのヘッダ |
main.cpp | アプリケーションのソース |
あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。
Oren-Nayar 照明モデルとは、1992年に Michael Oren と Shree K. Nayar によって、提唱されたモデルで、1994年のSIGGRAPHで、「Generalization of Lambert's Reflectance Model」の題目で詳細な解説が行われています。
Oren-Nayar 照明モデルは、論文中では、O-N modelと書かれていましたが、王-長島と間違われるのを避けるためでは絶対にないでしょうけどOren-Nayar 拡散照明モデルと呼ばれることが多いです。
Oren-Nayar 照明モデルは、ランバート拡散を一般化したモデルです。
ランバート拡散は、つるつるなプラスチックを表現するのによいといわれていますが、
Oren と Nayar は、ざらざらした表面にふさわしい照明モデルを提唱しました。
Oren-Nayar 照明モデルは、Cook-Trrance 金属照明モデルのように、表面を仮想的に細かく見たときのマイクロファセットを導入し、拡散照明を拡張しました。
Oren-Nayar 照明モデルは、視線に依存する拡散光のモデルです。
表面に凸凹があるとすると、視線方向とライトの向きが近いと、明るい部分が多く目に入ってきますが、視線とライトの向きが離れていると、暗い部分が多く目に入るようになります。
このようなマイクロファセットの向きによる光の照射量や、自己遮蔽、自己反射の効果が含まれています。
Oren と Nayar は、具体的な例として、
マイクロファセットの向きが法線の向きとガウス分布の割合で離れている場合の計算をしました(その例の前に、一般論も展開されています)。
結果だけを書くと、
I = cos(N・L)*(A + B * max(0,cos(ae-al))*sin(M1)*tan(M2) ) A = 1 - 0.5*(D2 / (D2 + 0.33)) B = 0.45*(D2 / (D2 + 0.09)) cos(ae-al) = Noramlize(E - (N・E)N)・Noramlize(L - (N・L)N) cos(M1) = max(N・L, N・E) cos(M2) = min(N・L, N・E)
になります。
D は、マイクロファセットの分布の分散で、D=0のつるつるな表面の時には、A=1,B=0になって、ランバート拡散モデルと一致します。
ae-alは、法線と垂直な平面へ射影したライトベクトルと視線ベクトルなす角度です。
図で示すと、次のような各項の計算をしています。
では、プログラムを見ていきましょう。
今回は、ピクセルシェーダでほとんど済んでしまいます。
ピクセルシェーダには、ライトベクトル In.Light、法線ベクトル In.Normal および、視線ベクトル In.Eyeを入力します。
最初にsin(M1)tan(M2)の計算をします。
この計算には、テクスチャによる参照をします。UV値に、N・L、N・E をいれて、テクスチャを使って計算をします(実際には、視線からの輪郭部分で誤差によるノイズが出てしまうので、視線の輪郭部分で負の値を防ぐために0クランプしています)。
hlsl.fx 0069: // ------------------------------------------------------------- 0070: // ピクセルシェーダ 0071: // ------------------------------------------------------------- 0072: float4 PS(VS_OUTPUT In) : COLOR 0073: { 0074: float4 Out = 0; 0075: 0076: float3 l = normalize(In.Light); 0077: float3 n = normalize(In.Normal); 0078: float3 e = normalize(In.Eye); 0079: 0080: float2 tcoord = {dot(l,n), max(0,dot(e,n))}; 0081: float sintan = tex2D( SinTanSamp, 0.5f * tcoord + 0.5f ).x; 0082: 0083: float3 al = normalize(l-dot(l,n)*n); 0084: float3 ae = normalize(e-dot(e,n)*n); 0085: float C = max(0, dot(al,ae)); 0086: 0087: Out = dot(l,n) * (A + B * C * sintan); 0088: 0091: return Out; 0092: }
テクスチャには、次のような、sin(M1)tan(M2)を計算したテクスチャを使います。
このテクスチャは、R16Fフォーマットのテクスチャで、
実際には、左上の部分は値が負です。色が真っ赤な部分は、絶対値が大きい負の部分になります。
このテクスチャは、D3DXFillTexture を使って算術的に求められています。
テクスチャを求める関数は、次のようになります。
main.cpp 0078: //------------------------------------------------------------- 0079: // Name: SinTan() 0080: // Desc: sinAtanB テクスチャの作成 0081: //------------------------------------------------------------- 0082: VOID WINAPI SinTan (D3DXVECTOR4* pOut, CONST D3DXVECTOR2* pTexCoord, 0083: CONST D3DXVECTOR2* pTexelSize, LPVOID pData) 0084: { 0085: FLOAT x = pTexCoord->x; 0086: FLOAT y = pTexCoord->y; 0087: FLOAT min = 2.0f*((x<y)?x:y)-1.0f; 0088: FLOAT max = 2.0f*((x<y)?y:x)-1.0f; 0089: pOut->x = sinf( acosf(min) ) * tanf( acosf(max) ); 0090: }
ピクセルシェーダの残りの部分は、計算式どおりに求めます。
開発の鉄人では、「Lambertモデルに比べて計算が著しく重いが、 見栄えはそれほど良くはならない」書かれてしまった Oren-Nayer ディフューズモデルですが、 思ったよりも重くはなさそうですし、使っていっていい気がします。