ホーム < ゲームつくろー! < DirectX技術編 < 板ポリゴンに移る3Dオブジェクト


その6 板ポリゴンに写る3Dオブジェクト

 
 最近のSTGでは3Dのキャラが動くというのが増えてきました。中には本当に3次元空間で座標計算をしているのもあります。しかし、擬似的に3次元に見せているSTGも数多くあります。本当は単なる2Dの絵なのにそこに3Dのオブジェクトを描画する事で擬似的に空間を演出するわけです。

 2Dの絵に3Dを描画するためには、1つはそういう絵を用意しておくと言うのがあります。これは気軽にらしさを演出できます。別の方法として、テクスチャに3Dオブジェクトをレンダリングして、そのテクスチャを板ポリゴンに張るというのもあります。Direct3Dはそういう事が出来ます。

 この章の目的は、3Dのレンダリングをテクスチャに対して行い、それを板ポリゴンに張ろうというものです。その為に必要な事を次から検討していきます。

@ 最も簡単なテクスチャレンダリング

 まず、テクスチャにレンダリングするもっともシンプルな設定から見て行きます。
 Direct3Dではレンダリングするテクスチャやサーフェイスの事を「レンダリングターゲット」と総称しています。これまでの単純な描画は、レンダリングターゲットとしてバックバッファが指定されていただけでして、これをバックバッファ以外に差し替える事が可能です。レンダリングターゲットはIDirect3DDevice9::SetRenderTarget関数を用いると簡単に変更が出来ます。

HRESULT SetRenderTarget(
DWORD RenderTargetIndex,
IDirect3DSurface9* pRenderTarget
);

レンダリングターゲットは複数同時に持っておくことが出来ます。RenderTargetIndexはその番号です。しかし、制約もあり、またこの機能をサポートしていないビデオカードもありますので、ここでは0にして単体だけの実装にします。
pRenderTargetは対象となるサーフェイスへのポインタを渡します。以後のレンダリング作業はすべてこのサーフェイスに対して行われます。

 SetRenderTarget関数を用いる場合、最初にバックバッファのサーフェイスへのポインタを取得しておかなければなりません。IDirect3DDevice9::Present関数を呼び出したときに画面に出るのはあくまでもバックバッファなので、最終的にはバックバッファにレンダリングターゲットを切り替える必要があります。ですから、そのポインタを取っておかないと、差し替えた瞬間に最終的な描画をする「先」を失います。現在のレンダリングサーフェイスを取得するにはGetRenderTarget関数を用います。

HRESULT GetRenderTarget(
DWORD RenderTargetIndex,
IDirect3DSurface9** ppRenderTarget
);

RenderTargetIndexは取得したいレンダリングターゲットへの番号です。今は0を設定します。
ppRenderTargetに取得したいレンダリングターゲットへのポインタが返ります。

 この作業は初期化が終わった後に(「初期化なんて怖くないぜ!」を参照」)1度だけ行えば良いでしょう。ただし、1つ重要な事があります。GetRenderTarget関数でサーフェイスへのポインタを取得すると、そのサーフェイスの内部参照カウンタが1つ増えます。具体的には、初期化の後にバックバッファへのポインタを取得した段階で、バックバッファの内部参照カウンタは「2」になります。よって取得したバックバッファのポインタを使用しなくなった段階でRelease関数を呼び出さないとメモリリークが発生します。

 では具体的な話をしていきます。まず、今回はテクスチャにレンダリングしようという事なので、そういうテクスチャを作成しなければなりません。これはCreateTexture関数を持ちます(「テクスチャ作成あれこれ」を参照)。この時、フラグの設定が重要です。レンダリングターゲットにするには、UasageをD3DUSAGE_RENDERTARGETに設定します。マニュアルにもありますが、このフラグを設定した時にはPoolをD3DPOOL_DEFAULTにします。作成例は次の通りです。

pD3dDev->CreateTexture(
width, height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
pTexture,
NULL
);

レンダリングターゲットとなるテクスチャの大きさ(width, height)は任意です。ピクセルフォーマットもサポートしている物であれば何でもかまいません。

 次にテクスチャからサーフェイスへのポインタを取得します。これはIDirect3DTexture9::GetSurfaceLevel関数を用います。

HRESULT GetSurfaceLevel(
UINT Level,
IDirect3DSurface9** ppSurfaceLevel
);

Levelはミップマップのレベルです。一番最上位は0ですから、ここでは0を設定します。
ppSurfaceLevelに欲しいサーフェイスへのポインタが返ります。

GetRenderTarget関数と同様に、GetSurfaceLevel関数で得たサーフェイスも内部参照カウントが1つ増加します。使用しなくなったらサーフェイスを正しくReleaseしましょう。

 後は得たサーフェイスへのポインタをSetRenderTargetへ渡せばとりあえずテクスチャへのレンダリングは出来ます。ここまでの流れを下にまとめました。


これで一応描画は可能になるのですが、3Dの描画となるともう少し手を加えなければなりません。


A 3Dオブジェクトをテクスチャへレンダリングするために

 @ではテクスチャへのレンダリングに必要な最低限の流れを示しました。3Dへのレンダリングを行う場合、さらに色々と用意する物があります。
 通常のバックバッファへ3Dオブジェクトを描画する時には「Zバッファ」やと「ステンシルバッファ」を同時に使用しています。Zバッファとは、3Dのオブジェクトを描画する時のオブジェクトの最前位置を保持するメモリ領域の事で、オブジェクトの前後判断をこれで行っています。ステンシルバッファとは、描画そのものを行うか行わないかを決める値を保持するメモリ領域の事です。これらのバッファは合わせて「深度バッファ」「と呼ばれています。
 テクスチャへ描画する時には、深度バッファも用意しなければうまく3Dのレンダリングが出来ません。例えオブジェクトが1つであってもポリゴン単位の前後判断を行うために深度バッファは必要です。深度バッファはIDirect3DDevice9::SetDepthStencilSurface関数でレンダリングターゲットに設定します。この時、バックバッファの深度バッファをIDirect3DDevice9::GetDepthStencilSurface関数で保持しておかなければ差し替えがうまくいきません。

HRESULT SetDepthStencilSurface(
IDirect3DSurface9* pZStencilSurface
);
HRESULT GetDepthStencilSurface(
IDirect3DSurface9** ppZStencilSurface
);

pZStencilSurface及びppZStencilSurfaceが設定及び取得のポインタです。GetDepthStencilSurface関数もまた取得するとサーフェイスの内部参照カウンタが1つ増えますので、いらなくなったらReleaseを忘れずに行ってください。

深度バッファはCreateDepthStencilSurface関数を用います。

HRESULT CreateDepthStencilSurface(
UINT Width,
UINT Heignt,
D3DFORMAT Format,
D3DMULTISAMPLE_TYPE MultiSample,
DWORD MultisampleQuality,
BOOL Discard,
IDirect3DSurface9** ppSurface,
HANDLE* pHandle
);

WidthHeightは深度バッファの大きさで、これはレンダリングターゲットとして確保したテクスチャと必ず同じ大きさにする必要があります。
Formatは深度バッファのピクセルフォーマットを指定します。これはRGBAという方式ではなくて、専用の方式がD3DFORMAT列挙型に定義されています。D3DFMT_D16にすると、16ビットのZバッファが確保されますが、ステンシルバッファは使用されません。D3DFMT_D24S8にすると、24ビットのZバッファと8ビットのステンシルバッファが確保されます。
MultiSampleはマルチサンプルと言う補間レベルを設定しますが、ここはマルチサンプルを使わないのでD3DMULTISAMPLE_NONEにします。深度バッファについては補間する必要性は殆どありません。
MultisampleQualityは補間の品質を設定します。ここも使わないので0にします。
DiscardはSetDepthStencileSurface関数で深度バッファを変更した時にバッファを破棄するかどうかを決定します。TRUEにすると破棄する事になります。ここではTRUEを設定しておきましょう。
IDirect3DSurface9に深度バッファサーフェイスが格納されます。
pHandleは現在使用されていないのでNULLを指定して下さい。


 SetRenderTarget関数でレンダリングターゲットをテクスチャに変更した時、確保した深度バッファも一緒にSetDepthStencil関数で設定します。これで3Dを描画するための準備はできました。

 レンダリングターゲットを変更した直後にはサーフェイスをクリアするのが普通です(そうしない場合もありますが)。それにはIDirect3DDevice9::Clear関数を用います。以下は典型的な設定例です。

HRESULT hr = Clear(
0, // クリアする矩形領域の数
NULL, // 矩形領域
D3DCLEAR_TARGET
| D3DCLEAR_ZBUFFER
| D3DCLEAR_STENCIL,
// レンダリングターゲットと深度バッファをクリア
D3DCOLOR_RGBA(0, 0, 0, 0) // 色を黒に透明度を100%にクリア
1.0f, // Zバッファのクリア値
0 // ステンシルバッファのクリア値
);

もし深度バッファを用いていないのであれば、Clear関数の第3引数にはD3DCLEAR_TARGETのみを指定して下さい。またステンシルバッファを用いないフォーマットにした場合は、D3DCLEAR_ZBUFFERのみを追加してください。ここの設定を間違うと変な描画になります。


 後は、通常の描画とまったく同じ方法でテクスチャに対してレンダリングができます。カメラや射影行列、ライトなどもしっかり設定してください(もちろん、それらを使用しない頂点変換済み2Dポリゴンを使ってもいっこうに構いません)。テクスチャに対して3Dのオブジェクトをレンダリングして、そのテクスチャを2Dの板ポリゴンに貼ってバックバッファに書き込む。これによって「擬似3Dオブジェクトを用いた2Dゲーム」が出来ます。

 この方法は便利と言えば便利なのですが、レンダリングを沢山行う事になるので、使いすぎるとパフォーマンスに影響を及ぼします。程ほどにするのがベターでしょうね。
 使い道はとても幅広く、ステンシルバッファも併用すれば鏡面効果や影などを演出できます。ゲームの場合、ゲーム画面やスコア画面など画面構成をレンダリングターゲット単位にすると、クリッピングなどの事を考えなくてよくなるので非常に開発が楽になりますし、実はパフォーマンスも良くなります。例えば、ゲームの画面が400×400ピクセルくらいだとして、残りの画面は常に固定的だとしましょう。この時、背景となるレンダリングターゲットはクリアせず、ゲーム画面を構成する部分だけを変更します。そうすると全画面消去をする必要がなくなるので、パフォーマンスの向上が見込めます。これは、横画面で縦STGを作る時に、余った部分に対しての処理を緩和させるのに便利です。またあえて大きいテクスチャを作成しておいて、画面サイズの板ポリゴンに貼り付け描画すれば、アンチエイリアスのかかった綺麗なゲームも出来るかもしれません(処理落ちしそうですが・・・)。

 最後に今回の部分のクラス化についてです。これまでの説明からテクスチャへの描画作業が別の描画プロセス中に「入れ子」になっている事がわかると思います。こういうのは案外クラス化しやすいものなのです。ターゲットとするテクスチャを指定し、それに対してレンダリングをする一連の塊をクラスにしてしまいます。また、前のレンダリングターゲットの情報があれば、処理終了後に簡単にターゲットを戻す事が出来ますし、関数を工夫すればそういう作業をすべて隠蔽し、描画だけに集中できます。
 この描画クラスについては「DirectX クラス構築編」で考えてみたいと思います。

参照: N2FACTORY. 「DirectXゲームグラフィックスプログラミング Ver. 2.0」