動的法線マップ


~ Dynamic normal map ~






■はじめに

今回は、やっていそうでやっていなかった法線マップをリアルタイムに計算します。
よくある波のシミュレーションをして、その高さから法線マップを生成して、フォンの鏡面反射則でライティングしています。
上の絵の左上のよくわからない物が波のシミュレーションで、赤色成分が高さ情報、緑色成分が高さの変化する速度になっています。
その下が、その高さに関する法線マップです。
背景はビットマップを表示して、ライトの位置は適当に調整しました。

下のファイルがソースと実行ファイルです。

まぁ、いつものように適当にファイルが入っています。

wave.cg波のシミュレーションのフラグメントプログラム。
normal.cg法線プログラムの生成のフラグメントプログラム。
vp.cg照明計算の頂点プログラム。
fp.cg照明計算のフラグメントプログラム。
main.cppメインループ。
CBitmap.hビットマップファイルの読み込み。
CBitmap.cppビットマップファイルの読み込み。
sky.bmp

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

■波の手続きテクスチャの生成

最初に波の作り方を説明しましょう。
Game Programming Gems に方法は載っていいますが、手元に本がないので、適当にプログラミングしてみます。
波は、テクスチャのそれぞれのテクセルをばねでつないだ粒子の集まりと考えます。

但し、ばねの動く方向は高さ方向だけにしか動かないことにします(物理的には横波だけを扱うことになります。現実にはそのような媒質は存在しませんが、光の反射に寄与する部分だけ抽出したと考えればよいと思います)。
(i,j)の位置にあるばねの運動方程式は、高さ方向だけに注目すると、

dxij = vij dt
dvij = Fij/m dt
     = -(k/m)((xij-xi-1j))+(xij-xi+1j))+(xij-xij-1))+(xij-xij+1)) dt
     = -(k/m)(4xij-xi-1j-xi+1j-xij-1-xij+1) dt

になります。
運動方程式には、位置と速度の2つの自由度があるので、2つのバッファに値を保存します。
位置をテクスチャの赤色成分、速度をテクスチャの緑色の成分に入れます。
ともに0から255の値を持つので、128の時を基準点として、 128からの差で高さや速度を表現します。

実際のシェーダプログラムでは、最近接4近傍のデータを取り出して、

wave.cg
0018:     float2 shift0 = {  0.0f/128.0f,  0.0f/128.0f };
0019:     float2 shift1 = {  1.0f/128.0f,  0.0f/128.0f };
0020:     float2 shift2 = {  0.0f/128.0f,  1.0f/128.0f };
0021:     float2 shift3 = { -1.0f/128.0f,  0.0f/128.0f };
0022:     float2 shift4 = {  0.0f/128.0f, -1.0f/128.0f };
0023: 
0024:     float3 tex0 = f3tex2D( SrcTex, I.tcoords.xy+shift0 );
0025:     float3 tex1 = f3tex2D( SrcTex, I.tcoords.xy+shift1 );
0026:     float3 tex2 = f3tex2D( SrcTex, I.tcoords.xy+shift2 );
0027:     float3 tex3 = f3tex2D( SrcTex, I.tcoords.xy+shift3 );
0028:     float3 tex4 = f3tex2D( SrcTex, I.tcoords.xy+shift4 );

位置には速度を(0.5で引いて1、色の強さが128の時に変化0になるようにして)足します。
速度は、高さの差を足しこんで、変化させます。

wave.cg
0030:     O.col.x = tex0.x + tex0.y - 0.5f;                               // 高さ変化
0031:     O.col.y = tex0.y                                                // 元の速度
0032:                  + tex1.x + tex2.x + tex3.x + tex4.x - 4.0f*tex0.x; // 加速度

今回は、フラグメントプログラムだけを使いましたが、頂点プログラムとフラグメントプログラムを使えば、より効率的に(DirectX8世代の処理で)プログラムができます。

■法線マップの生成

次に、波のシミュレーションで求められた高さ情報から、法線マップを作ります。
法線マップは、法線ベクトルをテクスチャなどのマップに落とし込んだものです。
法線ベクトルの求め方ですが、接ベクトルが法線ベクトルに直行することを利用します。
今回の場合は、高さ情報だけしか変化しないので、X軸及びZ軸の方向に関する高さの偏微分が接ベクトルの方向になります。

上の図のように、高さが変化する場合に、x軸及びz軸方向の高さ変化について前後の勾配の平均を取ると、

      (yi+1j-yij) + (yij-yi-1j)    yi+1j - yi-1j
dyx = ---------------------- = -------------
                2                    2

      (yij+1-yij) + (yij-yij-1)    yij+1 - yij-1
dyz = ---------------------- = -------------
                2                    2

になります。したがって、法線ベクトルは、この勾配から、

n = normalize(du x dv)
du = (1, dyx, 0)
dv = (0, dyz, 1)

の式によって求められます(左手座標系ですね)。

シェーダプログラムでは、隣接する4点のテクセルから高さ情報を取り出して-1から1までの範囲に変換して、それらの差分から接ベクトルを作成した後に、外積、正規化計算して法線ベクトルを作成します。

wave.cg
0017: fragout main(vert2frag I
0018:             , uniform texobj2D SrcTex : texunit0
0019:     )
0020: {
0021:     fragout O;
0022:     
0023:     float2 shiftX = {  1.0f/128.0f,  0.0f/128.0f };
0025:     float2 shiftx = { -1.0f/128.0f,  0.0f/128.0f };
0024:     float2 shiftZ = {  0.0f/128.0f,  1.0f/128.0f };
0026:     float2 shiftz = {  0.0f/128.0f, -1.0f/128.0f };
0027: 
0028:     float3 texX = 2.0f*f3tex2D( SrcTex, I.tcoords.xy+shiftX )-1.0f;
0030:     float3 texx = 2.0f*f3tex2D( SrcTex, I.tcoords.xy+shiftx )-1.0f;
0029:     float3 texZ = 2.0f*f3tex2D( SrcTex, I.tcoords.xy+shiftZ )-1.0f;
0031:     float3 texz = 2.0f*f3tex2D( SrcTex, I.tcoords.xy+shiftz )-1.0f;
0032:     
0033:     float3 du = {1,0.5f*(texX.x-texx.x),0};
0034:     float3 dv = {0,0.5f*(texZ.x-texz.x),1};
0035:     O.col.xyz = 0.5f*normalize(cross(du, dv))+0.5f;
0036:     
0037:     return O;
0038: } 

■ライティング

次の段階は、法線マップを使った鏡面反射光の計算です。
今回は、手を抜いて、曲面計算を省いて平面を張りました。 ということで、頂点プログラムの出力は、視線ベクトルと光源ベクトル及び法線マップのためのテクスチャ座標をテクスチャ座標出力に吐き出します。
視線ベクトルや光源ベクトルはローカル座標で取り扱います。

vp.cg
0024: vertout main(appdata IN
0025:             , uniform float4x4  ModelViewProj
0026:             , uniform float4    EyePos
0027:             , uniform float4    LightPos
0028:             )
0029: {
0030:     vertout OUT;
0031: 
0032:     // 座標変換
0033:     OUT.hpos = mul(ModelViewProj, IN.position);
0034: 
0035:     // 視線ベクトル
0036:     OUT.eye = EyePos - IN.position;
0037:     
0038:     // 光源ベクトル
0039:     OUT.light = LightPos - IN.position;
0040:     
0041:     // テクスチャ座標
0042:     OUT.tcoords.xy = IN.texcoord;
0043: 
0044:     return OUT;
0045: }

フラグメントプログラムでは、法線ベクトルを取り出したり、各種ベクトルを正規化して単位ベクトルを手に入れます。
次に反射ベクトルを作った後に、反射ベクトルや法線ベクトルと光源ベクトルの内積を計算して、光源計算に必要な量を手に入れます。
最後にそれぞれの色の強さを光の強度に掛けて、最終的な色を算出します。
特徴としては、鏡面反射色を黄色にしているのですが、飽和させて白い色を作り出しています。
HDR を使えば、ほうわした部分にフレアを掛けてより面白い効果が狙えると思います。

fp.cg
0014: fragout main(vertout I
0015:             , uniform texobj2D SrcTex : texunit0
0016:     )
0017: {
0018:     fragout O;
0019:     
0020:     float3 N = - 2.0f * f3tex2D( SrcTex, I.tcoords.xy ) + 1.0f;// 法線ベクトル
0021:     float3 E = normalize(I.eye.xyz);        // 視線ベクトル
0022:     float3 R = -E + 2.0f * dot(E, N) * N;   // 反射ベクトル
0023:     float3 L = normalize(I.light.xyz);      // 光源ベクトル
0024: 
0025:     float RL = max(dot(R,L), 0.0f);         // (反射,光源)
0026:     float NL = max(dot(N,L), 0.0f);         // (法線,光源)
0027: 
0028:     float4 amb = {0.1f, 0.05f,  0.04f, 1};  // 環境色
0029:     float4 dif = {1.0f, 0.6f, 0.4f, 1};     // 拡散色
0030:     float4 spe = {2.0f, 1.8f, 1.2f, 1};     // 反射色
0031: 
0032:     // フォンスペキュラー
0033:     O.col = amb + dif*NL + spe*pow(RL, 64);
0034:     
0035:     return O;
0036: } 

■最後に

今回のプログラムはほおって置くと、どんどん波が減っていきます。
精度落ちもあるでしょうし、高さにクランプがかかるのも原因とは思いますが、 そこら辺を解消しないと現実には使えませんね。

フレネル効果を入れていないのですが、縦に伸びる光の筋が観察できます。
波を作るプログラムや法線を作るプログラムには、より効率の良いアルゴリズムがありますが、とりあえず基本として…





もどる

imagire@gmail.com