今回は、バックミラーを作ってみました(画面では、前にいるクマがバックミラーに移っていますが、そこは適当に補正して下さい)。
私の勤めている会社では、レースゲームも作っていますが、今回はそれらの技術は全く見ていません(おい、勉強のために見とけよ!)。
今回のソースは、次のものです。
内容は次のとおりになっています。
main.h | 基本的な定数など。今回も出番無し。 |
main.cpp | 描画に関係しないシステム的な部分。変更が無いので、出番無し。 |
draw.h | 描画の各関数の定義。特に意味無いので出番無し。 |
draw.cpp | メインの描画部分。主にここが説明される。 |
vs.vsh | オブジェクト(車)表示用の頂点シェーダープログラム。平行光源ライト。 |
あと、モデルとして、nsx.xと、実行ファイルの MyBase.exe 及び、 VC++ でコンパイルするためのプロジェクトファイル MyBase.dsw MyBase.dsp が入っています。
どのような手順でバックミラーを作るのか説明します。
基本的には、通常の描画とバックミラー用の描画を2回行います。
最初に、普通に描画をしてから、表示領域を切り変えて、バックミラーの中を描画します。
その時に、zバッファ(深度バッファ)が残っていると、深度値が前に描いた後ろになった場合に描画されないので、
一度深度バッファをクリアします。
DirectX には、描画領域を変更するための関数 SetViewport が用意されています。
バックミラーを描画する時には、この関数を呼び出し描画領域を変更してから描画します。
また、現在の描画領域を所得する関数 GetViewport も同時に用意されています。
これは、最初の領域を保存しておいて、バックミラーの描画が終了した後、元の状態に戻すため等に使います。
そのための構造体や関数の使い方は、次のとおりになります。
typedef struct _D3DVIEWPORT8 { DWORD X, Y; // 描画範囲の左上 DWORD Width, Height; // 描画範囲の大きさ float MinZ, MaxZ; // 描画後の深度値の最低値を最大値 } D3DVIEWPORT8; D3DVIEWPORT8 viewData = { 0, 0, width, height, 0.0f, 1.0f }; lpD3DDev->SetViewport(&viewData); lpD3DDev->GetViewport(&viewData);
描画領域のデータ用の構造体 D3DVIEWPORT8 に、MinZ, MaxZ があります。
この値を共に 0.0fにすれば、描画する時に最前面に描画してくれるのですが、Zバッファが効かない描画になるので、
今回は通常に深度値を書き込むようにしました (MinZ=0.0f, MaxZ=1.0f)。
さて、バックミラーは鏡です。従って左右反転して表示されなければなりません。
『左右反転して描画』なんてコマンドは用意されないので、自分で何とかしなくてはなりません。
左右反転とは、文字通り右のものが左になることです。
視線方向をZ軸、上向きをY軸とすると、横軸はX軸になります。
従って、視線をZ軸にした世界(ビュー座標系)で、xを-xにする変換をかませれば、左右反転が表現されます。
プログラム的には、ワールド行列とビュー行列の間にX軸を反転させる行列をかまします。
すると、元の世界が左右反転されてから視線に入るので、鏡の世界が表現されたことになります。
ここで注意しなければならないことは、鏡の世界では回転が反対になる(右周りが左回りになる)ということです。
何をいいたいのかというと、表裏判定のカリングが反対になるということです。
両面を描画している時は問題が無いのですが、片面しか描画しない場合は、カリングのモードを逆にしなくてはなりません。
以上で説明は終わりです。次に実際のソースを見ましょう。
今回は、レンダリングを2回行います。
1回目は、普通のレンダリングで、2回目はバックミラーのレンダリングです。
const DWORD WIN_W = 400; const DWORD WIN_H = 80; const DWORD WIN_X = (WIDTH - WIN_W)/2; const DWORD WIN_Y = 30; void Render(LPDIRECT3DDEVICE8 lpD3DDEV) { if(NULL == pMeshVB) return; D3DVIEWPORT8 viewport={WIN_X, WIN_Y, WIN_W, WIN_H, 0.0f, 1.0f}; D3DVIEWPORT8 viewport_bak; lpD3DDEV->GetViewport(&viewport_bak); // 元のビューポート矩形を取っておく for(int n = 0; n < 2; n++){ D3DXMATRIX mWorld, mView, mProj, m; D3DXVECTOR3 eye = D3DXVECTOR3(0.0f, 10.0f, 30.0f); D3DXVECTOR3 lookAt = D3DXVECTOR3(0.0f, 3.0f, 0.0f); D3DXVECTOR3 up = D3DXVECTOR3(0.0f, 1.0f, 0.0f); if(0 == n){ // 通常表示 D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up); D3DXMatrixPerspectiveFovLH(&mProj ,60.0f*PI/180.0f // 視野角 ,(float)WIDTH/(float)HEIGHT // アスペクト比 ,0.01f,100.0f // 最近接距離,最遠方距離 ); }else{ // バックミラー表示 lpD3DDEV->SetViewport(&viewport); lpD3DDEV->Clear(0,NULL,D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,D3DCOLOR_XRGB(0,0x40, 0x40),1.0f,0); lpD3DDEV->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); eye.z = -30.0f; // 窓で表示するときは、反対から見る D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up); D3DXMatrixIdentity(&m); m(0,0)=-1.0f; // x軸に関して鏡像を取る mView = m * mView; D3DXMatrixPerspectiveFovLH(&mProj ,20.0f*PI/180.0f // 視野角 ,(float)WIN_W/(float)WIN_H // アスペクト比 ,0.01f,100.0f // 最近接距離,最遠方距離 ); } D3DXMatrixRotationY( &mWorld, timeGetTime()/1000.0f ); m = mWorld * mView * mProj; D3DXMatrixTranspose( &m , &m); lpD3DDEV->SetVertexShaderConstant(0,&m, 4); D3DXVECTOR4 lightDir(1.0f, 1.0f, 0.5f, 0.0f); D3DXVec4Normalize(&lightDir, &lightDir); D3DXMatrixInverse( &m, NULL, &mWorld); D3DXVec4Transform(&lightDir, &lightDir, &m); lightDir[3] = 0.3f;// 環境光の強さ lpD3DDEV->SetVertexShaderConstant(13, &lightDir, 1); lpD3DDEV->SetVertexShader(hVertexShader); //メッシュの描画 lpD3DDEV->SetStreamSource(0, pMeshVB, sizeof(D3D_CUSTOMVERTEX)); lpD3DDEV->SetIndices(pMeshIndex,0); for(DWORD i=0;i//色をセット D3DXVECTOR4 vl; vl.x = pMeshMaterials[i].Diffuse.r; vl.y = pMeshMaterials[i].Diffuse.g; vl.z = pMeshMaterials[i].Diffuse.b; lpD3DDEV->SetVertexShaderConstant(14, &vl, 1); lpD3DDEV->SetTexture(0,pMeshTextures[i]); lpD3DDEV->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, pSubsetTable[i].VertexStart, pSubsetTable[i].VertexCount, pSubsetTable[i].FaceStart * 3, pSubsetTable[i].FaceCount); } } lpD3DDEV->SetViewport(&viewport_bak); // ビューポート矩形を戻す lpD3DDEV->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); lpD3DDEV->SetTexture(0, NULL); }
あまり重要でない部分は省略しました。
1回目と2回目のレンダリングの違いは、下のソースの黄色の部分と青緑色の部分です。
1回目は、今までと同じレンダリングを行います。
2回目は、ビューポートを切り替えた後に、Zバッファをクリアするための画面消去を行います。
次に、鏡の世界は左右反転していて、右回りが左回りになるので、カリングのためのポリゴンの表裏反転を逆転します。
また、視線の覗く方向(eye.z)を反対にしました。これで、前から後ろを移した画面になります。
さらに、鏡の世界ということで、YZ平面に関して鏡像反転をビュー行列の直前に施します。
これで、カメラに反対向きに写って、表示されます。
最後に、この後の描画のために、ビューポートを元に戻して起きます。
モデル表示です。
普通に表示しますが、今回はicemanさんに触発されて高速化に挑戦してみました(でも、6命令まで届かない・・・)。
今回の工夫は法線ベクトルのw成分を使ったことです(Masaさんのおっしゃられていた方法かな?)。
法線ベクトルのw成分に1.0fを入れておきます(普通は0が多いかな?)。
すると、dp4 命令を使った時にw成分が残るので、この成分を方向に依存しない環境光に用いることができます。
欠点は、平行光源と環境光の色に同じ色を使わなければいけないことです。
【後日談】suzunaさんから、D3DVSDT_FLOAT3 にすると、w成分に自動的に1.0fを入れてくれるので、そちらを使ったほうが手軽だそうです。
typedef struct { float x,y,z; float nx,ny,nz,nw; float tu0,tv0; }D3D_CUSTOMVERTEX; #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_FLOAT4 ), //D3DVSDE_NORMAL, 3 D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ), //D3DVSDE_TEXCOORD0, 7 D3DVSD_END() };
実際の頂点シェーダーは次の7命令になります。
; c0-3 -- world + ビュー + 透視変換行列 ; c13 -- ライトのベクトル (w成分は環境光の強さ) ; c14 -- ライトの色(メッシュの色) ; ; v0 頂点の座標値 ; v3 法線ベクトル (w成分は1.0f) ; v7 テクスチャ座標 vs.1.0 ;座標変換 dp4 oPos.x, v0, c0 dp4 oPos.y, v0, c1 dp4 oPos.z, v0, c2 dp4 oPos.w, v0, c3 ; ((l,n) + l.w)*c14 (平行光源のライティング) dp4 r0.w, v3, c13 mul oD0, r0.w, c14 ; テクスチャーを張る mov oT0, v7
dp4 を使って、法線と光の向きから光の強さを求めた後、ライトやメッシュの色を乗せます。
実際には、下の設定部分で分かるように、メッシュの色をそのまま乗せているので、白色光が当っていることになります。
環境光の強さは0.3fです(平行光源が面に垂直に当った時との比)。
また、今回は、ライトの方向にワールド行列の逆行列を掛けました。
これは、ローカル座標で光源計算を行う事に相当します。
頂点シェーダーで、毎頂点ワールド座標での法線ベクトルを用いる(数千回程度?)よりも、
ライトをローカル座標に変換するほうが(1回のみ)、計算量ははるかに少ないです。
以下の部分が頂点シェーダーの固定レジスタを代入する部分です。
m = mWorld * mView * mProj; D3DXMatrixTranspose( &m , &m); lpD3DDEV->SetVertexShaderConstant(0,&m, 4); D3DXVECTOR4 lightDir(1.0f, 1.0f, 0.5f, 0.0f); D3DXVec4Normalize(&lightDir, &lightDir); D3DXMatrixInverse( &m, NULL, &mWorld); D3DXVec4Transform(&lightDir, &lightDir, &m); lightDir[3] = 0.3f;// 環境光の強さ lpD3DDEV->SetVertexShaderConstant(13, &lightDir, 1); //色をセット D3DXVECTOR4 vl; vl.x = pMeshMaterials[i].Diffuse.r; vl. = pMeshMaterials[i].Diffuse.g; vl.z = pMeshMaterials[i].Diffuse.b; lpD3DDEV->SetVertexShaderConstant(14, &vl, 1);
BBS で、suzunaさんや、Kanoさん、icemanさんにコメントをいただきました。
上記の方法では、内積の値が負になると環境光の成分を消してしまうので、最終的に黒い部分ができてしまいます。
ということで、テクスチャーによるライティングをしました。
ファイルの中身は、
vs.vsh | オブジェクト表示用の頂点シェーダープログラム。平行光源ライト。 |
draw.cpp | テクスチャーライティング用に一部修正。 |
main.h | 基本的な定数など。今回も出番無し。 |
main.cpp | 描画に関係しないシステム的な部分。fps表示を追加。 |
draw.h | 描画の各関数の定義。特に意味無いので出番無し。 |
font.h | fps 表示用。既出。 |
font.cpp | fps 表示用。既出。 |
normal_transform.vsh | おまけ。前回のライトの計算をワールド座標で行う。 |
と、実行ファイルやXfile、プロジェクトファイルおよびライティング用のテクスチャーlight.bmpが入っています。
light.bmp は
に、なっています。
環境光を緑、ディフューズ光を赤にするために、左(u=0)が緑、右(u=1)を赤にしました。
頂点シェーダープログラムは、次になります。
トゥーンと同じように、マルチテクスチャーで光源用のテクスチャーを張ります。
vs.1.0 dp4 oPos.x, v0, c0 ; 座標変換 dp4 oPos.y, v0, c1 dp4 oPos.z, v0, c2 dp4 oPos.w, v0, c3 dp4 oT0.x, v3, c13 ; l dot n (ライティング) mov oT1, v7 ; テクスチャーを張る
やっと、6命令という非常に短いプログラムになりました。
描画部分はツゥーンと同じような設定になります。違いを黄色で塗りました。
void Render(LPDIRECT3DDEVICE8 lpD3DDEV) { if(NULL == pMeshVB) return; D3DVIEWPORT8 viewport={WIN_X, WIN_Y, WIN_W, WIN_H, 0.0f, 0.0f}; D3DVIEWPORT8 viewport_bak; lpD3DDEV->GetViewport(&viewport_bak); // 元のビューポート矩形を取っておく for(int n = 0; n < 2; n++){ D3DXMATRIX mWorld, mView, mProj, m; D3DXMatrixRotationY( &mWorld, timeGetTime()/1000.0f ); D3DXVECTOR3 eye, lookAt, up; eye.x = 0.0f; eye.y = 10.0f; eye.z = 30.0f; lookAt.x = 0.0f; lookAt.y = 3.0f; lookAt.z = 0.0f; up.x = 0.0f; up.y = 1.0f; up.z = 0.0f; if(1 == n){ // 窓で表示するときは、反対から見る eye.z = -30.0f; D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up); D3DXMatrixIdentity(&m); m(0,0)=-1.0f; mView = m * mView; D3DXMatrixPerspectiveFovLH(&mProj, 20.0f*PI/180.0f ,(float)WIN_W/(float)WIN_H ,0.01f ,100.0f); }else{ D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up); D3DXMatrixPerspectiveFovLH(&mProj, 20.0f*PI/180.0f ,(float)WIDTH/(float)HEIGHT ,0.01f ,100.0f); } m = mWorld * mView * mProj; D3DXMatrixTranspose( &m , &m); lpD3DDEV->SetVertexShaderConstant(0,&m, 4); D3DXVECTOR4 lightDir(1.0f, 1.0f, 0.5f, 0.0f); D3DXVec4Normalize(&lightDir, &lightDir); D3DXMatrixInverse( &m, NULL, &mWorld); D3DXVec4Transform(&lightDir, &lightDir, &m); lpD3DDEV->SetVertexShaderConstant(13, &lightDir, 1); lpD3DDEV->SetTextureStageState(0,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); lpD3DDEV->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_SELECTARG1); lpD3DDEV->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE); lpD3DDEV->SetTextureStageState(0,D3DTSS_MAGFILTER, D3DTEXF_LINEAR); lpD3DDEV->SetTextureStageState(0,D3DTSS_MINFILTER, D3DTEXF_LINEAR); 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_MAGFILTER,D3DTEXF_LINEAR); lpD3DDEV->SetTextureStageState(1,D3DTSS_MINFILTER,D3DTEXF_LINEAR); lpD3DDEV->SetVertexShader(hVertexShader); lpD3DDEV->SetTexture(0,pTexture); //メッシュの描画 lpD3DDEV->SetStreamSource(0, pMeshVB, sizeof(D3D_CUSTOMVERTEX)); lpD3DDEV->SetIndices(pMeshIndex,0); for(DWORD i=0;ilpD3DDEV->SetTexture(1,pMeshTextures[i]); lpD3DDEV->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, pSubsetTable[i].VertexStart, pSubsetTable[i].VertexCount, pSubsetTable[i].FaceStart * 3, pSubsetTable[i].FaceCount); } if(n==0){ frame_back_draw(lpD3DDEV); lpD3DDEV->SetViewport(&viewport); // 新しいビューポート矩形を設定する lpD3DDEV->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); }else{ lpD3DDEV->SetViewport(&viewport_bak); // ビューポート矩形を戻す lpD3DDEV->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); } } lpD3DDEV->SetTexture(0, NULL); lpD3DDEV->SetTexture(1, NULL); }
テクスチャーステージ0はテクスチャー(ライティング)のみを使用して、テクスチャーステージ1はテクスチャー(メッシュ)とテクスチャーステージ0の結果を合成する設定にしました。
メッシュのテクスチャーを後回しにしたので、テクスチャーがないモデルでも正常に動作します。
今回は、ライトの w 成分を 0.0f にしました。
内積の値が負のときに同じ(環境光の)色になるように、このようにしましたが、ライトの成分を
l = (0.5*lx, 0.5*ly, 0.5*lz, 0.5) ((lx, ly, lz)は正規化された方向ベクトル)
にすると、法線とライトのベクトルの内積が -1.0f~1.0f のときに、dp4 の結果は 0.0f~1.0f になるので、
内積が負の場合にもライトに表情をつけることができます。
今回使わなかった理由は、環境光+平行光源のモデルを用いたので、内積が負のときに同じ値となり、テクスチャーの半分の色が一色になってもったいないからです。
それよりも、同じテクスチャーサイズでもグラデーションに高い解像度を与えられる w = 0.0f を採用しました。
w = 0.5f 以外にするのは、テクスチャー位置の全体にずらすことに対応するので、動きをつけたり、テクスチャーを効率よく使用するために使えそうです。
実際のパフォーマンスですが、テクスチャーライティングの場合は 322fpsで、もともとのプログラムの場合は 326fps 出ていました。
1%ほどと、小さいですが確実にテクスチャーライティングの方がパフォーマンスが悪かったでした。
とはいえないようです。
今回は、バックミラーを作ってみました。
また、計算量がより多い場合として、(normal_transform.vsh を使って) ワールド座標で光源計算した場合には 326fps と、ローカル座標で光源計算したときと同じ結果が出ました。
実験したのが Celeron 450MHz Dual の非力なマシンなので、そちらがボトルネックになっていることが考えられますが、
命令数が少ない=実行速度が速い
■最後に
適当に作ったので、きれいにまとめられていません。
皆さんは、ビュー行列や描画範囲をクラス化したりして、
いい感じのオブジェクト等にまとめてくださいね。