背景マッピング


~ Pre-Computed Matting ~






■はじめに

BBXで、マリーニさんから、

実は、今DirectX9でバイオハザードのような一枚絵のゲームを作っていますて、
視点の切り替え時にレンダリング&Zバッファを作成、保存しておき次からは
その視点である限りコピーして使いまわそう、と考えまして。これなら、とて
もポリゴン数が増えても、切り替え時しかレンダリングしないので結構イケま
すよね。

しかし、Zバッファのコピーはやってみるとできないじゃないですか。(笑)
色々と調べましたが、どうやらだめで。ロックすればなんとか
できそうですが、機種が限られてしまうとか。

ですので今は、背景となるポリゴンをテクスチャをひっぺがしてライティング
なしで、極力負荷が抑えられるように描画し(Zバッファ書き込みだけが必要で
あるため)、その上に保存しておいた一枚絵を描画しています。

なんとか、解決する方法ありませんかな、と思って投稿しました。皆様のお知恵
をお貸しください。よろしくお願いします。

という質問を受けて、「ps 1_3以上なら出来るんじゃないの?ま、でも実用的なのは2_0以上だろ」と返したのですが、 そういえば深度バッファへの書き込みはやっていなかったので、実装してみました。

今回のプログラムは、次のものです。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

あと、実行ファイル、プロジェクトファイルが入っています。

■何やってるの?

今回の方法は、あらかじめ背景の色と深度バッファの値をテクスチャに保存しておいて、レンダリングの時には、背景を描画する代わりにこれらのテクスチャをレンダリングします。
今回は、初期化時やカメラが変わった時に背景画像を作り直していますが、基本的には、RGB8ビットの画像なので、バイオハザードのようなカメラが変わらないゲームでは、あらかじめ画像ファイルを用意しておいて、適当なときに読み込むのが良いでしょう。

さて、上の図で、深度バッファのイメージがカラフルなことに気がついたでしょうか。
我々が普段使う深度バッファは16ビットないしは24ビットの精度があります。 これをそのままテクスチャに書き出しただけでは8ビットの精度に落ちてしまうので、深度値を各色成分に上位、中位、下位に分けて保存しました。
なお、浮動小数点バッファを使う手もありますが、今回は汎用性を重視して個の実装にしました。

具体的に、0.0~1.0の深度値を展開する式は、各成分が256段階なので、

R = depth
G = 小数部(        256.0 * depth)
B = 小数部(256.0 * 256.0 * depth)
のようにすれば、それぞれの上位8ビットを取って適当な値が保存されます。
逆に色から深度値を戻すには、
depth = R + G / 256.0 + B / (256.0*256.0)
のようにすればよいでしょう。
実際にテクスチャを見ると、次のようになります。

■背景テクスチャの作成

ということで、今回のプログラムを紹介してきましょう。
今回一番働き者なのはピクセルシェーダで、色成分に普通にレンダリングすると共に、 頂点シェーダから出力された座標In.Posから、深度バッファに記録される値In.Pos.z/In.Pos.w計算して、色ごとに深度値をテクスチャに出力します。

hlsl.fx
0089: // -------------------------------------------------------------
0090: // ピクセルシェーダプログラム
0091: // -------------------------------------------------------------
0092: PS_OUTPUT PS ( VS_OUTPUT In ) {
0093:     
0094:     PS_OUTPUT Out = ( PS_OUTPUT ) 0;
0095:     
0096:     // 通常色
0097:     Out.Color = tex2D( DecaleSamp, In.Tex );
0098:     
0099:     // 深度
0100:     float depth = In.Pos.z / In.Pos.w;
0101:     Out.Depth.x = depth;
0102:     Out.Depth.y = depth * 256.0;
0103:     Out.Depth.z = depth * 256.0f * 256.0f;
0104:     Out.Depth.w = 0.0f;
0105:     Out.Depth = frac(Out.Depth);
0106: 
0107:     return Out;
0108: }

ちなみに、今回は、マルチレンダリングターゲットを使って、色と深度値を一度に別々のテクスチャに出力しました。
ピクセルシェーダから出力されるデータは、次のような構造体です。

hlsl.fx
0082: // -------------------------------------------------------------
0083: // ピクセルシェーダ出力データ
0084: // -------------------------------------------------------------
0085: struct PS_OUTPUT {
0086:     float4 Color  : COLOR0;
0087:     float4 Depth  : COLOR1;
0088: };

頂点シェーダも特別なプログラムを組まなくてはなりません。
ピクセルシェーダからは、レンダリングされる座標は直接はわからないので、 テクスチャ座標を通して、頂点シェーダからピクセルシェーダに座標値を出力します。

hlsl.fx
0057: // ------------------------------------------------------------
0058: // 頂点シェーダからピクセルシェーダに渡されるデータ
0059: // ------------------------------------------------------------
0060: struct VS_OUTPUT
0061: {
0062:     float4 Position     : POSITION;
0063:     float2 Tex          : TEXCOORD0;
0064:     float4 Pos          : TEXCOORD1;
0065: };

頂点シェーダプログラムでは、POSITION と TEXCOORD1の2つに同じ座標値を出力します。

hlsl.fx
0066: // ------------------------------------------------------------
0067: // 頂点シェーダプログラム
0068: // ------------------------------------------------------------
0069: VS_OUTPUT VS (
0070:       float4 Pos    : POSITION           // モデルの頂点
0071:      ,float4 Tex    : TEXCOORD0          // テクスチャ座標
0072: ){
0073:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0074:     
0075:     // 位置座標
0076:     Out.Pos = Out.Position = mul( Pos, mWVP );
0077:     
0078:     Out.Tex = Tex;
0079:     
0080:     return Out;
0081: }

アプリケーション側で用意するオブジェクトは、色用と深度用の2枚のテクスチャとそのサーフェス、また、レンダリング時に使う深度バッファになります。
また、m_bCreateというフラグを用意しました。これは、カメラが変わったときと初期化時にTRUEになるフラグで、これがたっているときにテクスチャを描画します。

main.h
0029: //-------------------------------------------------------------
0030: // Name: class CMyD3DApplication
0031: // Desc: アプリケーションのクラス
0032: //-------------------------------------------------------------
0033: class CMyD3DApplication : public CD3DApplication
0034: {
0035:     BOOL                    m_bCreate;  // マップを作成する
0036: 
0037:     CD3DMesh                *m_pMesh;
0038:     CD3DMesh                *m_pMeshBg;
0039:             
0040:     // シェーダ
0041:     LPD3DXEFFECT            m_pEffect;      // エフェクト
0042:     D3DXHANDLE              m_hTechnique;   // テクニック
0043:     D3DXHANDLE              m_htColor;      // テクスチャ
0044:     D3DXHANDLE              m_htDepth;      // テクスチャ
0045:     D3DXHANDLE              m_hmWVP;        // 変換行列
0046: 
0047:     // シャドウマップ
0048:     LPDIRECT3DSURFACE9      m_pMapZ;            // 深度バッファ
0049:     LPDIRECT3DTEXTURE9      m_pColorTex;        // 背景の色を格納する
0050:     LPDIRECT3DSURFACE9      m_pColorSurf;
0051:     LPDIRECT3DTEXTURE9      m_pDepthTex;        // 背景の深度を格納する
0052:     LPDIRECT3DSURFACE9      m_pDepthSurf;
0053: 

描画するプログラムですが、レンダリングターゲットを変更するときの基本的な手法と同じく、今までのレンダリングターゲットを保存しておいてから、レンダリングターゲットを切り替えます。
今回は、2枚のテクスチャに同時に書き込むので、レンダリングターゲットの設定SetRenderTargetは、2回呼び出します。

main.cpp
0397:         //-------------------------------------------------
0398:         // 背景マップの作成
0399:         //-------------------------------------------------
0400:         if(m_bCreate){
0401:             m_bCreate = FALSE;
0402: 
0403:             //-------------------------------------------------
0404:             // レンダリングターゲットの保存
0405:             //-------------------------------------------------
0406:             m_pd3dDevice->GetRenderTarget(0, &pOldBackBuffer);
0407:             m_pd3dDevice->GetDepthStencilSurface(&pOldZBuffer);
0408:             m_pd3dDevice->GetViewport(&oldViewport);
0409: 
0410:             //-------------------------------------------------
0411:             // レンダリングターゲットの変更
0412:             //-------------------------------------------------
0413:             m_pd3dDevice->SetRenderTarget(0, m_pColorSurf);
0414:             m_pd3dDevice->SetRenderTarget(1, m_pDepthSurf);
0415:             m_pd3dDevice->SetDepthStencilSurface(m_pMapZ);
0416:             // ビューポートの変更
0417:             D3DVIEWPORT9 viewport = {0,0      // 左上の座標
0418:                             , MAP_WIDTH  // 幅
0419:                             , MAP_HEIGHT // 高さ
0420:                             , 0.0f,1.0f};     // 前面、後面
0421:             m_pd3dDevice->SetViewport(&viewport);

描画するときは、先ほど作成したシェーダに描画方を切り替えて普通に描画します。
なお、今回の背景が白いのは、仕方なくやっていることです。
深度テクスチャとして初期化すべき値は1.0なのですが、深度用のテクスチャと色用のテクスチャで違う色で初期化できないので、全てのテクスチャを白で初期化します。

main.cpp
0423:             // クリア
0424:             m_pd3dDevice->Clear(0L, NULL
0425:                             , D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER
0426:                             , 0xffffff, 1.0f, 0L);
0427: 
0428:             //-------------------------------------------------
0429:             // シェーダの設定
0430:             //-------------------------------------------------
0431:             m_pEffect->SetTechnique( m_hTechnique );
0432:             m_pEffect->Begin( NULL, 0 );
0433:             m_pEffect->Pass( 0 );
0434: 
0435:             //-------------------------------------------------
0436:             // 背景の描画
0437:             //-------------------------------------------------
0438:             m = m_mWorld * m_mView * m_mProj;
0439:             m_pEffect->SetMatrix(m_hmWVP, &m);
0440: 
0441:             TSS( 0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1 );
0442:             TSS( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
0443:             m_pMeshBg->Render( m_pd3dDevice );
0444: 
0445:             m_pEffect->End();

最後にレンダリングターゲットを元に戻してテクスチャの作成は終了です。
特に2つめのレンダリングターゲットにNULLを設定することを忘れないようにしましょう。

main.cpp
0447:             //-----------------------------------------------------
0448:             // レンダリングターゲットを元に戻す
0449:             //-----------------------------------------------------
0450:             m_pd3dDevice->SetRenderTarget(0, pOldBackBuffer);
0451:             m_pd3dDevice->SetRenderTarget(1, NULL);
0452:             m_pd3dDevice->SetDepthStencilSurface(pOldZBuffer);
0453:             m_pd3dDevice->SetViewport(&oldViewport);
0454:             pOldBackBuffer->Release();
0455:             pOldZBuffer->Release();
0456:         }

■背景の描画

さて、後は、用意したテクスチャを使って背景を描画しましょう。
これは、普段画面クリアして、背景を描画するルーチンと下のルーチンを置き換えます。

main.cpp
0458:         //-------------------------------------------------
0459:         // 作成した背景の張り込み
0460:         //-------------------------------------------------
0461:         m_pEffect->SetTechnique( m_hTechnique );
0462:         m_pEffect->Begin( NULL, 0 );
0463:         m_pEffect->Pass( 1 );
0464: 
0465:         RS( D3DRS_ZFUNC, D3DCMP_ALWAYS );
0466:         RS( D3DRS_LIGHTING, FALSE );
0467:         TSS(0,D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0468:         TSS(0,D3DTSS_COLORARG1, D3DTA_TEXTURE);
0469:         TSS(1,D3DTSS_COLOROP,   D3DTOP_DISABLE);
0470:         
0471:         FLOAT w = (FLOAT)this->m_d3dsdBackBuffer.Width;
0472:         FLOAT h = (FLOAT)this->m_d3dsdBackBuffer.Height;
0473:         TVERTEX Vertex1[4] = {
0474:             //x  y     z    rhw  tu tv
0475:             { 0, 0, 0.001f, 1.0f, 0, 0,},
0476:             { w, 0, 0.001f, 1.0f, 1, 0,},
0477:             { w, h, 0.001f, 1.0f, 1, 1,},
0478:             { 0, h, 0.001f, 1.0f, 0, 1,},
0479:         };
0480:         m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX1 );
0481:         m_pEffect->SetTexture(m_htColor, m_pColorTex);
0482:         m_pEffect->SetTexture(m_htDepth, m_pDepthTex);
0483:         m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0484:                         , 2, Vertex1, sizeof( TVERTEX ) );
0485: 
0486:         m_pEffect->End();
0487: 
0488:         RS( D3DRS_ZFUNC, D3DCMP_LESSEQUAL ); 

基本的には、エフェクトを使って、四角形ポリゴンを全画面に表示しているだけです。
ここで、大事なのは、レンダリングステート D3DRS_ZFUNC に D3DCMP_ALWAYS を指定して、深度テストに常に合格するようにすることです。
これがないと、現在の深度バッファの値よりも奥の深度に書き込めなくなってしまいます。

さて、シェーダですが、次のように色成分はそのまま出力して、深度成分は成分ごとに格納された深度値の値を元に戻して出力します。

hlsl.fx
0123: // -------------------------------------------------------------
0124: // ピクセルシェーダ
0125: // -------------------------------------------------------------
0126: PS_OUTPUT_Mapping psMapping ( float4 Tex : TEXCOORD0 ) {
0127:     
0128:     PS_OUTPUT_Mapping Out = ( PS_OUTPUT_Mapping ) 0;
0129:     
0130:     // 通常色
0131:     Out.Color = tex2D( ColorSamp, Tex );
0132:     
0133:     // 深度
0134:     float4 depth = tex2D( DepthSamp, Tex );
0135:     Out.Depth = depth.x
0136:               + depth.y / 256.0f
0137:               + depth.z / (256.0f*256.0f);
0138:     
0139:     return Out;
0140: }

なお、ピクセルシェーダからの出力は、次の構造体になります。
DEPTH セマンティクスを使って、深度値の出力を宣言します。

hlsl.fx
0116: // -------------------------------------------------------------
0117: // ピクセルシェーダ出力データ
0118: // -------------------------------------------------------------
0119: struct PS_OUTPUT_Mapping {
0120:     float4 Color  : COLOR0;
0121:     float  Depth  : DEPTH;
0122: };

■最後に

ちなみに、今回の方法がどのくらい有効かということですが、 いつものように、毎フレ背景を描画したときには、520FPS、 今回の方法を使えば、700FPS と、確かに有効な方法であることが確認できました。
ただし、背景を作成したフレームでは、420FPSと遅くなってしまうので、 画像を更新するフレームには気をつけてください。

ところで、この技法って、どこかの会社が特許とってますかねぇ。
いかにも取られていても不思議でない気がするのですが、どなたかご存知ですか?





もどる

imagire@gmail.com