下の画像をクリックすると、異方性のパラメータを変化させた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: }
図で見ると、次のような感じでしょうか。
実用にはなるようですが、パラメータの調整は細かくする必要がありそうですね。