異方性ライティング


~ Ashikhmin Lighting ~







下の画像をクリックすると、異方性のパラメータを変化させたAshikhmin照明の効果がアニメーションで見られます。


■はじめに

今回は、サテンやベルベットの生地に適したライティングである、Ashikhmin 照明モデルを扱います。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。

■Ashikhmin 照明モデルとは

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 のモデル、

             (nh)n
ρs(e, l) = c ―――
              nl

が、使われます。nは面の粗さをあらわすパラメータです。
nl で割る部分は、BRDFの定義をするための積分計算

Lr(e) = ∫ρ(e, l)Li(l)cosθll

に、入射方向の余弦が入るので、その項をキャンセルするために挿入します。
ここで、Lr(e)は、放射された放射輝度で、Li(l)は入射された放射輝度です。

ただし、このρs(e, l)は、BRDFが備えているべきであるHelmholtzの相反性ρs(e, l)=ρs(l, e)を満たしていません。
Neumann と Neumann は、鏡面反射の特性から、相反性を満たすBRDFを考案しました。
Neumann と Neumann が導入したBRDFは、

                  (nh)n
ρs(e, l) = c ――――――― F(kh)
              max(ne, nl)

です。kは、elのどちらかになります。
ここで、金属物質が持つFrenselの項を追加しました。

さて、Ashikhmin らは、このBRDFに異方性を追加しました。
異方性は、反射のパラメータnを拡張します。
nに関して、従法線ベクトル方向uと、接ベクトル方向vに応じて反射率が違うものと設定します。
ここで、従法線ベクトルや接ベクトルは、それぞれの局所座標で直行する必要がありますが、場所に応じて向きは異なります。例えば、CDの裏面をレンダリングしようと思えば、 従法線ベクトルや接ベクトルは、CDの動径方向と、円周方向に取るのが適切でしょう。

Ashikhmin らによるBRDFは、次のようになります。

              (nh)nucos2φ+nvsin2φ
ρs(e, l) = c ―――――――― F(kh)
               max(ne, nl)

nu = nv の異方性がないときには、Ashikhmin らのBRDFは、Neumann & Neumann のBRDFと一致します。

さらに、Ashikhmin らは係数cも求めました。係数はエネルギーの保存則

R(e) = ∫ρ(e, l)cosθll≦1

から求められ、最終的な結果は、

              (nu+1)1/2(nv+1)1/2   (nh)nucos2φ+nvsin2φ
ρs(e, l) = Rs ―――――――― ―――――――――― F(kh)
                    8π       (hk) max(ne, nl)

になります。

Ashikhmin らは、拡散光成分に関しても議論しています。
彼らは、Shirley らによる総エネルギーの保存を考慮した、拡散光に関するBRDF

ρd(e, l) = c Rd (1-R(e)) (1-R(l))

から、議論を始め、鏡面反射光の時と同じように、エネルギーの保存則から、

            28                      ne            nl
ρ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(ne, nl)
            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(kh)
ρs(e, l) = Rs ―――――――― ―――――――――― (nh)nucos2φ (nh)nvsin2φ
                8π       (hk) max(ne, nl)

ここで、角度を幾何学的に記述すると、

          (hu)2
cos2φ = ――――
         1-(hn)2

          (hv)2
sin2φ = ――――
         1-(hn)2

のように描けるので、BRDFは、次のように描けます。

              (nu+1)1/2(nv+1)1/2
ρs(e, l) = Rs ―――――――― Tex1(hk, max(ne, nl)) Tex2(nh, √nu hu) Tex2(nh, √nv hv)
                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にオフセットを設定します。huは、正の値も負の値も取りうるので、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(hu)                  √nu(hv)
Tex2(nh, ―――― + 0.5f) Tex2(nh, ―――― + 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: }

図で見ると、次のような感じでしょうか。

■最後に

実用にはなるようですが、パラメータの調整は細かくする必要がありそうですね。





もどる

imagire@gmail.com