Vertex Shader 入門


~自分でT&L~




■今回のファイルは

Pixel Shader を既に取り扱いましたが、より、幅を広げるために、 ここで、バーテックスシェーダーをやっておこうと思います。
バーテックスシェーダーは、自前で座標計算と、ライティングをするための仕組みです。
Pixel Shader では、形の変形は出来ませんが、バーテックスシェーダーはポリゴンそのものをゆがませることができます。
まぁ、最終的には Displacement Mapping すなわち Per Vertex( & Pixel) Shader に統合されるでしょうが、 数世代先の話をしてもしょうがないので、今できることをします。

次のソースを、ダウンロードしてください。

今回は、テクスチャーを張った3次元ポリゴンを表示します。
既に取り扱った例題ですが、その分バーテックスシェーダーの使い方が分かると思います。

■今回のオブジェクト

例によって、draw.cpp を見ていきます。
今回追加したオブジェクトは、hVertexShader です。 いわゆるハンドルと言う奴で、バーテックスシェーダーを識別するのに Windows の内部で使われます。

LPDIRECT3DVERTEXBUFFER8     pVB            = NULL; // 頂点バッファ
LPDIRECT3DTEXTURE8          pTexture       = NULL; // テクスチャー
DWORD                    hVertexShader  = ~0; // シェーダープログラムの情報

hVertexShader は ~0 (=0xffffffff) で初期化します。 他のサンプルなどを見ると、どうやら、これが無効なハンドルの値らしいです。 このまま使いましょう。

■初期化

今回は、頂点バッファや、テクスチャーを設定した後に、バーテックスシェーダーの初期化が入ります。

HRESULT InitRender(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    CUSTOMVERTEX vertices[] =
    {
        // x,     y,    z,     tu   tv
        {-16.0f, 12.0f, 0.0f, 0.0f,0.0f,},
        { 16.0f, 12.0f, 0.0f, 1.0f,0.0f,},
        {-16.0f,-12.0f, 0.0f, 0.0f,1.0f,},
        { 16.0f,-12.0f, 0.0f, 1.0f,1.0f,},
    };
    HRESULT hr;
    
    hr = lpD3DDEV->CreateVertexBuffer( 4*sizeof(CUSTOMVERTEX),0,
                    D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &pVB);
    if(FAILED(hr)) return E_FAIL;

    VOID* pVertices;
    hr = pVB->Lock( 0, sizeof(vertices), (BYTE**)&pVertices, 0);
    if(FAILED(hr)) return E_FAIL;
    memcpy( pVertices, vertices, sizeof(vertices) );
    pVB->Unlock();
    
    D3DXCreateTextureFromFileEx(lpD3DDEV, "sakura.bmp",0,0,0,0,D3DFMT_A8R8G8B8,
                                D3DPOOL_MANAGED, D3DX_FILTER_LINEAR, D3DX_FILTER_LINEAR,
                                0, NULL, NULL, &pTexture);

    // バーテックスシェーダーを作成する
    DWORD dwDecl[] =
    {
        D3DVSD_STREAM(0),
        D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ),            //D3DVSDE_POSITION,  0  
        D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ),            //D3DVSDE_TEXCOORD0, 7  
        D3DVSD_END()
    };
    ID3DXBuffer*    pshader;
    hr = D3DXAssembleShader( VertexShader , sizeof(VertexShader) - 1, 0 , NULL , &pshader , NULL );
    if ( FAILED(hr) ) return hr;
    hr = lpD3DDEV->CreateVertexShader( dwDecl, (DWORD*)pshader->GetBufferPointer(), &hVertexShader, 0 );
    RELEASE(pshader);
    if ( FAILED(hr) ) return hr;

    lpD3DDEV->SetRenderState( D3DRS_ZENABLE,  TRUE );
    lpD3DDEV->SetRenderState( D3DRS_LIGHTING, FALSE );        // 頂点の色を使うので、ライトは使わない
    lpD3DDEV->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );    // 両面表示
    
    return S_OK;
}

D3DXAssembleShader から後が、バーテックスシェーダーの部分です。
D3DXAssembleShader でプログラムを読み込みます。読んだ情報が pshader に入ります。
今回は、ソース中の文字列である、const char VertexShader[] を使いました。 プログラムが、何をやるかは、次に説明します。 ここでは、D3DXAssembleShader がバーテックスシェーダーのプログラムを読み込むところだと覚えてください。

読み込んだら、CreateVertexShader でプログラムからシェーダーを生成します。 できたシェーダーは、CreateVertexShader で設定すれば、使えるようになります。 ウィンドウズから割り当てられたメモリが、ハンドルとして hVertexShader に渡されます。 後で設定するときに、このハンドルを使います。

CreateVertexShader で、dwDecl という、DWORD 配列を使っています。 これは、ポリゴンのフォーマットをバーテックスシェーダーのプログラムに教えます。
バーテックスシェーダーは、今回使っている頂点フォーマット

struct CUSTOMVERTEX
{
    FLOAT x, y, z;  // 位置
    FLOAT tu, tv;	// テクスチャーの画像の位置
};

を、構造体としての CUSTOMVERTEX や、

#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_TEX1)

の形で、理解することが出来ません。はっきりいって、おバカさんです。
われわれが調教しなくてはいけなせん。 それが、dwDecl です。

DWORD dwDecl[] = {
    D3DVSD_STREAM(0),
    D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ),            //D3DVSDE_POSITION,  0  
    D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ),            //D3DVSDE_TEXCOORD0, 7  
    D3DVSD_END()
};

dwDecl は、D3DVSD_STREAM(0) で始まり、D3DVSD_END() で終わります。 まぁ、いつも使う時は、コピー&ペーストしましょう。
その間が、頂点フォーマットの指定です。 CUSTOMVERTEX の中身の順番通りに指定します。
最初は、座標の x, y, z です。float 型 3 つを、D3DVSDT_FLOAT3 として指定します。 こいつらは、座標なので、D3DVSDE_POSITION で指定します。
あとはテクスチャー座標です。tu, tv のfloat 型 2 つなので、D3DVSDT_FLOAT2 で指定します。 1つ目の(1つしかないですが、)テクスチャー座標なので、D3DVSDE_TEXCOORD0 に設定します。
以上です。
ここで、D3DVSDE_POSITION及び、D3DVSDE_TEXCOORD0 を使いましたが、 これは、DirectX が勝手に定義している定義名です。直接数字で、D3DVSD_REG(3, D3DVSDT_FLOAT3 ) と しても構いません。 その場合は、バーテックスシェーダープログラムでは、v3 に座標が入るので、v3 をポリゴンの頂点座標として扱えばバッチリです。 まぁ、読みやすさの点から言っても、定義名を使っていくほうがいい事が多いとは思います。

と言うわけで、VertexShader[] や、dwDecl を使って、バーテックスシェーダーを作成したので、この後の描画で使いましょう。

■シェーダープログラム (VertexShader[])

ここで、シェーダープログラムを見ましょう。
プログラムコードは、次のようになっています。 文字列がそのままプログラムになっています。 " や \n がないと考えて、プログラムを考えてください。

const char VertexShader[] =
"vs.1.0                             // バージョン情報            \n"\
"dp4     oPos.x , v0   , c4         // x成分の透視変換           \n"\
"dp4     oPos.y , v0   , c5         // y成分の透視変換           \n"\
"dp4     oPos.z , v0   , c6         // z成分の透視変換           \n"\
"dp4     oPos.w , v0   , c7         // w成分の透視変換           \n"\
"mov     oT0.xy , v7                // テクスチャーのUV値をコピー\n";

1 行目は バーテックスシェーダーのバージョン設定です。
2 行目から、3行目で、透視変換をします。 v0 にポリゴンの座標が入るように D3DVSD_REG で、D3DVSDE_POSITION(=0) に設定しました。 oPos が透視変換した後に出力する場所です。あとで、c4~c7 に変換行列を入れます。 行列計算として記すと、次のようになります。

                                                     ┌c4.x c5.x c6.x c7.x ┐
[oPos.x oPos.y oPos.z oPos.w] = [v0.x v0.y v0.z v0.w]|c4.y c5.y c6.y c7.y │
                                                     │c4.z c5.z c6.z c7.z │
                                                     └c4.w c5.w c6.w c7.w ┘

あとは、テクスチャーで、mov 命令で、v7 の xy 成分だけを、そのままコピーします。 v7 は初期化の時点で、テクスチャーのuv値が入るように、(D3DVSDE_TEXCOORD0(=7))として設定しました。
以上が、バーテックスシェーダーの計算です。後々、くわしくやっていきましょう。

■描画

いよいよ、表示の部分です。
今回は、3次元ポリゴンを表示した時のように、mWorld, mView, mProj を求めますが、 その後、SetTransform はしません。
全てを掛けて、1つの行列にした後、SetVertexShaderConstant で、バーテックスシェーダーの定数レジスタにほおりこみます。 実は、通常のポリゴンの計算も、内部で、mWorld, mView, mProj を同じように掛けて、一つにまとめて計算します。
なぜ、SetTransform で3つを別々にするかと言えば、人間にわかりやすいからです (他にも、カメラを複数持つのに便利だとか、スポットライトの計算に便利だとかありますが、本質的に1つの行列で十分です)。

そんなこんなで、SetVertexShaderConstant で、変換に使う行列を設定した後、 SetVertexShader で、バーテックスシェーダープログラムを透視変換に使うように設定します。
あとは、普通に表示すれば、バーテックスシェーダーを使った結果が得られます。

void Render(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    D3DXMATRIX mWorld, mRotX, mRotY, mTrans;
    D3DXMatrixTranslation(&mTrans, 0,0,0);
    float time = (float)timeGetTime();
    D3DXMatrixRotationY(&mRotY, time/600.0f);
    D3DXMatrixRotationX(&mRotX, 0.0f);
    mWorld = mRotX * mRotY * mTrans;
    
    // ワールド、ビュー座標系の変換
    D3DXMATRIX mView, mProj;
    D3DXMatrixLookAtLH(&mView
                    ,&D3DXVECTOR3(0,0.0f,-50.0f)  // カメラ位置
                    ,&D3DXVECTOR3(0,0,0)          // カメラの注目点
                    ,&D3DXVECTOR3(0,1,0)          // 上の向き
                    );
    
    // ビュー、スクリーン座標系の変換
    D3DXMatrixPerspectiveFovLH(&mProj
        ,60.0f*D3DX_PI/180.0f                   // 視野角
        ,(float)WIDTH/(float)HEIGHT             // アスペクト比
        ,0.01f                                  // 最近接距離
        ,100.0f                                 // 最遠方距離
        );
    
    D3DXMATRIX  m = mWorld * mView * mProj;
    D3DXMATRIX  trans;
    D3DXMatrixTranspose( &trans ,  &m);
    lpD3DDEV->SetVertexShaderConstant( 4 , &trans , 4 );    // バーテックスシェーダーの c4 ~ c7 レジスタの設定
    lpD3DDEV->SetVertexShader( hVertexShader );
    
    lpD3DDEV->SetTexture( 0, pTexture);
    lpD3DDEV->SetTextureStageState( 0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1 );
    lpD3DDEV->SetStreamSource( 0, pVB, sizeof(CUSTOMVERTEX) );
    lpD3DDEV->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2);

}

■後片付け

後片付けに、hVertexShader で使ったメモリの開放を追加します。

void CleanRender(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    if ( hVertexShader != ~0 ){
        lpD3DDEV->DeleteVertexShader( hVertexShader );
        hVertexShader = ~0;
    }
    RELEASE(pTexture);
    RELEASE(pVB);
}

あとは、いつもと変わりありません。

■今後

さぁ、バーテックスシェーダーが使えるようになりました。 今後、(Pixel Shader との複合技を含めて)バリエーションを増やしていきましょう 。





もどる

imagire@gmail.com