下の画像をクリックすると、異方性のパラメータを変化させたAshikhmin照明の効果がアニメーションで見られます。
今回は、サテンやベルベットの生地に適したライティングである、Ashikhmin 照明モデルを扱います。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
hlsl.fx | シェーダの入ったエフェクトファイル |
main.h | アプリケーションのヘッダ |
main.cpp | アプリケーションのソース |
あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。
Ashikhmin 照明モデルとは、1999年に Michael Ashikhmin らよって、提唱された異方性の照明モデルです。
有名なのは、SIGGRAPH 2000での論文でしょうか。ここ最近、spinでも取り上げられていたこともあって取り組んでみました。
Ashikhmin 照明モデルでは、入射光照度と反射光輝度の比であるBRDF(双方向反射関数)を求めるのにすべての精力がさかれます。
最初に、BRDFを2つの成分に分けます。
ρ(e, l) = ρd(e, l) + ρs(e, l)
ρd(e, l)は、BRDFの拡散光の成分で、ρs(e, l)は、鏡面反射光の成分です。
ここで、lは光源への方向ベクトル、 eは視線への方向ベクトルです。
最初に鏡面反射成分を見ていきましょう。
鏡面反射成分には、基本として Phong のモデル、
(n・h)n ρs(e, l) = c ――― n・l
が、使われます。nは面の粗さをあらわすパラメータです。
n・l で割る部分は、BRDFの定義をするための積分計算
Lr(e) = ∫ρ(e, l)Li(l)cosθldωl
に、入射方向の余弦が入るので、その項をキャンセルするために挿入します。
ここで、Lr(e)は、放射された放射輝度で、Li(l)は入射された放射輝度です。
ただし、このρs(e, l)は、BRDFが備えているべきであるHelmholtzの相反性ρs(e, l)=ρs(l, e)を満たしていません。
Neumann と Neumann は、鏡面反射の特性から、相反性を満たすBRDFを考案しました。
Neumann と Neumann が導入したBRDFは、
(n・h)n ρs(e, l) = c ――――――― F(k・h) max(n・e, n・l)
です。kは、eかlのどちらかになります。
ここで、金属物質が持つFrenselの項を追加しました。
さて、Ashikhmin らは、このBRDFに異方性を追加しました。
異方性は、反射のパラメータnを拡張します。
nに関して、従法線ベクトル方向uと、接ベクトル方向vに応じて反射率が違うものと設定します。
ここで、従法線ベクトルや接ベクトルは、それぞれの局所座標で直行する必要がありますが、場所に応じて向きは異なります。例えば、CDの裏面をレンダリングしようと思えば、
従法線ベクトルや接ベクトルは、CDの動径方向と、円周方向に取るのが適切でしょう。
Ashikhmin らによるBRDFは、次のようになります。
(n・h)nucos2φ+nvsin2φ ρs(e, l) = c ―――――――― F(k・h) max(n・e, n・l)
nu = nv の異方性がないときには、Ashikhmin らのBRDFは、Neumann & Neumann のBRDFと一致します。
さらに、Ashikhmin らは係数cも求めました。係数はエネルギーの保存則
R(e) = ∫ρ(e, l)cosθldωl≦1
から求められ、最終的な結果は、
(nu+1)1/2(nv+1)1/2 (n・h)nucos2φ+nvsin2φ ρs(e, l) = Rs ―――――――― ―――――――――― F(k・h) 8π (h・k) max(n・e, n・l)
になります。
Ashikhmin らは、拡散光成分に関しても議論しています。
彼らは、Shirley らによる総エネルギーの保存を考慮した、拡散光に関するBRDF
ρd(e, l) = c Rd (1-R(e)) (1-R(l))
から、議論を始め、鏡面反射光の時と同じように、エネルギーの保存則から、
28 n・e n・l ρd(e, l) = ―― Rd (1 - Rs) (1-(1 - ――)5) (1-(1 - ――)5) 23π 2 2
を導きました。これら式が、Ashikhmin モデルで使われるBRDFの式になります。
さて、Ashikhmin モデルは結構複雑です。今回は、Steigleder と McCool による実装を追います。
簡単なのは、拡散光です。
拡散光のBRDFを簡単に計算できる部分と、複雑な部分に分け、(1-u)5のような複雑な項を含んだ部分に関しては、テクスチャからサンプリングします。
28 ρd(e, l) = ―― Rd (1 - Rs) Tex0(n・e, n・l) 23π Tex0(u, v) = (1-(1 - u)5) (1-(1 - v)5)
このテクスチャTex0は、次のようになります。
実際にテクスチャを作るソースコードは、次のようになります
main.cpp 0088: VOID WINAPI Diffuse (D3DXVECTOR4* pOut, CONST D3DXVECTOR2* pTexCoord, 0089: CONST D3DXVECTOR2* pTexelSize, LPVOID pData) 0090: { 0091: FLOAT u = pTexCoord->x; 0092: FLOAT v = pTexCoord->y; 0093: 0094: u = 1-powf(1-u/2, 5); 0095: v = 1-powf(1-v/2, 5); 0096: 0097: pOut->x = pOut->y = pOut->z = u*v; 0098: }
鏡面反射光のBRDFもテクスチャサンプリングを利用して、複数のテクスチャの積として表現します。
BRDFを適当に分割します。
(nu+1)1/2(nv+1)1/2 F(k・h) ρs(e, l) = Rs ―――――――― ―――――――――― (n・h)nucos2φ (n・h)nvsin2φ 8π (h・k) max(n・e, n・l)
ここで、角度を幾何学的に記述すると、
(h・u)2 cos2φ = ―――― 1-(h・n)2 (h・v)2 sin2φ = ―――― 1-(h・n)2
のように描けるので、BRDFは、次のように描けます。
(nu+1)1/2(nv+1)1/2 ρs(e, l) = Rs ―――――――― Tex1(h・k, max(n・e, n・l)) Tex2(n・h, √nu h・u) Tex2(n・h, √nv h・v) 8π Tex1(u, v) = F(u)/uv Tex2(u, v) = u v2/(1-u2)
さて、Tex2は、u→1のときに指数の分母が発散します。
ところが、ここで具合のいい極限の式
lim u v2/(1-u2) = e -v2/2 u→1
があるので、発散を防ぐことができます。
さらに、テクスチャとして使うために、Tex2にオフセットを設定します。h・uは、正の値も負の値も取りうるので、0.5のオフセットを入れて、テクスチャの中心をずらします。また、スケールファクターβも導入します。nuの値は、10から10000程度まで動くので、テクスチャの範囲がいい感じに収まるようにスケールを入れて調整します。今回は5.0を入れましたが、実際は、√nuぐらいがいいのでしょうか。
スケールを考慮に入れたTex2(u, v)は、次のようになります。
u~1 : e -(β(v-0.5))2/2 Tex2(u, v) = それ以外: u (β(v-0.5))2/(1-u2)
また、BRDFで読み込まれる値はスケールとオフセットで次の変更を受けます。
√nu(h・u) √nu(h・v) Tex2(n・h, ―――― + 0.5f) Tex2(n・h, ―――― + 0.5f) β β
テクスチャTex2は、次のようになります。
テクスチャを作る関数は、次のようになります
main.cpp 0104: VOID WINAPI gs (D3DXVECTOR4* pOut, CONST D3DXVECTOR2* pTexCoord, 0105: CONST D3DXVECTOR2* pTexelSize, LPVOID pData) 0106: { 0107: FLOAT u = pTexCoord->x; 0108: FLOAT v = pTexCoord->y; 0109: FLOAT g; 0110: 0111: if(1.0f-1.0f/256.0f<u){ 0112: g = expf(-5.0f*5.0f*(v-0.5f)*(v-0.5f)/2.0f ); 0113: }else{ 0114: g = powf(u, 5.0f*5.0f*(v-0.5f)*(v-0.5f)/(1.0f-u*u)); 0115: } 0116: pOut->x = pOut->y = pOut->z = g; 0117: }
また、Tex1もu~0, v~0の時に発散しますが、適当な小さな数εを使って、
Tex1(u, v) = F(u)/(ε+uv)
のようにすることによって、発散を防ぐことができます。
テクスチャTex1は、次のようになります。
テクスチャを作る関数は、次のようになります
main.cpp 0134: VOID WINAPI gp (D3DXVECTOR4* pOut, CONST D3DXVECTOR2* pTexCoord, 0135: CONST D3DXVECTOR2* pTexelSize, LPVOID pData) 0136: { 0137: FLOAT u = pTexCoord->x; 0138: FLOAT v = pTexCoord->y; 0139: FLOAT g; 0140: 0141: g = Fresnel(u)/(1.0f/256.0f+u*v); 0142: 0143: pOut->x = pOut->y = pOut->z = g; 0144: }
Fresnel 項は、Schlickによる近似を用いました。
0123: FLOAT Fresnel(FLOAT n) 0124: { 0125: FLOAT Rs = 0.5f;// 鏡面反射率 0126: 0127: return Rs+(1-Rs)*(1-powf(n,5)); 0128: }
では、プログラムを軽く見ていきましょう。
今回もピクセルシェーダでほとんどの作業をこなしています。
頂点シェーダから視線ベクトルや光源ベクトルをもらい、ピクセル単位で正規化しつつハーフベクトルなどを計算します。
後は、それぞれのテクスチャをサンプリングして、係数を掛けることによって値を整えて描画します。
hlsl.fx 0102: // ------------------------------------------------------------- 0103: // ピクセルシェーダ 0104: // ------------------------------------------------------------- 0105: float4 PS(VS_OUTPUT In) : COLOR 0106: { 0107: float nu = 1000; 0108: float nv = 10; 0109: float PI = 3.14159; 0110: float4 Out = 0; 0111: float4 Rd = In.Color; 0112: float Rs = 0.5f; 0113: float fp, fu, fv; 0114: float fd, fs; 0115: 0116: float3 n = normalize(In.Normal); 0117: float3 l = normalize(In.Light); 0118: float3 e = normalize(In.Eye); 0119: float3 h = normalize(l+e); 0120: 0121: float2 td = {dot(l,n), dot(e,n)}; 0122: fd = (28/(23*PI))*tex2D( DiffuseSamp, td ).x; 0123: 0124: float2 tp = {dot(h,l), max(dot(e,n),dot(l,n))}; 0125: fp = tex2D( GpSamp, tp ).x; 0126: 0127: float2 tu = {dot(n,h), sqrt(nu)*dot(h,normalize(In.U))/5.0f+0.5f}; 0128: fu = tex2D( GsSamp, tu ).x; 0129: float2 tv = {dot(n,h), sqrt(nv)*dot(h,normalize(In.V))/5.0f+0.5f}; 0130: fv = tex2D( GsSamp, tv ).x; 0131: 0132: fs = (sqrt((nu+1)*(nv+1))/(8*PI))*fu*fv*fp; 0133: 0134: Out = (Rd*(1-Rs)*fd + Rs*fs)*dot(l,n); 0135: 0136: return Out; 0137: }
図で見ると、次のような感じでしょうか。
実用にはなるようですが、パラメータの調整は細かくする必要がありそうですね。