ホーム < ゲームつくろー! < DirectX技術編 < 深度バッファシャドウの根っこ:Z値テクスチャ作成クラスを作る

その45 深度バッファシャドウの根っこ:Z値テクスチャ作成クラスを作る


 深度バッファシャドウの原理についてその44で説明をしました。結論として、深度バッファシャドウはライト目線から見たZ値テクスチャを作成部分とZ値比較の2つにぱっきり分離できる事になります。Z値テクスチャの作成方法についてはその43で述べましたが、毎回これを書き込むのは何とも芸がありません。

 そこで、本章では深度バッファシャドウを実現する準備として、Z値テクスチャを作成するクラスを作り、前半部分の作業を簡素化隠蔽化してみます。プログラマブルシェーダの入ったクラスをどう作るか?そこがポイントです。



@ Z値プロットシェーダをリソースにします

 シェーダプログラムはテキストで書かれ外部に置かれます。ということは、そのままにすると改変され放題です。そこで、シェーダプログラムをリソースとして実行ファイルに埋め込んでしまいます。

 シェーダプログラムをリソースにすると、クラス化への道が見えてきます。これについてはプログラマブルシェーダ編その8『HLSLシェーダプログラムのリソース化』で詳し〜〜く説明しております。そこで導いた結論は、エフェクトリソースヘッダを共有し、リソースファイル等を各クラス独自で定義するというものでした。PSその8で引き合いに出した図をここでも示します:


 各エフェクトについてクラスヘッダー、実装ファイル、エフェクトファイルそしてリソースファイルを定義します。リソースファイルが必要とするリソースIDマクロを1つのエフェクトリソースヘッダーに登録すれば、シェーダプログラムをリソースから使う事が可能になります。


 さて、Z値テクスチャを作成するエフェクトファイルはすでに作成してあります(その43のサンプルプログラム)。ここではこれをクラスとしてまとめる事に集中します。



A Z値テクスチャ作成クラスはID3DXEffectのラッパーです

 Z値テクスチャ作成クラス(CZTexCreatorクラス)の使い方は簡単にしたいのですが、ID3DXEffectをベースに敷くためどうしても描画時の呼び出しルールにクラスを従わせざるを得ません。その結果、CZTexCreatorクラスはID3DXEffectのラッパークラスとして働く事になります。

 ID3DXEffectインターフェイスを用いる場合、描画は必ずID3DXEffect::BeginPassメソッドとEndPassメソッドの間で行う必要があります。しかも面倒な事に、この呼び出しは1つの描画対象(サブセット)を描画する度に呼び出さなければなりません。例えば、

pEffect->BeginPass(0);
for(i=0; i<10;i++)
{
   DrawObject[i]->Draw();
}
pEffect->EndPass();

というパス内での連続的な描画呼び出しが「できない」という事です。正しく描画するには、

for(i=0; i<10;i++)
{
   pEffect->BeginPass(0);
   DrawObject[i]->Draw();
   pEffect->EndPass();
}

と毎回パスを呼び出さなければなりません。面倒ですが仕様なのでどうしようもありません。ラッパーであるZ値テクスチャ作成クラスもこの仕様に縛られます。すなわち描画時には、

CZTexCreatorクラスによる描画
pZTexCreator->Begin();
for(i=0; i<10;i++)
{
   pZTexCreator->BeginPass(0);
   pZTexCreator->SetWorldMat( DrawObject[i].GetWorldMat() );
   DrawObject[i]->Draw();
   pZTexCreator->EndPass();
}
pZTexCreator->End();

とパスを毎回呼び出す使い方になります。ただもちろん、必要な行列を設定するメソッドは公開し、テクニックの設定など外部が知る必要の無い部分は内部にしっかり隠蔽します。



B CZTexCreatorクラスに必要な情報

 Z値テクスチャ作成クラス(CZTexCreator)の仕事の目的は、もちろんある視点から見たZ値を格納したテクスチャを作成する事です。

 Z値テクスチャを作成するために、外部が与えなければいけないのは次の情報です:

・ 描画デバイス
・ Z値テクスチャのフォーマットと大きさ(幅高)
・ あるオブジェクトに適用されるワールド変換行列
・ シーンに設定されるカメラビュー変換行列
・ シーンに設定される射影変換行列

独自の深度バッファ、エフェクトプログラムの呼び出し、テクニックの指定などはすべてクラスの内部に隠蔽してしまいますので、ユーザはそれを気にする必要はありません。最終的にテクスチャを取得してそれを活用していく事になります。

 これらの設定メソッドに加え、描画時に必要なメソッドは次の通りです:

・ 描画の開始を告げる (Beginメソッド)
・ パスを開始する (BeginPassメソッド)
・ パスを終了する (EndPassメソッド)
・ 描画の終了を告げる (Endメソッド)
・ 設定値を適用する (SetParamToEffectメソッド)

これらはすべてID3DXEffectインターフェイスが持つ同名のメソッドに使い方を含め準拠します。

 ということで、クラス宣言部内のメンバ変数宣言は次のようになりそうです:

CTexCreatorクラス宣言部(メンバ変数)
class CZTexCreator
{
protected:
   Com_ptr<IDirect3DDevice9>  m_cpDev;       // 描画デバイス
   Com_ptr<IDirect3DTexture9> m_cpZTex;      // Z値テクスチャ
   Com_ptr<IDirect3DSurface9> m_cpZTexSurf;  // Z値テクスチャサーフェイス
   Com_ptr<IDirect3DSurface9> m_cpDepthBuff; // 深度バッファ
   Com_ptr<IDirect3DSurface9> m_cpDevBuffer; // デバイスバックバッファ
   Com_ptr<IDirect3DSurface9> m_cpDevDepth;  // デバイス深度バッファ
   Com_ptr<ID3DXEffect>       m_cpEffect;    // Z値プロットエフェクト
   D3DXMATRIX m_matWorld;   // ワールド変換行列
   D3DXMATRIX m_matView;    // ビュー変換行列
   D3DXMATRIX m_matProj;    // 射影変換行列
   D3DXHANDLE m_hWorldMat;  // ワールド変換行列ハンドル
   D3DXHANDLE m_hViewMat;   // ビュー変換行列ハンドル
   D3DXHANDLE m_hProjMat;   // 射影変換行列ハンドル
   D3DXHANDLE m_hTechnique; // テクニックへのハンドル

   //...
};

どばーっと沢山ありますが、特に怖い事はありません。サーフェイス関係はDirectX技術編その43で説明したZ値テクスチャの作成方法で示した物です。m_cpEffectはZ値プロットを行えるエフェクトですが、当然外部には公開されません。後は各種行列関係が続きます。1つD3DXHANDLEは初物です。

 D3DXHANDLEはエフェクト内で使用される変数等を識別するハンドルです。この後にもすぐ出てきますが、例えばエフェクトにワールド変換行列を設定する時には、

pEffect->SetMatrix( "matWorld", &m_matWorld );

としてもできます。第1引数にエフェクト内で定義されている変数名をそのまま渡す事が可能なんです。ところが、この方法は負荷がかかります。どの程度かかるか測定はしていませんが、これはマニュアルにも記載されております。そこでD3DXHANDLEの登場です。

m_hWorldMat = pEffect->GetParameterByName( NULL, "matWorld" );

としますと、エフェクト内のmatWorldに対応するハンドルを取得する事ができます。一度取得すると後は、

pEffect->SetMatrix( m_hWorldMat, &m_matWorld );

とハンドルを渡す事で高速に値の代入を行う事ができます。クラス化して隠蔽する部分ですから、これを採用しない理由はありません。メンバ変数は以上となります。



C メンバメソッドの簡単な紹介

 メンバメソッドについては特段難しい部分はありません。DirectX技術編その43で示した方法を分割するだけです。その43との重複も多いため、ここでは設定したメソッドとその簡単な説明にとどめる事にしました。

 まず、クラス宣言部はこうなっています:

CTexCreatorクラス宣言部(メソッド)
class CZTexCreator
{
public:
   CZTexCreator();
   virtual ~CZTexCreator();

public:
   // 初期化メソッド
   bool Init( Com_ptr<IDirect3DDevice9> &cpDev, UINT ZTexWidth, UINT ZTexHeight, D3DFORMAT ZTexFormat=D3DFMT_A8R8G8B8 );

   // 描画対象オブジェクトのワールド変換行列を設定
   void SetWorldMatrix( D3DXMATRIX *pMat );

   // ビュー行列を設定
   void SetViewMatrix( D3DXMATRIX *pMat );

   // 射影変換行列を設定
   void SetProjMatrix( D3DXMATRIX *pMat );

   // 描画の開始を宣言する
   HRESULT Begin();

   // 描画の終了を宣言する
   HRESULT End();

   // パスの開始を宣言する
   HRESULT BeginPass();

   // パスの終了を宣言する
   HRESULT EndPass();

   // 登録されているパラメータ情報をエフェクトにセット
   bool SetParamToEffect();

   // Z値テクスチャを取得する
   bool GetZTex( Com_ptr<IDirect3DTexture9> &cpTex );
};


○ Initメソッド

 初期化を行います。引数に渡したデバイスを使いまわす事になります。引数にはあとZ値テクスチャの幅と高さ、そしてそのフォーマットを指定します。うまくいかなければfalseが返されます。


○ 各種行列設定メソッド(ワールド・ビュー・射影変換行列)

 Z値を算出するのに必要な行列を設定するメソッド群です。ワールド変換行列は1つのオブジェクトを描画する度に取り替えます。一方ビュー変換行列は1シーンごとに設定します。射影変換行列は普通一度設定したら不変です(そうでない場合もあります)。これらは描画する前であればいつでも取替え可能ですが、後述するSetParamToEffectメソッドを呼び出さないとエフェクトに適用されないので注意です。


○ Beginメソッド、Endメソッド

 描画開始と終了を宣言するメソッドです。Beginメソッドを呼び出すとデバイス内に設定されているバックバッファと深度バッファがクラス独自のバッファとそっくり入れ替わります。デバイスのデフォルトサーフェイスは一時的に保存されます。そしてEndメソッドで元に戻ります。描画はこのメソッドの間で行います。これらの呼び出しは1枚のZ値テクスチャを作成する時に一度だけ呼ばれれば良いだけですが、必ずペアで呼び出す必要があります。そうしないと、デバイスが持っていたバックバッファが迷子になってしまいます。


○ BeginPassメソッド、EndPassメソッド

 1回のオブジェクトを描画する時の宣言メソッドです。1回とは「1回のIDirect3DDevice9::DrawPrimitiveメソッド系」か、「1回のID3DXMesh::DrawSubsetメソッド系」です。BeginPassメソッドは描画前に、EndPassメソッドは描画後に必ずペアで呼び出さなければなりません。このメソッドは内部のエフェクトの再設定を行っているんです。ただ、行列パラメータは代入されません。


○ SetParamToEffectメソッド

 内部に保持されている行列をエフェクトに適用します。これは描画の直前に呼び出します。DrawSubsetメソッドなどの直前に呼び出すのが普通です。


○ GetZTexメソッド

 作成したZ値テクスチャを取得します。これはいつでも取得できます。



D とりあえず使ってみましょう

 クラスは試しに使ってみた方が覚えるもんです。詳しい例はサンプルプログラムの方に回すとしまして、ここでは簡単な使い方を示します。

 Z値テクスチャだけを得る目的ならば、オブジェクトを作成してからテクスチャを取得するまでは下のようなプログラムで十分です:

Z値テクスチャ作成例
// すでにCOMポインタデバイス(cpDev)描画メッシュが10個(cpMesh[10])、各種行列があるとします

CZTexCreator ZC;
if( !ZC.Init( cpDev, 1024, 1024 ) )
   return false;

// Z値テクスチャ作成
ZC.Begin();
ZC.SetViewMatrix( &matView );   // ビュー変換行列設定
ZC.SetProjMatrix( &matProj );     // 射影変換行列
for( i=0; i<10; i++)
{
   ZC.SetWorldMatrix( &matWorld[i] );
   for(j=0; j<MatNum[i]; j++)
   {
      ZC.BeginPass();
      ZC.SetParamsToEffect();
      cpMesh->DrawSubset(j);
      ZC.EndPass();
   }
}
ZC.End();


// Z値テクスチャ取得
Com_ptr<IDirect3DTexture9> cpZTex;
ZC.GetZTex( cpZTex );


初期化の後にもういきなり描画に入れます。描画の筋道もこんな感じで大体固定です。メッシュのサブセット後にBeginPassメソッドとEndPassメソッドがあることに注意してください。



 スクリーンに見えているオブジェクトまでの距離(Z値)の算出(テクスチャ作成)は、DirectX技術編その44で説明している深度シャドウマップによる影生成の前半部分に相当します。このクラスだけでその部分が少し楽になります。また、Z値を使ったゲームだって創意と工夫で考えられるかもしれません。願わくばエフェクトの煩雑な呼び出しが簡素にできれば良いのですが・・・。もし、「パス内で何度も描画できるんですよ」という情報をお持ちの方がいらっしゃいましたら、是非お知らせ下さい。