DirectX9 で導入された機能に両面ステンシル機能があります。
両面ステンシル機能は、ステンシルテストで合格したときのステンシルへの書き込みを、
表面と裏面をポリゴンで違う動作にすることができます
(これだけに限らず、もっと多彩な変化が可能です。
表と裏でステンシルのレンダリングステートに違う設定ができるのが両面ステンシルです)。
これを使えば、シャドウボリュームによる影の描画パスの回数を1回減らすことができます。
といっても、 id software の DOOMⅢ のために追加された気がしてしょうがありませんが…
今回は、前にやったボリューム影を両面ステンシルで行っただけでなく、元のモデルと同じ形の影を落とす方法に改良しました。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
| hlsl.fx | シェーダの入ったエフェクトファイル |
| main.h | アプリケーションのヘッダ |
| main.cpp | アプリケーションのソース |
| CShadowVolume.h | アプリケーションのソース |
| CShadowVolume.cpp | アプリケーションのソース |
あと、実行ファイル、表示用モデル及び、プロジェクトファイルが入っています。
今回のモデルは、コーネル箱の外枠モデルと、大きさ1の箱を用意しました。
今回の方法は、シャドウボリュームを作る専用のメッシュを作成します。
基本的にDirectXで扱うポリゴンは3角形ポリゴンです。
その3角形を切り開いて、稜線(頂点と頂点を結ぶ辺)に4角形ポリゴンを挟み込みます。
といっても、実際には、3角形ポリゴンを2枚埋め込むことになります。
シャドウボリュームのメッシュの法線には、面の法線を使います
(この時点で共有された頂点はなくなるので、全ての頂点がどこかの3角形に所属しています)。
頂点の位置も、「切り開いた」といっても、位置はずらさないで、もとのモデルと同じ頂点座標を使います。
したがって、シャドウボリュームのメッシュは、元のメッシュと見た目上同じに見えます。
稜線に埋め込んだ四角形ポリゴンは、普段は大きさが0の縮退をして姿を現しません。
さて、このポリゴンの量ですが、ちょっと多いです。
元のメッシュのポリゴン1つにつき3つの稜線があります。
ただし、稜線は2つのポリゴンにまたがっているので、1つのポリゴンに付き3/2の四角形ポリゴンが追加されることになります。
4角形ポリゴンを3角形に割るとちょうど、1つのポリゴンに付き3つの3角形ポリゴンが追加されることになります。
つまり、稜線に埋め込まれるのは、元のポリゴン数の3倍のポリゴンです。
さらに、元のメッシュも必要なので、シャドウボリュームのメッシュは元のメッシュの4倍のポリゴン数になります。
多いか少ないかは微妙なとこですね。
さて、プログラムですが、ちょっとややこしいので少しづつ見ていきましょう。
最初に、元のメッシュのFVFを変換して、いらない情報を削りとります。
次に、メッシュの面の数から、シャドウボリュームのメッシュの面数や頂点数を算出して、メモリを確保します。
CShadowVolume.cpp
0032: // ---------------------------------------------------------------------------
0033: // 生成
0034: // ---------------------------------------------------------------------------
0035: HRESULT CShadowVolume::Create( LPDIRECT3DDEVICE9 pd3dDevice, LPD3DXMESH pSrcMesh )
0036: {
0037: HRESULT ret = S_OK;
0038: struct MESHVERTEX { D3DXVECTOR3 p, n; };
0039: SHADOW_VOLUME_VERTEX* pVertices;
0040: WORD* pIndices;
0041: DWORD i,j,k,l, face;
0042: LPD3DXMESH pMesh;
0043:
0044: if( FAILED( pSrcMesh->CloneMeshFVF( D3DXMESH_SYSTEMMEM,
0045: D3DFVF_XYZ | D3DFVF_NORMAL,
0046: pd3dDevice, &pMesh ) ) )
0047: return E_FAIL;
0048: DWORD dwNumFaces = pMesh->GetNumFaces();
0049:
0050: // 出力用のメモリの確保
0051: m_dwNumFaces = 4*dwNumFaces;
0052: m_pVertices = new SHADOW_VOLUME_VERTEX[3*m_dwNumFaces];
そしたら、元のメッシュをロックして、情報を所得します。
所得する情報は、面ごとの頂点の位置データです。
位置データから面の法線ベクトルを計算します。
また、シャドウボリュームのメッシュの元のメッシュと同じ部分を作成しておきます。
CShadowVolume.cpp
0054: // バッファを専有
0055: pMesh->LockVertexBuffer( 0L, (LPVOID*)&pVertices );
0056: pMesh->LockIndexBuffer ( 0L, (LPVOID*)&pIndices );
0057:
0058: // 法線保存用
0059: D3DXVECTOR3 *vNormal = new D3DXVECTOR3[dwNumFaces];
0060: if(NULL==vNormal){
0061: m_dwNumFaces = 0;
0062: ret = E_OUTOFMEMORY;
0063: goto end;
0064: }
0065: // 通常データ製作
0066: for( i=0; i<dwNumFaces; i++ )
0067: {
0068: D3DXVECTOR3 v0 = pVertices[pIndices[3*i+0]].p;
0069: D3DXVECTOR3 v1 = pVertices[pIndices[3*i+1]].p;
0070: D3DXVECTOR3 v2 = pVertices[pIndices[3*i+2]].p;
0071:
0072: D3DXVECTOR3 vCross1(v1-v0);
0073: D3DXVECTOR3 vCross2(v2-v1);
0074: D3DXVec3Cross( &vNormal[i], &vCross1, &vCross2 );
0075:
0076: m_pVertices[3*i+0].p = v0;
0077: m_pVertices[3*i+1].p = v1;
0078: m_pVertices[3*i+2].p = v2;
0079: m_pVertices[3*i+0].n = vNormal[i];
0080: m_pVertices[3*i+1].n = vNormal[i];
0081: m_pVertices[3*i+2].n = vNormal[i];
0082: }
後は、稜線に埋め込むデータです。
元のメッシュの各面を総当りして共通する辺があるか検索します。
共通する辺は同じ位置にある頂点が2つあれば辺が重なることを利用して計算します。
今回の方法では、厚さ0の張り合わさったメッシュは適応できないので、気をつけてください。
稜線が見つかったら、そのインデックスから頂点の位置や法線を求めます。
ここで、ポリゴンの表を決めるためにポリゴンの周り順が左回りになるように調整します。
CShadowVolume.cpp
0084: // 稜線に挟み込むデータ製作
0085: face = dwNumFaces;
0086: for( i=0 ; i<dwNumFaces; i++ ){
0087: for( j=i+1; j<dwNumFaces; j++ ){
0088: DWORD id[2][2];
0089: DWORD cnt=0;
0090: for(k=0;k<3;k++){
0091: for(l=0;l<3;l++){
0092: D3DXVECTOR3 dv;
0093: D3DXVec3Subtract( &dv, &pVertices[pIndices[3*i+k]].p,
0094: &pVertices[pIndices[3*j+l]].p);
0095: if( D3DXVec3LengthSq( &dv ) < 0.001f ){
0096: // 頂点の位置が同じデータを検索
0097: id[cnt][0] = 3*i+k;
0098: id[cnt][1] = 3*j+l;
0099: cnt++;
0100: }
0101: }
0102: }
0103: if(2==cnt){
0104: // 共有稜線があった
0105: if(id[1][0]-id[0][0]!=1){
0106: // ポリゴンの表向きを調整するための順番ずらし
0107: DWORD tmp = id[0][0];
0108: id[0][0] = id[1][0];
0109: id[1][0] = tmp;
0110: tmp = id[0][1];
0111: id[0][1] = id[1][1];
0112: id[1][1] = tmp;
0113: }
0114: // 稜線にポリゴンを埋め込む
0115: m_pVertices[3*face+0].p = pVertices[pIndices[id[1][0]]].p;
0116: m_pVertices[3*face+2].p = pVertices[pIndices[id[0][1]]].p;
0117: m_pVertices[3*face+1].p = pVertices[pIndices[id[0][0]]].p;
0118: m_pVertices[3*face+0].n = vNormal[i];
0119: m_pVertices[3*face+2].n = vNormal[j];
0120: m_pVertices[3*face+1].n = vNormal[i];
0121: face++;
0122: m_pVertices[3*face+0].p = pVertices[pIndices[id[1][0]]].p;
0123: m_pVertices[3*face+2].p = pVertices[pIndices[id[1][1]]].p;
0124: m_pVertices[3*face+1].p = pVertices[pIndices[id[0][1]]].p;
0125: m_pVertices[3*face+0].n = vNormal[i];
0126: m_pVertices[3*face+2].n = vNormal[j];
0127: m_pVertices[3*face+1].n = vNormal[j];
0128: face++;
0129: }
0130: }
0131: }
0132: assert(face == m_dwNumFaces);
後は、ロックを解いたり、メモリを開放します。
CShadowVolume.cpp 0135: delete[] vNormal; 0136: end: 0137: // バッファの専有を解除 0138: pMesh->UnlockVertexBuffer(); 0139: pMesh->UnlockIndexBuffer(); 0140: 0141: pMesh->Release(); 0142: 0143: return ret; 0144: }
さて、シェーダです。
シャドウボリュームをレンダリングするときにはフレームバッファにピクセルを埋め込まなくてもよいので、
ピクセルシェーダは必要ありません(固定機能が使われます)。
さらに、ラスタライザに渡すデータも何も要らないので、位置情報だけを頂点シェーダの出力とします。
頂点シェーダのプログラム自体はいたって普通で、法線とライトの方向を比較して、表を向いていたらそのまま、
裏を向いていたら、定数だけライトから反対に引き伸ばします。
影をプログラムする時の常で、シャドウボリュームのメッシュと元のメッシュが重なるところでちらつくので、
少し内側にずらしています。
ワールド行列に少し縮小スケールをかけておいてもいいかもしれません(元のメッシュを大きくする手もありますが)。
今回の計算はローカル座標系で行っているので、ライトの位置をローカル座標に変換しておく必要があります。
hlsl.fx
0007: // -------------------------------------------------------------
0008: // グローバル変数
0009: // -------------------------------------------------------------
0010: float4x4 mWVP; // ローカルから射影空間への座標変換
0011: float4 vLightPos; // ライトの位置
0012:
0013: // -------------------------------------------------------------
0014: // 頂点シェーダからピクセルシェーダに渡すデータ
0015: // -------------------------------------------------------------
0016: struct VS_OUTPUT
0017: {
0018: float4 Pos : POSITION;
0019: };
0020:
0021: // -------------------------------------------------------------
0022: // 頂点シェーダプログラム
0023: // -------------------------------------------------------------
0024: VS_OUTPUT VS (
0025: float4 Pos : POSITION, // モデルの頂点
0026: float3 Normal : NORMAL // モデルの法線
0027: ){
0028: VS_OUTPUT Out = (VS_OUTPUT)0; // 出力データ
0029:
0030: // 光の裏になっている面を後ろに引き伸ばす
0031: float4 dir = vLightPos - Pos;
0032: float LN = dot( Normal, dir );
0033: float scale = (0<LN) ? 0.0f : 1.0f;
0034:
0035: // 座標変換
0036: Pos.xyz -= 0.001f*Pos;// 縞がおきないように少し縮める
0037: Out.Pos = mul( Pos - scale * dir, mWVP );
0038:
0039: return Out;
0040: }
0041: // -------------------------------------------------------------
0042: // テクニック
0043: // -------------------------------------------------------------
0044: technique TShader
0045: {
0046: pass P0
0047: {
0048: // シェーダ
0049: VertexShader = compile vs_1_1 VS();
0050: PixelShader = NULL;
0051: }
0052: }
このシェーダでレンダリングされるシャドウボリュームは、下のようになります。
真っ白な部分が、シェーダでレンダリングされたシャドウボリュームです。
DirectX で必要なのが能力チェックです。
今回は、いつものシェーダのほかに、両面ステンシルが使えるかどうかのチェックと、ステンシルバッファが使えるかどうかチェックします。
今回は、adapterFormat と、backBufferFormat の2つのフォーマットが必要なので、
AppWizard のフレームワークを使うのではなくて、サンプルのものを使いました。
main.cpp
0200: HRESULT CMyD3DApplication::ConfirmDevice( D3DCAPS9* pCaps, DWORD dwBehavior,
0201: D3DFORMAT adapterFormat, D3DFORMAT backBufferFormat )
0202: {
0203: // シェーダのチェック
0204: if( pCaps->VertexShaderVersion < D3DVS_VERSION(1,1) )
0205: if( (dwBehavior & D3DCREATE_SOFTWARE_VERTEXPROCESSING ) == 0 )
0206: return E_FAIL;
0207:
0208: // 両面ステンシル機能の確認
0209: if( !( pCaps->StencilCaps & D3DSTENCILCAPS_TWOSIDED ) ) return E_FAIL;
0210:
0211: // ステンシル機能をサポートしているかチェック
0212: if( FAILED( m_pD3D->CheckDeviceFormat( pCaps->AdapterOrdinal
0213: , pCaps->DeviceType
0214: , adapterFormat
0215: , D3DUSAGE_RENDERTARGET
0216: | D3DUSAGE_QUERY_POSTPIXELSHADER_BLENDING
0217: , D3DRTYPE_SURFACE
0218: , backBufferFormat ) ) )
0219: return E_FAIL;
0220:
0221: return S_OK;
0222: }
では、描画の部分を見てみましょう。
ステンシルシャドウの最初はいつもどおりの描画です。
画面クリアした後に、普通にモデルを表示します。
main.cpp
0425: HRESULT CMyD3DApplication::Render()
0426: {
0427: D3DXMATRIX m, mW, mS, mR, mT;
0428: D3DXVECTOR4 v;
0429:
0430:
0431: //画面のクリア
0432: m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL,
0433: 0x000000ff, 1.0f, 0L );
0434:
0435: // 描画の開始
0436: if( SUCCEEDED( m_pd3dDevice->BeginScene() ) ) {
0437:
0438: // ----------------------------------------------------------
0439: // 下準備:影なし部分の描画
0440: // ----------------------------------------------------------
0441: D3DXMatrixIdentity( &m );
0442: m_pd3dDevice->SetTransform( D3DTS_WORLD, &m );
0443: m_pMeshBG->Render( m_pd3dDevice );
0444:
0445: // 小さい箱
0446: D3DXMatrixScaling( &mS, 1.82f,1.65f, 1.82f );
0447: D3DXMatrixRotationY( &mR, 0.59f*D3DX_PI );
0448: D3DXMatrixTranslation( &mT, 2.73f-1.85f, 0.f , 1.69f );
0449: m = mS * mR * mT;
0450: m_pd3dDevice->SetTransform( D3DTS_WORLD, &m );
0451: m_pMeshBox->Render( m_pd3dDevice );
0452:
0453: // 大きい箱
0454: D3DXMatrixScaling( &mS, 1.69f, 3.30f, 1.69f );
0455: D3DXMatrixRotationY( &mR, 0.91f*D3DX_PI );
0456: D3DXMatrixTranslation( &mT, 2.73f-3.685f, 0, 3.51f );
0457: m = mS * mR * mT;
0458: m_pd3dDevice->SetTransform( D3DTS_WORLD, &m );
0459: m_pMeshBox->Render( m_pd3dDevice );
0460:
次にシャドウボリュームの描画です。
とにかくステンシル系の設定が倍になりました。
D3DRS_TWOSIDEDSTENCILMODE を有効にすれば、両面ステンシルがつかるようになります。
後は、「CCW」が付く状態が裏面を描画したときに変更されるステンシルバッファに関する設定なので、
いつも2パス目で書くシャドウボリュームの設定をこちらに描きます。
main.cpp 0461: // ---------------------------------------------------------- 0462: // パス1:影ボリュームの描画 0463: // ---------------------------------------------------------- 0464: // 深度バッファに書き込みはしない 0465: m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, FALSE ); 0466: // レンダリングターゲットに書き込みはしない 0467: m_pd3dDevice->SetRenderState( D3DRS_COLORWRITEENABLE, FALSE ); 0468: // フラットシェーディングする 0469: m_pd3dDevice->SetRenderState( D3DRS_SHADEMODE, D3DSHADE_FLAT ); 0470: // 両面描く 0471: m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ); 0472: 0473: // 両面ステンシルを使用する 0474: m_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, TRUE ); 0475: m_pd3dDevice->SetRenderState( D3DRS_TWOSIDEDSTENCILMODE, TRUE ); 0476: 0477: // ステンシルテストは常に合格する(=テストしない) 0478: m_pd3dDevice->SetRenderState( D3DRS_STENCILFUNC, D3DCMP_ALWAYS ); 0479: m_pd3dDevice->SetRenderState( D3DRS_CCW_STENCILFUNC, D3DCMP_ALWAYS ); 0480: // ステンシルバッファの増減を1に設定する 0481: m_pd3dDevice->SetRenderState( D3DRS_STENCILREF, 0x1 ); 0482: m_pd3dDevice->SetRenderState( D3DRS_STENCILMASK, 0xffffffff ); 0483: m_pd3dDevice->SetRenderState( D3DRS_STENCILWRITEMASK, 0xffffffff ); 0484: // 表面は深度テストに合格したらステンシルバッファの内容を+1する 0485: m_pd3dDevice->SetRenderState( D3DRS_STENCILPASS, D3DSTENCILOP_INCR ); 0486: m_pd3dDevice->SetRenderState( D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP ); 0487: m_pd3dDevice->SetRenderState( D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP ); 0488: // 裏面は深度テストに合格したらステンシルバッファの内容を-1する 0489: m_pd3dDevice->SetRenderState( D3DRS_CCW_STENCILPASS, D3DSTENCILOP_DECR ); 0490: m_pd3dDevice->SetRenderState( D3DRS_CCW_STENCILZFAIL, D3DSTENCILOP_KEEP ); 0491: m_pd3dDevice->SetRenderState( D3DRS_CCW_STENCILFAIL, D3DSTENCILOP_KEEP );
そして、実際のレンダリングは、シェーダやシェーダの変数を設定してレンダリングすれば大丈夫です。
main.cpp
0493: // レンダリングする
0494: if( m_pFx != NULL ){
0495: D3DXHANDLE hTechnique = m_pFx->GetTechniqueByName( "TShader" );
0496: m_pFx->SetTechnique( hTechnique );
0497: m_pFx->Begin( NULL, 0 );
0498: m_pFx->Pass( 0 );
0499:
0500: // 小さい箱
0501: D3DXMatrixScaling( &mS, 1.82f,1.65f, 1.82f );
0502: D3DXMatrixRotationY( &mR, 0.59f*D3DX_PI );
0503: D3DXMatrixTranslation( &mT, 2.73f-1.85f, 0.f , 1.69f );
0504: mW = mS * mR * mT;
0505: m = mW * m_mView * m_mProj;
0506: if( m_hmWVP != NULL ) m_pFx->SetMatrix( m_hmWVP, &m );
0507: D3DXMatrixInverse( &m, NULL, &mW);
0508: D3DXVec3Transform( &v, &m_LighPos, &m );
0509: if( m_hvPos != NULL ) m_pFx->SetVector( m_hvPos, &v );
0510: m_pShadowBox->Render( m_pd3dDevice );
0511:
0512: // 大きい箱
0513: D3DXMatrixScaling( &mS, 1.69f, 3.30f, 1.69f );
0514: D3DXMatrixRotationY( &mR, 0.91f*D3DX_PI );
0515: D3DXMatrixTranslation( &mT, 2.73f-3.685f, 0, 3.51f );
0516: mW = mS * mR * mT;
0517: m = mW * m_mView * m_mProj;
0518: if( m_hmWVP != NULL ) m_pFx->SetMatrix( m_hmWVP, &m );
0519: D3DXMatrixInverse( &m, NULL, &mW);
0520: D3DXVec3Transform( &v, &m_LighPos, &m );
0521: if( m_hvPos != NULL ) m_pFx->SetVector( m_hvPos, &v );
0522: m_pShadowBox->Render( m_pd3dDevice );
0523:
0524: m_pFx->End();
0525: }
最後にステンシルテストしながら全画面に暗い半透明を描画します。
ステンシルバッファの値が0でないところが影になるので、そのようなテストで書き込みます。
main.cpp 0527: // 状態を元に戻す 0528: m_pd3dDevice->SetRenderState( D3DRS_SHADEMODE, D3DSHADE_GOURAUD ); 0529: m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW ); 0530: m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, TRUE ); 0531: m_pd3dDevice->SetRenderState( D3DRS_COLORWRITEENABLE, 0xf ); 0532: m_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, FALSE ); 0533: m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE ); 0534: m_pd3dDevice->SetRenderState( D3DRS_TWOSIDEDSTENCILMODE, FALSE ); 0535: 0536: // ---------------------------------------------------------- 0537: // パス2:影の描画 0538: // ---------------------------------------------------------- 0539: // 深度テストはしない 0540: m_pd3dDevice->SetRenderState( D3DRS_ZENABLE, FALSE ); 0541: // ステンシルテストはする 0542: m_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, TRUE ); 0543: // アルファブレンディングは線形に掛ける 0544: m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE ); 0545: m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); 0546: m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); 0547: // ポリゴンを描画するときには、テクスチャと頂点色の両方を見る 0548: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); 0549: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE ); 0550: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE ); 0551: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); 0552: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE ); 0553: m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); 0554: 0555: // ステンシルバッファの値が1以上のときに書き込む 0556: m_pd3dDevice->SetRenderState( D3DRS_STENCILREF, 0x1 ); 0557: m_pd3dDevice->SetRenderState( D3DRS_STENCILFUNC, D3DCMP_LESSEQUAL ); 0558: m_pd3dDevice->SetRenderState( D3DRS_STENCILPASS, D3DSTENCILOP_KEEP ); 0559: 0560: m_pBigSquare->Render( m_pd3dDevice );
最後に元に戻して終了です。
main.cpp 0562: // 状態を元に戻す 0563: m_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE ); 0564: m_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, FALSE ); 0565: m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE ); 0566: 0570: // 描画の終了 0571: m_pd3dDevice->EndScene(); 0572: } 0573: 0574: return S_OK; 0575: }
まぁ、当たり前なのですが、コーネル大にある元の配置と
今回の結果は、影の位置が一致しますね(もちろん、こちらは面光源を使っていないのでぼやけませんが)。
コーネル箱は本当にただの箱なので、メリハリをつけるのが大変ですね。
影については、ちゃんとしたのができたので、個人的には気に入っています。
ただ、ステンシルシャドウのくっきりする欠点はどうしようも無いですね。
ステンシルソフトシャドウは、どうすればできるのだろうか・・・