さて、前回は、バーテックスシェーダーの使い方を覚えました。 前回のポリゴンは、テクスチャーを張っただけのポリゴンの表示でした。 今回は、基本的なライティングの例題を取り扱います。
まずは、ライティングはせずに、頂点色でポリゴンの色をつけます。
以下のファイルをロードしてください。
まずは、ライティングはせずに、頂点色でポリゴンの色をつけます。
以下のファイルをロードしてください。
今回は、vs.vsh というファイルを追加しました。
これは、バーテックスシェーダーのプログラムです。
前回は、D3DXAssembleShaderFromFile を使って、ファイルから読み込むようにしました。
さて、いつもの様に draw.cpp です。今回の頂点は、
struct CUSTOMVERTEX { FLOAT x, y, z; // 位置 DWORD color; // 色 FLOAT tu, tv; // テクスチャーの画像の位置 }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1) DWORD dwDecl[] = { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), //D3DVSDE_POSITION, 0 D3DVSD_REG(D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), //D3DVSDE_DIFFUSE, 5 D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ), //D3DVSDE_TEXCOORD0, 7 D3DVSD_END() };
です。前回と比較して、DWORD color が付きました。 バーテックスシェーダープログラムに伝えるフォーマット dwDecl にも、D3DVSDE_DIFFUSE を追加しました。 色のような、DWORD 型のデータの場合には、D3DVSDT_D3DCOLOR を使います。
初期化は、D3DXAssembleShaderFromFile を使うことが前回との違いです。
また、頂点の変更に対応して、頂点色を指定しました。左上、右上、左下、右下の順に、白、赤、緑、青色になります。
HRESULT InitRender(LPDIRECT3DDEVICE8 lpD3DDEV) { CUSTOMVERTEX vertices[] = { // x, y, z, 赤 緑 青 α tu tv {-16.0f, 12.0f, 0.0f, D3DCOLOR_RGBA(0xff, 0xff, 0xff, 0xff), 0.0f,0.0f,}, { 16.0f, 12.0f, 0.0f, D3DCOLOR_RGBA(0xff, 0x00, 0x00, 0xff), 1.0f,0.0f,}, {-16.0f,-12.0f, 0.0f, D3DCOLOR_RGBA(0x00, 0xff, 0x00, 0xff), 0.0f,1.0f,}, { 16.0f,-12.0f, 0.0f, D3DCOLOR_RGBA(0x00, 0x00, 0xff, 0xff), 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); // バーテックスシェーダーを作成する ID3DXBuffer* pshader; hr = D3DXAssembleShaderFromFile("vs.vsh", 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; }
描画部分は一箇所だけ異なります。
座標変換するための固定レジスタを c4 ~ c7 から、c0 ~ c3 に、移しました。
まぁ、バーテックスシェーダーのプログラムを、そう変えただけで、深い意味はありません。
lpD3DDEV->SetVertexShaderConstant( 0 , &trans , 4 ); // Vertex shader の c0 ~ c3 レジスタの設定
肝心かなめのバーテックスシェーダープログラムですが、次のようになります。
vs.1.0 ; 単純な頂点シェーダ ; 定数 ; reg c0-3 = WorldViewProj 行列 ; ストリーム 0 ; reg v0 = 位置 ( 3 x 1 ベクトル) ; reg v5 = ディフューズ色 ; reg v7 = テクスチャ座標 (2 x 1 ベクトル) dp4 oPos.x , v0 , c0 ; 射影後の x 位置を出力 dp4 oPos.y , v0 , c1 ; 射影後の y 位置を出力 dp4 oPos.z , v0 , c2 ; 射影後の z 位置を出力 dp4 oPos.w , v0 , c3 ; 射影後の w 位置を出力 mov oD0 , v5 mov oT0.xy , v7 ; テクスチャ座標をコピー
前回との違いは、oD0 への出力です。これが頂点色になります。
ここでは、v5 レジスタに入った CUSTOMVERTEX の color 成分を、そのまま、コピーします。
これで、頂点の色が反映されたポリゴンの表示ができます。
次は、平行光源です。平行光源は、太陽のような非常に遠方にあり、ほとんど光が広がらないライトをシミュレートしたものです。
ソースは、次です。
平行光源の光の強さは、光源の方向のみに依存するので、各頂点の明るさは、法線 n と、光源の方向 l の内積(の関数)になります。
したがって、頂点に法線情報を追加します。
struct CUSTOMVERTEX { FLOAT x, y, z; // 位置 FLOAT nx, ny, nz; // 法線 FLOAT tu, tv; // テクスチャーの画像の位置 }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1) DWORD dwDecl[] = { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), //D3DVSDE_POSITION, 0 D3DVSD_REG(D3DVSDE_NORMAL, D3DVSDT_FLOAT3 ), //D3DVSDE_NORMAL, 3 D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ), //D3DVSDE_TEXCOORD0, 7 D3DVSD_END() };
初期化は、先ほどの例と、頂点のデータが違うだけです。
今回は、光の表現をディフォルメするために、法線を面の外側に少し広げました。
広げないときには、各頂点に関する光の強さが変化しないので、面に当たる光の強さが一様になります。
CUSTOMVERTEX vertices[] = { // x, y, z, nx ny nz tu tv {-16.0f, 12.0f, 0.0f,-1.0f/2.0f, 1.0f/2.0f,-1.7320508f/2.0f, 0.0f,0.0f,}, { 16.0f, 12.0f, 0.0f, 1.0f/2.0f, 1.0f/2.0f,-1.7320508f/2.0f, 1.0f,0.0f,}, {-16.0f,-12.0f, 0.0f,-1.0f/2.0f,-1.0f/2.0f,-1.7320508f/2.0f, 0.0f,1.0f,}, { 16.0f,-12.0f, 0.0f, 1.0f/2.0f,-1.0f/2.0f,-1.7320508f/2.0f, 1.0f,1.0f,}, };
描画部分は、ちょいと違います。ライトの方向のベクトルを、固定レジスタの c4 に入れています。
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 // 最遠方距離 ); // Vertex shader の c0 ~ c3 レジスタの設定 D3DXMATRIX m = mWorld * mView * mProj; D3DXMATRIX trans; D3DXMatrixTranspose( &trans , &m); lpD3DDEV->SetVertexShaderConstant( 0 , &trans , 4 ); // ライトのc4 レジスタの設定 D3DXVECTOR4 vLight(0, 0.707f, 0.707f, 0); D3DXMatrixInverse(&mWorld, NULL, &mWorld); D3DXVec4Transform( &vLight, &vLight, &mWorld); lpD3DDEV->SetVertexShaderConstant( 4 , &vLight , 1 ); lpD3DDEV->SetVertexShader( hVertexShader ); lpD3DDEV->SetTexture( 0, pTexture); lpD3DDEV->SetStreamSource( 0, pVB, sizeof(CUSTOMVERTEX) ); lpD3DDEV->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2); }
この時に、ワールド行列の逆行列を掛けました。
ポリゴンが回転すると、法線も回転する必要があるので、その計算をしています。
ただ、各頂点の法線を毎回ワールド座標に変換するのは、計算量が多いので、
ライトが固定して、ポリゴンが回る=ポリゴンを固定して、ライトを逆方向に回す
ということを利用して、ライトにワールド行列の逆行列を掛けることで、ワールド座標への変換を済ませています。
もし、各頂点の法線を全て変換していたら、行列変換が数百~数千頂点の計算が必要です。
それを行列演算一回ですましたことになります。
; v0 : 頂点座標 ; v3 : 法線 ; v7 : テクスチャー座標(UV) ; ; c0-c3 : World-View-Proj 行列 ; c4 : ライトベクトル ; vs.1.0 ; バージョン情報 dp4 oPos.x , v0 , c0 ; x成分の透視変換 dp4 oPos.y , v0 , c1 ; y成分の透視変換 dp4 oPos.z , v0 , c2 ; z成分の透視変換 dp4 oPos.w , v0 , c3 ; w成分の透視変換 dp3 oD0 , v3 , -c4 ; 法線とライトの方向ベクトルの内積から、色を計算 mov oT0.xy , v7 ; copy texcoords
さて、シェーダープログラムです。
今回は、oD0 の出力が v3 と -c4 の内積になっています。
これは、負の値を取っているのは、法線とライトの向きは向かい合っているとき(内積の値が-1の時)が、
一番光が強く当たっている時なので、そのときに色が1になるように調整しました。
この計算では、内積の値が xyzw の全ての成分に入ります。
したがって、光の色は白になります。
さて、以上の二つの合成です。
頂点に法線と、頂点色の両方が入ります。
struct CUSTOMVERTEX { FLOAT x, y, z; // 位置 FLOAT nx, ny, nz; // 法線 DWORD color; // 頂点色 FLOAT tu, tv; // テクスチャーの画像の位置 }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1) DWORD dwDecl[] = { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), //D3DVSDE_POSITION, 0 D3DVSD_REG(D3DVSDE_NORMAL, D3DVSDT_FLOAT3 ), //D3DVSDE_NORMAL, 3 D3DVSD_REG(D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), //D3DVSDE_DIFFUSE, 5 D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ), //D3DVSDE_TEXCOORD0, 7 D3DVSD_END() };
初期化も、データが違うだけで、平行光源のものそのままです。 描画に関しては、全く同じです。
CUSTOMVERTEX vertices[] = { // x, y, z, nx ny nz 赤 緑 青 α tu tv {-16.0f, 12.0f, 0.0f,-1.0f/2.0f, 1.0f/2.0f,-1.7320508f/2.0f, D3DCOLOR_RGBA(0xff, 0xff, 0xff, 0xff), 0.0f,0.0f,}, { 16.0f, 12.0f, 0.0f, 1.0f/2.0f, 1.0f/2.0f,-1.7320508f/2.0f, D3DCOLOR_RGBA(0xff, 0x00, 0x00, 0xff), 1.0f,0.0f,}, {-16.0f,-12.0f, 0.0f,-1.0f/2.0f,-1.0f/2.0f,-1.7320508f/2.0f, D3DCOLOR_RGBA(0x00, 0xff, 0x00, 0xff), 0.0f,1.0f,}, { 16.0f,-12.0f, 0.0f, 1.0f/2.0f,-1.0f/2.0f,-1.7320508f/2.0f, D3DCOLOR_RGBA(0x00, 0x00, 0xff, 0xff), 1.0f,1.0f,}, };
そして、シェーダープログラムは、次のようになります。
; v0 : 頂点座標 ; v3 : 法線 ; v5 : 頂点色 ; v7 : テクスチャー座標(UV) ; ; c0-c3 : World-View-Proj 行列 ; c4 : ライトベクトル ; vs.1.0 ; バージョン情報 ; 座標変換 dp4 oPos.x , v0 , c0 ; x成分の透視変換 dp4 oPos.y , v0 , c1 ; y成分の透視変換 dp4 oPos.z , v0 , c2 ; z成分の透視変換 dp4 oPos.w , v0 , c3 ; w成分の透視変換 ; ライトの計算 dp3 r0.x , v3 , -c4 ; 法線とライトの方向ベクトルの内積から、色を計算 mul oD0 , v5 , r0.x ; さらに頂点カラーも考える ; テクスチャーを張る mov oT0.xy , v7 ; ビットマップのコピー
もったいないので、最初の内積計算を、x 成分だけ出力しました。 その値 r0.x をもちいて、mul 演算で、頂点色に強弱をつけて、光源の影響を計算します。
一般に、頂点色はデザイナーさんが付けて、物体の質感を高めます。
光源計算は、ライトの影響によって物体に存在感を与えます。
もちろん、どちらも使えると便利なのですが、一般に光源計算は遅いので、
頂点色だけで物体を表現することも多いです。
したがって、この計算ができれば、光源計算はできたと思ってもいいと思います。
これで、普通の光源計算はできました。
この後、バーテックスシェーダー特有の計算に移ろうと思います。