DirectX 8.1:Cook-Torrance の照明モデル


~算術的テクスチャーの作り方~




■はじめに

今回は、DirectX8.1 で導入された D3DXFillTexture を使って、テクスチャーを作ります。
この関数は、プログラマブルにテクスチャーを生成する関数です (Kanoさんに、教えていただきました。ありがとうございました)。
これを使って何をしようか考えたのですが、フレネル項をまだ取り扱っていなかったので、Cook-Torrance の照明モデルに使ってみました。

今回のソースは、次のものです。

内容は次のとおりになっています。

draw.cppメインの描画部分。
vs.vsh頂点シェーダープログラム。平行光源ライト+ライティングテクスチャー指定。
bg.cpp背景表示。
load.cppシェーダーやテクスチャーのロード開放
load.hシェーダーやテクスチャーのロード開放
main.cpp描画に関係しないシステム的な部分。変更が無いので、出番無し。
main.h基本的な定数など。今回も画面サイズは512x512です。
tile.bmp (床デカール)
sky.bmp (空デカール)

あと、モデルとして、nsx.xと、実行ファイルの MyBase.exe 及び、 VC++ でコンパイルするためのプロジェクトファイル MyBase.dsw MyBase.dsp が入っています。

■Cook-Torrance の照明モデル

Cook-Torrance の照明モデルは、物理ベースの照明モデルと呼ばれるもので、熱放射論を用いて照明モデルを定式化したものです。
詳しくは、『コンピュータグラフィックス 理論と実践』等を読まれるといいでしょう(というか、今回はそこから流用しています)。

さて、今までも Phone の照明モデルをやっています。
Phone の照明モデルは、ライティングの鏡面反射成分が反射ベクトル R = 2N(N.L)-L と視線ベクトル V の内積のべき乗に比例します。
近似としてのハーフベクトルと法線ベクトルの内積のべき乗の場合の照明モデルは、次の様になります。

Phone の照明モデル

ここで、左上に出ているテクスチャーは、横軸がハーフベクトルと法線ベクトルの内積、縦軸はハーフベクトルと光源ベクトルとの内積になります。

光の強さは、ハーフベクトルと法線ベクトルの内積 H・N にしか依存しないので、縦縞になります。
一方、今回使用した照明モデルのテクスチャーは次のようになります。

Cook-Torrance の照明モデル

縦方向にも濃度変化が現れています。
具体的には、法線が光の正面を向いている場合に、光の強さが下がっています。
言い換えると、光は視線ベクトルによる反射よりも、裏面に回り込もうとします。

では、解説に移りましょう。
今回使った照度方程式(反射光の強さ Ir を求める式)は、

Ir = Iaρa + (N・L)kdρd + (N・L)ksρs
   = Cm(Ca + (N・L)Cd) + Cs

         Ia:環境光の強さ
        ρa:環境光の反射率
         kd:反射率のうち Lambert 光の占める割合
        ρd:Lambert 光に関する反射率
         ks:反射率のうち 鏡面反射光の占める割合 (kd+ks=1)
        ρs:鏡面反射光に関する反射率

         Cm:メッシュ(物体)の色
         Ca:環境光の色
         Cd:Lambert 光の色
         Cs:鏡面反射光の色

です。最初の式が、入射強度や反射率の定義から求められる式で、2つ目の式がその式を元に実際に使用する時の色の強さです。
まぁ、1つ目の式はあまり気にしなくて結構です。
特徴は、メッシュの色 Cm は、鏡面反射光に依存しないようにしたことです。
Lambert 光は、光が一度吸収されて、その後放射される光なのですから、そのスペクトルはほぼ決まっています。というか、まさにそのスペクトルがメッシュの色です。
また、環境色はメッシュを照らすわけです。それは、メッシュに跳ね返った光が Lambert 拡散したことに他ならないわけですから、メッシュの色がつきます。
しかし、鏡面反射光は原子に吸収されずにすぐに放出された光が、他の原子による散乱と共鳴して強く放射された光ですから、メッシュの色とは関係がありません。 より詳しく言えば、物質の境界に関して、一方は原子があり他方は原子が無い状態が、片方(原子がない空間)への散乱を強め、鏡面散乱になります。
従って、(最低次の近似として)鏡面反射の色はメッシュの色に関係ありません。
また、Cs と、定数の様に書きましたが、鏡面散乱は法線や光源ベクトル、視線ベクトルに影響します。

さて、Cook-Torrance の照明モデルです。
Cook と Torrance は、鏡面反射率に次の仮定をしました。

       F    D G
ρs = -- ----------
      π (N・V)(N・L)
      
      F:フレネル項
      D:マイクロファセットの分布関数
      G:幾何減衰率
      V:視線ベクトル

従って、鏡面反射光の色は次のようにかけます。

          F D G
Cs = as ----------
          (N・V)

係数 as には、入射光の色を含む定数が繰り込まれて入っています。
今回は、as の値は、適当に決めました。
では、それぞれの効果を見てみましょう。

□ フレネル項 F

フレネル項は、フレネルの公式で与えられます(真空から屈折率 n の物質へ入射する場合)。

    1(g-c)2     [c(g+c)-1]2
F = ------ (1 + ----------)
    2(g+c)2     [c(g-c)+1]2

    g2 = n2+c2-1
    c  = L・H

導出方は、『理論電磁気学(砂川重信著)』等を読んでください(いやん!もうあたしには、そんな難しいの読めない~)。
フレネル項だけを抽出すると、その値は次のようになります。

実は、ここで、L・H~L・N を使いました。これは、H~N の時、つまり H・N~1 の右側の部分だけで成り立ちます (鏡面反射は、H~N の時に強くなるので、この近似は適当であり、この後も積極的につかっていきます)。
画像を見ると、光が正面から当たっている部分が薄くなっています。
従って、この項が反射の強い部分が反射ベクトルからずれる効果を生み出しています。

□ マイクロファセットの分布関数 D

マイクロファセットとは、平面を作る微小な平面です。
物質をミクロに見ると、小さな平面の集まりであると考えます。その一つ一つがマイクロファセットです。
Cook と Torrance は、巨視的な法線に対してマイクロファセットの微視的な法線分布が Beckmann 分布関数を持つことを指摘しました。
Beckmann 分布関数は、

        1
D = ---------exp(-tan2β/m2)
    4m2cos4β
    
    cos β = N・H

で、与えられます。
この分布関数の強さは、

になります。
これは、Phone の照明モデルと同じ結果に見えます。
分布関数は、Phone の照明モデルで考えたものと同じ反射分布の広がりを与えます。

□ 幾何減衰率 G

この項は、マイクロファセットが他のマイクロファセットに陰を作る効果です。
光の方向が法線に直交しているか、視線が法線に直交している場合に、この効果は大きくなります。

            (N・H)(N・V)   (N・H)(N・L)
g = min(1, 2----------, 2----------)
              (V・H)        (V・H)

  ~ min(1, 2(N・H) )

ということで、この強さは、

になります。分布関数の項に打ち消されて、意味がなくなっていますね。

□ その他の項

それ以外の部分は、

     1  
 ----------
   (N・V)

ですが、これは観測者に見える単位面積あたりのマイクロファセットの濃度だそうです。
ちなみに、

になります。
N・V が小さい、横から眺めた時に強さが強くなる効果が出ています。

これらを全てかけたものが、Cook-Torrance の照明モデルになります。
総じて、縁の部分に鏡面反射が出やすいようですね。

■テクスチャーの作成

テクスチャーを作成するためには、そのための関数を用意しなくてはなりません。
関数の型は決められています。次の形でなくてはなりません。

VOID func(D3DXVECTOR4* pOut, D3DXVECTOR2 *pTexCoord, D3DXVECTOR2 *pTexelSize, LPVOID pData);

pOut は、出力する色で、x, y, z, w の各成分に r, b, g, a の値を最終的に出力します
pTexCoord は、色を書き込む座標で、0.0f~1.0fの値が入ります。
pTexelSize は、テクスチャーのサイズの逆数が入ります。ミップレベルに応じて使い分ける為のものかな?
pData は、その他のパラメータの引渡しに使います。今回は使いません。

ということで、先ほどまで説明していた Cook-Torrance の照明モデルを計算すると、次のようになります。

0067: // ----------------------------------------------------------------------------
0068: // テクスチャーを作成する関数
0069: //       近似:(分布関数の影響により)H・N~1 のところしか効かない
0070: //              以上の近似から L・N~V・N になる
0071: //       pTexCoord->x : cosθ=H・N
0072: //       pTexCoord->y : L・N
0073: VOID MakeCookTorranceTexture(D3DXVECTOR4* pOut, D3DXVECTOR2 *pTexCoord, D3DXVECTOR2 *pTexelSize, LPVOID pData)
0074: {
0075:     float NH = pTexCoord->x;// cosθ=H・N
0076:     float NL = pTexCoord->y;// L・N
0077:     
0078:     // フレネル項
0079:     float n = 1.5f;
0080:     float c = NL;
0081:     float g = (float)sqrt(n*n+c*c-1.0f);
0082:     float F = 0.5*((g-c)*(g-c)/(g+c)*(g+c))*(1.0f+((c*(g+c)-1)*(c*(g+c)-1))/((c*(g-c)+1)*(c*(g-c)+1)));
0083: 
0084:     // Beckmann分布関数の近似
0085:     float m = 0.3f;
0086:     float D = (float)(exp(-(1.0f-NH*NH)/(m*m*NH*NH))/(4*m*m*NH*NH*NH*NH));
0087: 
0088:     // 幾何減衰率
0089:     float G = min(1.0f, 2.0f*NH);
0090: 
0091:     float rho = 1.0f*F*D*G/((NL));
0092:     rho = min(1.0f, rho);
0093:     rho = max(rho, 0.0f);
0094: 
0095:     pOut->x = rho;
0096:     pOut->y = rho;
0097:     pOut->z = rho;
0098:     pOut->w = 1.0f;
0099: }

ほとんどの変数は今までに出てきたものです。
屈折率 n=1.5, Beckmann分布関数のパラメータ m=0.3 でやっています。
最終的に0.0f~1.0fの間にクランプしています。

それでは、今の関数を呼び出して、テクスチャーを作ります。
初期化の段階で、テクスチャーを作ります。

0218: HRESULT InitRender(LPDIRECT3DDEVICE8 lpD3DDev)
0219: {
0220:     HRESULT hr;
0221: 
0222:     // モデルの読み込み
0224:     if ( FAILED(hr = LoadXFile("nsx.x", lpD3DDev)) ) return hr;
0225:     
0226:     // バーテックスシェーダーを作成する
0227:     if ( FAILED(hr = LoadVertexShader("vs.vsh", lpD3DDev, &hVertexShader, dwDecl)) ) return hr;
0228: 
0234:     // テクスチャーを作る
0235:     if( FAILED(hr = lpD3DDev->CreateTexture(512, 512, 1
                                       , D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8
                                       , D3DPOOL_DEFAULT, &pTexture))) return hr;
0236:     if( FAILED(hr = D3DXFillTexture(pTexture, MakeCookTorranceTexture, NULL))) return hr;
0237: 
0238:     InitBg(lpD3DDev);
0239: 
0240:     lpD3DDev->SetRenderState(D3DRS_LIGHTING, FALSE);
0241: 
0242:     return S_OK;
0243: }

黄色い部分がテクスチャーを作成している部分です。
CreateTexture をして、D3DXFillTexture でテクスチャーを作成するだけです。
D3DXFillTexture の第2引数で作成する関数を指定します。
すると、ピクセルの全ての座標に関して、MakeCookTorranceTexture を呼び出して、テクスチャーを作成してくれます。
あとは、普通のテクスチャーとして使えます。

■それ以外のソース

それ以外の部分は、加算合成するだけです。
頂点シェーダーは、色成分がランバート diffuseで、テクスチャーにハーフベクトルを生成してから、法線や光源との内積を取っています。

0001: ; c0-3   -- world + ビュー + 透視変換行列
0002: ; c12    -- {0.0, 0.5, 1.0, 2.0}    N.B. 今回出番無し
0003: ; c13    -- ライトのベクトル (w成分は環境光の強さ)
0004: ; c14    -- ライトの色(メッシュの色)
0005: ; c15    -- 視線ベクトル
0006: ;
0007: ; v0    頂点の座標値
0008: ; v3    法線ベクトル
0009: ; v7    テクスチャ座標
0010: 
0011: vs.1.0
0012: 
0013: ;座標変換
0014: dp4 oPos.x, v0,   c0
0015: dp4 oPos.y, v0,   c1
0016: dp4 oPos.z, v0,   c2
0017: dp4 oPos.w, v0,   c3
0018: 
0019: ; ランバート diffuse
0020: dp4 r0.w,   v3,  c13            ; l・n
0021: mul oD0,    c14,  r0.w          ; ライトの色(メッシュの色付き)をつける
0022: 
0023: ;カメラへの向きVを計算する
0024: add r2, c15, -v0
0025: 
0026: ;V の正規化
0027: dp3 r2.w, r2, r2
0028: rsq r2.w, r2.w
0029: mul r2, r2, r2.w
0030: 
0031: ; H = (V+L)/|V+L|
0032: add r2, r2, c13
0033: 
0034: dp3 r2.w, r2, r2
0035: rsq r2.w, r2.w
0036: mul r2, r2, r2.w
0037: 
0038: ; H・N
0039: dp3 oT0.x,   r2,   v3
0040: 
0041: ; N・L
0042: dp3 oT0.y,   v3,   c13

正規化を2回もするのが、やな部分ですね。

■最後に

やらないと不自然といわれるフレネル項を実装してみました。
ちょっとは金属っぽいですが、パラメータの選び方が下手なのか、そんなにリアルに見えませんね。
やっぱり、どっからか値を引っ張ってこなきゃ駄目かな?




もどる

imagire@gmail.com