ホーム < ゲームつくろー! < DirectX技術編 < 座標変換済み頂点で2D板ポリゴンを描画


その2 座標変換済み頂点で2D板ポリゴンを描画


 Direct3Dで2Dを作成する事に意味はあるのか?といわれるとはっきりと「はい!あります!」と答える事が出来ます。3Dゲームの中にもスコア、タイム、ネームエントリーなど、奥行きを必要としない表示はいくらでも出てきますね。これらの表示にはやはり2D描画の技術が必要になってきます。もちろん2Dゲームを作る場合は、その技術自体が主流となるはずです。

 Direct3Dで2D描画を行う方法は幾つかあります。D3DX(Direct3D eXtention)のID3DXSpriteインターフェイスなどはその方法の1つです。しかし、このスプライトは簡単なぶん回転や拡大縮小、透過処理など思った事がなかなか出来なかったりします。現在のビデオカードでは、そういうエフェクトは3Dで行ったほうがはるかに簡単なのです。そこで、ここでは柔軟性がとても高いDirect3Dの範疇で2Dを表示をする方法を述べていくことにします。



@ 座標変換済み頂点

 奥行きのある3D空間に2Dを配置するというのは、まともに考えると非常に難しい技術です。現在の3Dでは奥行きだけでなく遠近感をも演出します。そのような視線を模倣した空間に画面固定の2Dを表示させることは難しいのです。そこで、Direct3DにはTVのテロップのように3Dの空間とは独立した2D表現を行う方法が用意されています。それには「座標変換済み頂点」という頂点フォーマットを利用します。

 頂点というのはポリゴンを形成する点の事です。その座標は3次元(XYZ座標)で定義されます。
 今回扱う「座標変換済み頂点」というのは、「ジオメトリパイプラインですでに変換されたとみなされる頂点座標」を意味します。ジオメトリパイプラインというのは3Dのオブジェクトを2Dの画面に表示するまでの一連の頂点変換過程の事です。ポリゴンの頂点はローカル座標というオブジェクト主体の座標空間に最初に定義されますが、これをワールド変換、ビュー変換、射影変換という3つの変換を通して最終的に2Dである画面内の座標に収まります。詳しくはDirectX技術編その7「3Dオブジェクト描画のおさらい」をご覧下さい。座標変換済み頂点はこのパイプラインをすっ飛ばして、いきなり2Dの画面内の座標を示すことが出来るのです。

 Direct3Dでは、FVF(Flexible Vertex Format:柔軟な頂点フォーマット)という方法で頂点を定義します。「頂点」はXYZの座標だけでなく、頂点の色、頂点の質感、複数枚のテクスチャの座標などもひっくるめて定義されます。これらの情報の中にその頂点が座標変換済みなのかそうでないのかを指定する部分もあります。しかし、すべての情報を扱うのはとても大変ですし、数千点という頂点を扱う場合などは、ビデオメモリを圧迫してしまいます。そこで、自分が必要とする情報だけをピックアップして頂点を再定義することができます。

 2Dに必要なフォーマットは座標変換済みXYZ座標、頂点の色、テクスチャ座標くらいでしょう(Z座標は入れなければなりません)。まずはこれらをまとめた構造体の定義からスタートします。

struct CUSTOMVERTEX{
float x, y, z; // 頂点座標
float rhw; // 除算数
DWORD dwColor; // 頂点の色
float u, v; // テクスチャ座標
};


FVFにおいて、上記の変数の順序とその型は厳密に決まっています。これをあべこべにするとまったく動きませんので注意が必要です。詳しくはマニュアルの「頂点フォーマット」を参照してください。除算数rhwはパイプラインの最後に行うスケール変換の値で、これが構造体の中にあると「頂点が変換済みである」と判断されますので、必ず入れる必要があります。

 次に、DirectXにどの頂点上を使っているのかを知らせるための頂点情報フラグを設定します。

#define FVF_CUSTOM ( D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1 )

これは後で何度か使用します。フラグの種類についてはこれ以外にも沢山ありますので、興味のある方はマニュアルの「D3DFVF」をご覧下さい。今やろうとしている2D用の頂点について、基礎設定は以上でおしまいです。



A 板ポリゴンの生成

 続いて独自に定義したCUSTOMVERTEX構造体を4つ用いて板ポリゴンを生成します。普通に考えると長方形に並べれば良いと考えてしまうのですが、実はDirect3Dは三角形までしか扱うことが出来ません(点と直線は可能)。よって、四角い板は2枚の三角形の板を張り合わせて作成することになります。もう少し詳しく言えば、板ポリゴンの頂点座標の指定の仕方は「プリミティブの定義」によって変わってきます。プリミティブの定義とはDirectXがサポートしている頂点の並べ方の規則の事です。これはD3DPRIMITIVETYPE列挙型に明確に定義されています。板ポリゴンを作成する時には、この中の「D3DPT_TRIANGLESTRIP」という型を用いるのが楽です。

D3DPT_TRIANGLESTRIPは三角形ストリップという形式で点を並べます。以下の図をご覧下さい。


v1〜v5が頂点です。三角形ストリップは最初の三角形は3つの頂点で定義しますが、それにつながる三角形は1つの頂点で定義していきます。上の図で言うと、最初の1ポリゴンは(v1,v2,v3)ですが、次のポリゴンはすでに定義済みのv2、v3にv4を1つ加えて作成されています。これはちょうど赤い線で示したように、頂点をジグザグに定義していくことになります。このようにすると、細長く連なった長い帯を効率よく作ることが出来るわけです(ちなみに「ストリップ(strip)」というのは「帯状の物」という意味です)。

 気をつけなければならないのが、頂点の定義順番です。デフォルトの状態で、Direct3Dは「反時計回りのポリゴンの背面を描かない」というカリング方式を取っています。これは視点(カメラ)から見て反時計回りの頂点で構成されるポリゴンは「見える」と判断して描画するという意味です。なんだかあべこべになりそうで混乱してしまうのですが、これを間違うとポリゴンが裏返り、画面に出現せずに焦ります(笑)。法線の向きもこの頂点の反時計回りを基本に決まりますので、これはポリゴンを語る上で重要な概念となります。注意が必要です。

板ポリゴンは上のv1〜v4までで出来る事がわかります。その座標を配列に格納します。

CUSTOMVERTEX v[4]=
{
{1.0f, 0.0f, 0.0f, 1.0f, 0xffffffff, 0.0f, 0.0f},
{1.0f, 1.0f, 0.0f, 1.0f, 0xffffffff, 0.0f, 1.0f},
{0.0f, 0.0f, 0.0f, 1.0f, 0xffffffff, 1.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 1.0f, 0xffffffff, 1.0f, 1.0f}
};


CUSTOMVERTEXに定義したメンバの順番に合わせて配列の要素を入れています。太文字部分が頂点座標で、右下→右上→左下→左上という三角形ストリップの並び順になっていますね。



B 頂点バッファ

 次にこの配列の内容を書き込む「頂点バッファ」を作成します。頂点バッファとは、Direct3Dが扱う事の出来る頂点情報を格納したメモリ領域の事で、これは通常VRAM上に作成されます。先ほどの情報はシステムメモリにありまして、これをVRAMにコピーしなければちゃんとDirectXが扱ってくれないわけです。頂点バッファ格納するVRAM領域を確保する関数はIDirect3DDevice9::CreateVertexBuffer関数です。

HRESULT CreateVertexBuffer{
UINT Length,
DWORD Usage,
DWORD FVF,
D3DPOOL Pool,
IDirect3DVertexBuffer9** ppVertex,
HANDLE* pHandle
};


Lengthは確保する頂点バッファのサイズです。今回はCUSTOMVERTEX型の頂点が4つある板ポリゴンなので、この値はsizeof(CUSTOMVERTEX)*4となります。
Usageはリソースの使用法を定義するフラグです。頂点バッファをどう扱うかという事なんですが、ここではバッファに書き込みしたいのでD3DUSAGE_WRITEONLYを設定しておけばOKです。
FVFは柔軟な頂点フォーマットの型を指定する引数で、最初にマクロ定義したFVF_CUSTOMVERTEXマクロ定数を設定します。もちろん頂点フォーマットのフラグを直接書き込んでもかまいませんが、マクロで指定した方が人に優しいです(笑)
Poolは頂点バッファをどの種類のメモリに置くかをD3DPOOL列挙型で設定します。メモリには「システムメモリ」と「VRAM」がありまして、VRAMはビデオボードが直接扱えるメモリなので高速です。D3DPOOL_MANAGEDに設定すると、適切なメモリ管理をしてくれます。D3DPOOL_DEFAULTという設定もあります。これも同様に適切なメモリに頂点バッファを作成してくれますが、実はD3DPOOL_MANAGEDを使った時よりも扱いが面倒になります。
ppVertexには作成された頂点バッファコンポーネントへのポインタのアドレスが返ります。このポインタを通して頂点バッファへ実際に書き込んだりDirect3Dに描画を依頼したり出来ます。ポリゴン情報のすべてはここに集約されているわけです!
pHandleは現在使用されていません。NULLに設定します。

実装例は次のようになります。

HRESULT hr;
IDirect3DVertexBuffer9* pVertex;
hr = pD3dDev9->CreateVertexBuffer(
    sizeof(CUSTOMVERTEX)*4,
    D3DUSAGE_WRITEONLY,
    FVF_CUSTOM,
    D3DPOOL_MANAGED,
    &pVertex,
    NULL
);


関数がうまくいけばhrにD3D_OKが返ります。エラーが生じた場合、システムの機能としてD3DPOOL_MANAGEの設定が出来ないか、メモリが本当に無い可能性があります。

 これでpVertexの先に頂点バッファを扱うコンポーネントが生成され、VRAM上に出来た空の頂点バッファをインターフェイスで管理できます。後は頂点バッファに先ほど設定した板ポリゴンの頂点配列をコピーするだけです。ただ、VRAMは通常ユーザがおいそれと触れないメモリなので、読み書きは厳重になっています。
 頂点バッファに書き込むためには、まずそのメモリを「ロック」しなければなりません。ロックというのは読み書きする時に他の処理がそれを邪魔しないように作業を占有することです。これにより確実な読み書きが保障されます。メモリのロックは先ほど生成したIDirect3DVertexBuffer9オブジェクトのLock関数を使います。

HRESULT Lock(
UINT OffsetToLock,
UINT SizeToLock,
VOID** ppbData,
DWORD Flags
);

OffsetToLockSizeToLockはロックするメモリの範囲を指定します。全体をロックするのであれば、OffsetToLockに0、SizeToLockにロックしたいメモリサイズを割り当てます。
ppbDataはロックされたメモリ範囲の先頭アドレスへのポインタが返ります。ここへの書き込みが許可されるわけです。
Flagsはロック時の動作を定義するオプションフラグで0でも動作します。フラグ設定時の動作は複雑なので、必要な方はマニュアルのD3DLOCKを参照して、適切に設定して下さい。

 関数が成功すればppbDataが指す先には指定した大きさのメモリがありますから、そこに頂点配列を間違いなく上書きすれば、頂点バッファへの書き込みは終了します。書き込みが終わったあとは必ず「アンロック」しなければなりません。アンロックはロック解除の事です。アンロックをしなければ誰もVRAMメモリの読み書きができなくなるので、以後の描画はすべて失敗してしまいます。

実装は次の通り。

void *pData;
hr = pVertex->Lock(0, sizeof(CUSTOMVERTEX)*4, (void**)&pData, 0);

if(hr == D3D_OK){
   memcpy(pData, v, sizeof(CUSTOMVERTEX)*4);
   pVertex->Unlock();
}

memcpyを使って頂点を一気にコピーします。最後にアンロックを忘れずに。

 これで頂点変換済み2D板ポリゴンの頂点をVRAMにセットできました。残りの描画部分を一気にやってしまいます。



C Direct3Dの描画

 通常、Direct3Dの描画は大変難しい部分の1つです。これを様々に駆使して巷の素晴らしいエフェクトの効いたゲームが出来上がっているんです。しかし、座標定義済み頂点に関して言えば、「頂点変換」という面倒な部分がごっそりと省略されているので、描画は比較的簡単です。

 どのようなDirect3Dのオブジェクトも、描画はIDirect3DDevice9インターフェイスのBeginScene関数を呼び出した後に行います。そして、すべての描画が終了したら必ずEndScene関数で描画終了宣言をします。実際の描画はIDirect3DDevice9インターフェイスのSetStreamSourceSetFVFそしてDrawPrimitive関数の3点セットを使います。SetStreamSource関数は、「ストリームソース」という頂点処理の流れに頂点バッファを実際にセットする関数です。SetFVF関数は、頂点バッファの型をストリームに教えます。そしてDrawPrimitive関数で、頂点の結び方とポリゴンの描画枚数を指定します。

HRESULT IDirect3DDevice9::SetStreamSource(
UINT StreamNumber,
IDirect3DVertexBuffer9* pStreamData,
UINT OffsetByte,
UINT Stride
);


StreamNumberとは訳して流れの番号、つまりストリームの番号という物を指します。今はここに0を入れおいて下さい。これについては非常に複雑な話になりますので、ここで詳しくは説明しません。興味のある方はマニュアルの「プログラマブルなストリームモデル」を参照してください。ほぉ〜っと思う事が書いてあります。
pStreamDataは先ほど生成した頂点バッファコンポーネントへのポインタを渡します。
OffetByteは頂点データの書き始めを示すオフセットです。通常は0ですが、例えば特別なヘッダーが書き込まれている頂点データのような物があった場合に、ここで頂点の先頭に合わせたりします。
Strideは1頂点のサイズを指定します。今回の場合はsizeof(CUSTOMVERTEX)ですね。

IDirect3DDevice9::SetFVF関数へはマクロ定数で定義したFVF_CUSTOMVERTEXを渡します。これでDirectXは頂点バッファでどんな頂点情報がしていされているかを判断します。

DrawPrimitve関数は次の通りです。

HRESULT IDirect3DDevice9::DrawPrimitive(
D3DPRIMITIVETYPE PrimitiveType,
UINT StartVertex,
UINT PrimitiveCount
);


PrimitiveTypeは頂点をどう結んでポリゴンにするかをDirect3Dで決められているフラグで指定します。D3DPRIMITIVETYPE列挙型には幾つかありますが、今回の板ポリゴンは「三角形ストリップ」で作成するのでD3DPT_TRIANGLESTRIPを指定します。
StartVertexは描画を開始する最初の頂点番号で、普通は一番最初の頂点である0にします。
PrimitiveCountは描画するポリゴンの数を指定します。板ポリゴンは2枚の三角形ポリゴンから出来ていますから、ここは2です。

実装例を示します。

pD3dDev9->BeginScene();   // 描画開始
  pD3dDev9->SetStreamSource(0, pVertex, 0, sizeof(CUSTOMVERTEX));
  pD3dDev9->SetFVF(FVF_CUSTOM);
  pD3dDev9->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
pD3dDev9->EndScene();   // 描画終了


 この段階ではバックバッファ(正確には現在指定されているサーフェイス)に描画が行われます。描画後、

pD3dDev9->Present(NULL, NULL, NULL, NULL);

Present関数を用いると、バックバッファとフロントバッファが交代(フリップ)し、実際に画面に絵が出現します。先ほど設定した板ポリゴンは縦横ともに1ピクセルの小さな正方形です。頂点の位置を変えてもっと大きな板にすれば、目に見える大きさの白いポリゴンが出現します。



 板ポリゴンを画面に表示するだけなんですが、結構面倒な事をしてきました。でも、頂点フォーマットを決めて、頂点バッファを作成し、デバイスにそれを設定してバックバッファに描画する、というこの流れはDirect3Dの描画の基本中の基本になります。これが出来れば、3Dオブジェクトを表示させるのが大分楽になります。
 ここでの設定を元にして頂点変換済み2D板ポリゴンを作成するクラスを作成してしまえば、これらの作業はもう考えなくて良くなります。クラス化する事によって、板ポリゴンの作成が非常に簡単になり、あっという間に沢山の絵を画面に出す事ができるようになります。そうなると、ゲーム作りの土台が出来たも同然です。

 とは言うものの、普通板ポリゴンにはテクスチャを貼り付けます。このテクスチャをどう貼るかが、ゲームの見た目の質や演出の良し悪しを左右してしまいます。テクスチャの貼り方については後の章で説明していきます。