Vertex Shader:法線マップによるバンプマップ


~さらなるディテールアップ~




■はじめに

さて、ありがちなエフェクトでやっていなかったバンプマップです。
バンプマップはでこぼこした質感をライティングによって表現します。
床の質感にバンプマップを使っています。

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

今回は、半球ライティングのソースを変更して作っています。重要なファイルは bg.cpp と bg.vsh です。
ソースは、次の通りです。

bg.cpp地面+天円柱の描画。今回のメイン。
bg.vsh頂点シェーダープログラム。バンプマップ用。
 
main.h基本的な定数など。今回も出番無し。
main.cpp描画に関係しないシステム的な部分。変更が無いので、出番無し。
draw.h描画の各関数の定義。特に意味無いので出番無し。
draw.cppメインの描画部分。半球ライティングのもののまま。
font.hFPS表示用。既出。
font.cppFPS表示用。既出。
load.hファイルの読み込み、開放。
load.cppファイルの読み込み、開放。
light_eff.cppライトの方向を説明するラインを描画。半球ライティングのまま。
resource.hメニューのリソース管理。
 
diffuse.vsh頂点シェーダープログラム。シンプルな平行光源+環境光のライト。
specular.vsh頂点シェーダープログラム。頂点色によるスペキュラーライト。
texspec_vsh.vsh頂点シェーダープログラム。テクスチャーによるスペキュラーライト。
hemisphere.vsh頂点シェーダープログラム。平行半球ライト。
hemi_amb.vsh頂点シェーダープログラム。平行光源+半球環境光ライト。
hemi_spec.vsh頂点シェーダープログラム。平行光源+半球環境光+スペキュラーライト。
soft.vsh頂点シェーダープログラム。やわらかいトゥーン。
edge.vsh頂点シェーダープログラム。輪郭を描く。
tile.bmp (床デカール) normal.bmp (法線マップ)
spec.bmp (スペキュラ用) metal.bmp (メタル用) soft.bmp (やわらかいトゥーン用) sky.bmp (空デカール)

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

■バンプマップに関するうんちく

バンプマップの効果はどのようなものでしょうか?
下にバンプマップをした画像と、していない画像を並べてみました。
バンプマップされた床は、光源方向(右手前)の部分が光っており、立体感が増しています。
実際に出っ張っているわけではないので、真横にしたときに平らな板になってばればれなのですが、光源がぐるぐる動く場面ではかなり強力です。

使用前 使用後

どのようなデータを用意すればよいのでしょうか?
いくつかの方法がありますが、今回は法線マップを用いたバンプマップを行います
バンプマップでは、表面のでこぼこ具合対応した画像を用意します。
normal.bmp が tile.bmp から作成した法線マップのデータです。
nVIDIA の、Photoshop プラグインで作成しました。

tile.bmp (床デカール) normal.bmp (法線マップ)

法線マップを作成するためには、周辺のピクセルと明るさを比較します。
結果として、明るさが異なれば、その方向にベクトルを生成します。

輝度変化が大きければ大きいほど、傾きは大きくなります。
慣習として、輝度変化がおきないときの軸をZ軸として、変化がおきた(U、V)軸を、X,Y軸に対応させます。
normal.bmp がほとんど青色なのは、(R,G,B)を(X,Y,Z)と対応させて、平らなほとんどの部分を青色として表現しているためです。
傾きの大きさは、-1.0f~1.0f の範囲ですが、色の強さは、0~255 なので、

(nx, ny, nz)                         | -1.0f ・・・ 1.0f
------------------------------------------------------------------------------
( R,  G,  B) = (128*(nx+1.0f), 128*(ny+1.0f), 128*(nz+1.0f)) |    0  ・・・  256

のように、変換します。
従って、でこぼこのない平らな部分の色は、(128,128,256) になります。
ちなみに、法線ベクトルの集合を単位球に対応させて、X軸を横軸、Y軸を縦軸とした時の傾きに応じた色は、次のようになります。

次に、この法線マップをモデルに貼り付けなければなりません。
法線マップはでこぼこしていない時は、青色です。
実際のモデルのでこぼこの基準はなんでしょうか?
それは法線方向です。
ライティングは、モデル座標系で行います。
そのために、法線マップを、モデル座標系でZ軸(青色)が法線方向になるように座標変換しなければなりません。
そのための基底ベクトルが必要です。
一つは、頂点の法線で、別の基底ベクトルは、適当に(法線に直交して、光の当っている方向が正しくなるように)取ります。
最後の一つは、以上の2つのベクトルの外積から求めます。

今回用いるのは、床なので、ポリゴンのX軸をS,Y軸をT,法線をSxTの簡単な計算になります。
この座標系を用いれば、Z軸の出っ張り(青色)は、SxTの法線の方向の出っ張りに変換されます。
また、それ以外の軸は、赤及び緑で表現される表面のでこぼこになります。

う~ん、この説明じゃわからんよな。
結果を見たほうが早いだろ。

■ソース

さて、ソースファイルを見てみましょう。
頂点シェーダープログラムは次のようになります。

; c0-3   -- world + ビュー + 透視変換行列
; c12    -- {0.0, 0.5, 1.0, 2.0}
; c13    -- ライトのベクトル (w成分は環境光の強さ)
;
; v0    頂点の座標値
; v3    接空間の基底ベクトル S
; v4    接空間の基底ベクトル T
; v5    接空間の基底ベクトル SxT
; v7    テクスチャ座標0

vs.1.0

; 透視変換
dp4 oPos.x, v0, c0
dp4 oPos.y, v0, c1
dp4 oPos.z, v0, c2
dp4 oPos.w, v0, c3

; テクスチャー座標
mov oT0, v7
mov oT1, v7

; 頂点色を決める
dp3 r3.x, v4, c13       ; ライトを基底ベクトルを使って、モデル座標系に変換する
dp3 r3.y, v5, c13       ; r3 = LIGHT_LOCAL
dp3 r3.z, v3, c13

add r3, r3, c12.z       ; 0-1 にスケール
mul oD0, r3, c12.y

mov oD0.w, c12.z        ; alpha 成分は 1 にする。

座標変換はシンプルな透視変換です。
テクスチャー座標も、そのままコピーします。
今回は、デカールテクスチャー(普通の模様のテクスチャー)と法線マップを張りますが、 同じ模様なので、同じ座標(v7)を用います。
特殊なのは、頂点色です。
ライトの方向を基底ベクトル(S,T,SxT) を用いて座標変換します。
これで、ライトの方向ベクトルは、法線の向きから見た方向に変換されます。
このベクトルと法線マップの各ベクトル(法線マップの色を3次元ベクトルとみなしたときのピクセル単位のベクトル)の内積計算すれば、 平行光源のライティングが実現できます。
ライトの方向ベクトルの各成分の範囲は -1 ~ 1 ですが、頂点色は 0 ~ 1 の範囲でないといけないので、

oD0 = 0.5*L' + 0.5    (L':基底ベクトルで変換したライトの方向ベクトル)

の変換で範囲を調整します。

次は、Cソースである bg.cpp です。
頂点宣言は、次のようになります。

typedef struct {
    float  x,  y,  z;
    float tu, tv;
    float xx, xy, xz;
    float sx, sy, sz;
    float tx, ty, tz;
}FloorVertex;

static DWORD dwDeclFloor[] = {
    D3DVSD_STREAM(0),
    D3DVSD_REG(D3DVSDE_POSITION,    D3DVSDT_FLOAT3 ),    // D3DVSDE_POSITION,  0
    D3DVSD_REG(D3DVSDE_TEXCOORD0,   D3DVSDT_FLOAT2 ),    // D3DVSDE_TEXCOORD0, 7  
    D3DVSD_REG(3,                   D3DVSDT_FLOAT3 ),    // SxT  
    D3DVSD_REG(4,                   D3DVSDT_FLOAT3 ),    // S
    D3DVSD_REG(5,                   D3DVSDT_FLOAT3 ),    // T
    D3DVSD_END()
};

いつもの3次元座標 D3DVSDE_POSITION、テクスチャー座標 D3DVSDE_TEXCOORD0に加えて、 接ベクトル空間の基底 S, T, SxT が、存在します。
適当に、3, 4, 5 番に振りました。

初期化は、次のようになります。

void InitBg(LPDIRECT3DDEVICE8 lpD3DDev)
{
    //
    // 円柱
    //
    
    // 略・・・・・・・・・・・・
    
    //
    // 床
    //
    // 頂点バッファの作成 
    FloorVertex *pFloorDest;
    lpD3DDev->CreateVertexBuffer( 4 * sizeof(FloorVertex),
                                D3DUSAGE_WRITEONLY, 0, D3DPOOL_MANAGED,
                                &pFloorVB );
    // 頂点をセットアップ
    pFloorVB->Lock ( 0, 0, (BYTE**)&pFloorDest, 0 );
    for (DWORD i = 0; i < 4; i++) {
        pFloorDest->x   = (i == 0 || i == 1)?(-FLOOR_SIZE):(+FLOOR_SIZE);// XZ平面上に四角形を作成
        pFloorDest->z   = (i == 0 || i == 2)?(-FLOOR_SIZE):(+FLOOR_SIZE);
        pFloorDest->y   = 0.0f;
        pFloorDest->tu = (i == 0 || i == 1)?0:FLOOR_UV;
        pFloorDest->tv = (i == 0 || i == 2)?0:FLOOR_UV;
        pFloorDest->sx = 1.0f;pFloorDest->sy = 0.0f;pFloorDest->sz = 0.0f;// 基底ベクトルの設定
        pFloorDest->tx = 0.0f;pFloorDest->ty = 0.0f;pFloorDest->tz = 1.0f;
        pFloorDest->xx = 0.0f;pFloorDest->xy = 1.0f;pFloorDest->xz = 0.0f;
        pFloorDest += 1;
    }
    pFloorVB->Unlock ();


    // インデックスをセットアップ
    lpD3DDev->CreateIndexBuffer( 6 * sizeof(WORD),
                               0,
                               D3DFMT_INDEX16, D3DPOOL_MANAGED,
                               &pFloorIB );
    pFloorIB->Lock ( 0, 0, (BYTE**)&pIndex, 0 );
    pIndex[0] = 0;    pIndex[1] = 1;    pIndex[2] = 2;    // 0--2 の四角形
    pIndex[3] = 1;    pIndex[4] = 3;    pIndex[5] = 2;    // 1--3
    pFloorIB->Unlock ();

    CTextureMgr::Load(lpD3DDev, "tile.bmp", &pFloorTex);
    CTextureMgr::Load(lpD3DDev, "normal.bmp", &pNormalTex);
    CVertexShaderMgr::Load(lpD3DDev, "bg.vsh", &hVertexShader, dwDeclFloor);
    CVertexShaderMgr::Load(lpD3DDev, "diffuse.vsh", &hDiffuseShader, dwDeclFloor);
}

普段よりも、バーテックスバッファのロック(他のプログラムから使えないように宣言する)である、pFloorVB->Lock の後が長いです。
ここが、基底ベクトルの定義です。今回の床は平面なので、平らであり全てが同じ基底ベクトルになっています。
より複雑なモデルの場合は、それに応じた基底ベクトルを定義しなくてはなりません。
その方法は(一般的な場合は)、知りません。最近のモデリングツールはは吐き出せるのかな?
普通は、テクスチャーの上方向は上向きなので、この情報を用いて基底ベクトルを作成するのだと思います。
と思ったら、DirectX8.1の追加の D3DXComputeTangent って、何でしょう。こ、これか?

あとは、適当にインデックスバッファを定義して、頂点シェーダーをロードしています。
ここらへんは独自関数ですが、まだ未発達でございまする。

次は、描画部分です。
モデル座標系からスクリーン座標系への変換行列を頂点シェーダーの定数レジスタに追加します。
また、モデル座標系でのライトの方向ベクトルも設定します。
新しいのは、D3DTOP_DOTPRODUCT3 です。
D3DTSS_COLOROP (各テクスチャーステージでのテクスチャーや頂点色の組み合わせ方法)の値の一つです。
D3DTOP_DOTPRODUCT3 は、D3DTSS_COLORARG1 と D3DTSS_COLORARG2 の値をベクトルとみなして、その内積をとります。
D3DTSS_COLORARG1 と D3DTSS_COLORARG2 に法線マップ、ライトベクトルが入っていれば、ピクセル単位での並行光源の計算になります。
あとは、テクスチャーを混ぜて、最終的な出力にします。

void DrawBg(LPDIRECT3DDEVICE8 lpD3DDev, D3DXMATRIX mWorld, D3DXMATRIX mView, D3DXMATRIX mProj)
{
    // 空
    
    // 略・・・・・・・・・・・・
    
    
    // 床
    D3DXMATRIX m = mWorld * mView * mProj;
    D3DXMatrixTranspose( &m ,  &m);
    lpD3DDev->SetVertexShaderConstant(0,&m, 4);
    
    extern D3DXVECTOR4 lpos;
    D3DXVECTOR4 lightDir;
    D3DXMatrixInverse( &m,  NULL, &mWorld);
    D3DXVec4Transform(&lightDir, &lpos, &m);
    D3DXVec4Normalize(&lightDir, &lightDir);
    lpD3DDev->SetVertexShaderConstant(13, &lightDir, 1);
    
    lpD3DDev->SetTexture(0,pNormalTex);
    lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_DOTPRODUCT3);    // これ肝
    lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
    lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG2,  D3DTA_DIFFUSE);
    lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSU,   D3DTADDRESS_WRAP);
    lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSV,   D3DTADDRESS_WRAP);
    lpD3DDev->SetRenderState( D3DRS_WRAP0, D3DWRAP_U | D3DWRAP_V);
    lpD3DDev->SetTexture(1,pFloorTex);
    lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_MODULATE);
    lpD3DDev->SetTextureStageState(1,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
    lpD3DDev->SetTextureStageState(1,D3DTSS_COLORARG2,  D3DTA_CURRENT);
    lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSU,   D3DTADDRESS_WRAP);
    lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSV,   D3DTADDRESS_WRAP);
    lpD3DDev->SetRenderState( D3DRS_WRAP1, D3DWRAP_U | D3DWRAP_V);

    lpD3DDev->SetVertexShader(hVertexShader);
    
    lpD3DDev->SetStreamSource(0, pFloorVB, sizeof(FloorVertex));
    lpD3DDev->SetIndices(pFloorIB,0);
    lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );
}

後片付けは、いつもの用にシェーダープログラムやテクスチャーを開放するだけなので、適当にどうぞ。

■ピクセルシェーダー版

同じ処理をピクセルシェーダーでもやってみました (ピクセルシェーダー版は、上の処理でなかった環境色を含んでいるので豪華だったりしますが)。
ソースファイルは次のものです。

追加、変更されたプログラムは、以下になります。

bg.pshピクセルシェーダープログラム。
bg.cpp地面+天円柱の描画。

では、中身を紹介します。
まずは、ピクセルシェーダープログラム bg.psh です。
固定レジスタに環境色の値を入れました。

ps.1.0

def c0, 0.3f, 0.3f, 0.3f, 0.0f    ; アンビエント色

tex t0                            ; 法線マップの読み込み
tex t1                            ; デカールテクスチャーの読み込み

dp3_sat   r1,     t0_bx2, v0_bx2  ; 法線マップと頂点色(ライトベクトル)との内積
add       r1,     r1,     c0      ; アンビエント色を加えて、ライトの強さを決定する
mul_sat   r0.rgb, r1,     t1      ; ライトの強さとテクスチャをかけて色を計算する
mul r0.a, t1.a,   v0.a            ; テクスチャと頂点色のアルファを乗算

法線マップのベクトル値と、頂点色の oD0 の出力である v0 を _bx2 を使って、-1から1の範囲に戻します。
そして、dp3 命令を使って、内積計算して、各ピクセルの輝度を計算します。
その後に、環境色を加えた後、デカールを掛けて、最終的なピクセルの色を計算します。 α成分は、シンプルにデカールテクスチャーの成分とライトの結果を掛けたものを用いておきます。

後は、bg.cpp です。
初期化と後始末は、ピクセルシェーダープログラムのロードや開放が追加されただけなので、省略します。
描画関数の DrawBg ですが、簡単になっています。
D3DTSS_COLOROP は設定しません(その中身がピクセルシェーダープログラムですから当然ですが)。
後は、ピクセルシェーダーの設定だけです。

// ----------------------------------------------------------------------------
void DrawBg(LPDIRECT3DDEVICE8 lpD3DDev, D3DXMATRIX mWorld, D3DXMATRIX mView, D3DXMATRIX mProj)
{
    // 空
    
    // 略・・・・・・・・・・・・
    
    // 床
    D3DXMATRIX m = mWorld * mView * mProj;
    D3DXMatrixTranspose( &m ,  &m);
    lpD3DDev->SetVertexShaderConstant(0,&m, 4);
    extern D3DXVECTOR4 lpos;
    D3DXVECTOR4 lightDir;
    D3DXMatrixInverse( &m,  NULL, &mWorld);
    D3DXVec4Transform(&lightDir, &lpos, &m);
    D3DXVec4Normalize(&lightDir, &lightDir);
    lpD3DDev->SetVertexShaderConstant(13, &lightDir, 1);
    
    lpD3DDev->SetTexture(0,pNormalTex);
    lpD3DDev->SetTexture(1,pFloorTex);
    lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSU,   D3DTADDRESS_WRAP);
    lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSV,   D3DTADDRESS_WRAP);
    lpD3DDev->SetRenderState( D3DRS_WRAP0, D3DWRAP_U | D3DWRAP_V);
    lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSU,   D3DTADDRESS_WRAP);
    lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSV,   D3DTADDRESS_WRAP);
    lpD3DDev->SetRenderState( D3DRS_WRAP1, D3DWRAP_U | D3DWRAP_V);

    lpD3DDev->SetVertexShader(hVertexShader);
    lpD3DDev->SetPixelShader(hPixelShader);
    lpD3DDev->SetStreamSource(0, pFloorVB, sizeof(FloorVertex));
    lpD3DDev->SetIndices(pFloorIB,0);
    lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );
    lpD3DDev->SetPixelShader(NULL);
}

■最後に

バンプマップに挑戦してみました。
もっと丸くないと目立たないかもしれませんね。




もどる

imagire@gmail.com