ホーム < ゲームつくろー! < DirectX技術編 < 気になる頂点インデックスの意義

その30 気になる頂点インデックスの意義


 Direct3Dのチュートリアルで、私たちは頂点を定義して描画する基本を学びます。しかし、複雑な図形になると面倒なので、今度はXファイルを使うようになります。Xファイルの扱いも本来は面倒なものですが、ありがたいことにDirect3DExtention(Direct3DX)にあるD3DXLoadMeshFromX関数等によってXファイルを読み込んでID3DXMeshインターフェイスを取得し、ID3DXMesh::DrawSubset関数であっさり描画ができてしまいます。

 ところが、Direct3Dには「頂点インデックス」なるものがあり、どうやらこれでも描画が出来るらしい。そんなことにそのうち気が付きます。気が付くんですけど、色々疑問が出るんです。「頂点インデックスという物自体が良くわからない」「頂点だけで描画できるのになんでこれがあるの?」など。しかし、頂点インデックスは私たちが知らずに使っているものなんです!

 この章では頂点インデックスがなぜあるのか、そしてそれをどう扱うのかについて見ていくことにしましょう。


@ 頂点インデックスはどうして必要か?

 いつものように言葉から見てみましょう。インデックス(index)というのは「索引」という意味です。本などの後ろについていますね。大辞泉によりますと、索引とは「ある書物の中の語句や事項などを、容易に探し出せるように抽出して一定の順序に配列し、その所在を示した表。インデックス」とあります。わかりやすいですね。つまり、頂点インデックスを大辞泉の言葉を借りて説明するなら、「ある頂点を簡単に探し出せるように一定の順序に並べてその場所を示した表(配列)」と言うことになります。ただ、実際はちょっとだけニュアンスが異なります。頂点インデックスをずばっと分かりやすく言うと「ポリゴンを作るための頂点結び順配列」です。

 ポリゴンを作るためには、頂点の座標等の情報が必要です。これが無いと当然描画もできません。しかし、私たちが良く知っているように、頂点情報さえあれば描画はできるのです。では、なぜ「頂点インデックス」なる物が必要なのでしょうか。当初私もその理由は良くわかりませんでした。

 頂点インデックスの存在理由はズバリ「ポリゴンメッシュを作るため」です。ポリゴンメッシュというのは、ポリゴンが沢山組み合わせられた大きなオブジェクトを指します。これを効率良く作るためにどうしても必要なんです。私たちが最初にチュートリアルで覚えた描画図形は「インデックス無しプリミティブ」と呼ばれています(プリミティブとは「根源の」という意味で、ここでは基本図形の意)。これは、ポリゴンの描き方を頂点の配列順序を工夫することで表したものです。三角形ストリップとか三角形リストと言う特別な並べ方をすることで、図形を表現していたわけです。ところが、この方法には「頂点の重複」という悩ましい問題が含まれています。

 三角形リストを例に見てみます。三角形リストで正方形を作るには、2枚の三角形を貼りあわせます。

 上の図は見やすいようにわざと隙間を空けています。この2枚のポリゴンを正方形ポリゴンとしてみなすには、頂点番号1と4及び3と5を「いつも同じ値」にしなければなりません。これは絶対です。頂点を動かす時に毎回同じ値にするわけです。上の正方形ならまだマシですが、この正方形がずらずらと沢山並んでいる床などの場合、同じ値だらけになってしまいます。

 これは無駄という面もありますが、何よりも「頂点を動かせない」のが痛いんです。上の床で頂点番号7番を動かそうと思ったら、6、10、15、17、20番も同時に動かすのです!こんなの、やってられません(笑)。

 上図の床で、重なった頂点を全部一まとめにすると、9箇所の異なる頂点座標だけになります。つまり、実際に動かすのはその9つの頂点で必要十分なわけです。であるならば、頂点の位置を決めた後にポリゴンと言う皮を順序どおりに貼れば、頂点をどう動かそうとも間違いなく隙間の無い床が作れます。これなら、大変合理的で効率的だと思いませんか?そう、このポリゴンの貼り方を記述したのが「頂点インデックス」に相当するわけです!!

 頂点の座標とポリゴンの作り方を分離することで、どのように複雑に作成されたポリゴンオブジェクトも間違いなくみっちり隙間無く作れます。いくら頂点を動かしても大丈夫です。頂点インデックスの最大の意義は、ここにあるわけです。

 頂点情報に合わせて頂点インデックスも定義されている図形を「インデックス付きプリミティブ」と言います。この場合、頂点情報の並びに意味はありません。単なる頂点の情報が格納された配列になります。三角形ポリゴンを形成する頂点の並びは頂点インデックスが変わりに請け負います。

 正方形を例にしますと、頂点は4つです。重複はありません。頂点インデックスバッファ(配列です)の中には、1つのポリゴンを作るための頂点の結び順に頂点番号が格納されます。非常に単純な話でして、インデックスIB[0]、IB[1]、IB[2]で1つ目の緑のポリゴン、IB[3]、IB[4]、IB[5]で2つ目のピンクのポリゴンが形成されるわけです。同じ頂点で裏表のあるポリゴンを作るのも簡単です。4枚のポリゴンを形成するように頂点インデックスバッファで連結すれば良いだけなんです。このように、頂点インデックスを用いると、最適化された頂点でポリゴンオブジェクトを作ることができます。

 インデックス付きプリミティブには1つ欠点があります。それは、メモリをより多く使用してしまうんです。頂点バッファだけでなく、三角ポリゴンを形成する頂点番号も全部記述するのですから、単純計算で[ポリゴンの数]×3だけメモリを余計に圧迫します(三角形リスト形式の場合)。これは、かなりのメモリサイズになります。そこで、頂点インデックスは通常16bit(2byte)で記述されます。これだと65536個の頂点しか扱えませんが、多少メモリの節約になります。
 実際は、先ほどの例で見てきたように、インデックス無しだと頂点の重複が相当に起こってしまいますので、メモリ効率が言いか悪いかは微妙です(1つの頂点を表す構造体は大きいですから)。



A インデックス付きプリミティブの描画までの流れ

 インデックス付きプリミティブは「頂点バッファ」と「インデックスバッファ」の2つのバッファ(配列)を用いてポリゴンを描画します。その流れを見てみることにしましょう。

 何は無くとも頂点情報は必要です。これはインデックス無しプリミティブと全く一緒でして、頂点バッファ(IDirect3DVertexBufferインターフェイス)を作成して、そこに必要な頂点を書き込んでいきます。このとき、煩わしかった頂点の描画順序は無視してください。単純にポリゴンオブジェクトを形成するのに必要な頂点を登録していきます。
 頂点の結び順配列である頂点インデックスバッファは、IDirect3DIndexBuffer9インターフェイスによって管理されます。このインターフェイスを取得するには、IDirect3DDevice9::CreateIndexBuffer関数を用います。この関数は、次のように定義されています。

HRESULT IDirect3DDevice9::CreateIndexBuffer(
    UINT Length,
    DWORD Usage,
    D3DFORMAT Format,
    D3DPOOL Pool,
    IDirect3DIndexBuffer9** ppIndexBuffer,
    HANDLE* pHandle
);

Lengthは頂点インデックスバッファのサイズをバイト単位で指定します。この計算は簡単です。例えば頂点が4つあると2枚の三角ポリゴンが描けますよね(片面だけです)。この時は6つのインデックスが必要です(ポリゴン数×3頂点)。頂点が5つだとポリゴンの数は3枚なので9つ。頂点が6つだと4ポリゴンで12インデックス…と考えていくと、頂点インデックスの配列数の一般式は[頂点の数-2]×3となります。1つの頂点インデックスは通常2バイトで表しますから、Lengthには[頂点の数-2]×3×2バイトとなるわけです(注:これは三角形リスト形式の場合です)。
Usageは頂点インデックスバッファをどのように使用するか定めます。動的な使用(後で書き換えるなど)する場合はD3DUSAGE_DYNAMICを指定しますが、デバイスがサポートしていなければ0を指定します。
Formatは1つのインデックスバッファのサイズをフラグで示します。もし2バイト(16bit)で表現するならばD3DFMT_INDEX16、もっと多くの頂点インデックスを使いたくて4バイト(32bit)で表すのであればD3DFMT_INDEX32を指定します。これ以外のフラグはありません。
Poolは頂点インデックスをどのメモリに置くかを指定します。大抵はD3DPOOL_MANAGEDを指定すれば問題ありません。D3DPOOL_DEFAULTでも良いのですが、デバイスが消失した時の処理を書く必要があります。
ppIndexBufferにIDirect3DIndexBuffer9インターフェイスが返ります。
pHandleは現在使用されていないのでNULLを指定して下さい。

 これで空のインデックスバッファが出来上がります。次に、インデックスバッファに頂点の番号(頂点バッファの配列要素番号)を格納していきます。この時、三角形リストであれば、上から3つごとで1つのポリゴンを表すことになります。インデックスバッファで唯一面倒なのがこの作業です。しかし、平面的な床などの単純な図形であれば、規則正しいアルゴリズムが組み立てられます。頂点インデックスバッファに値を書き込むには、頂点バッファの時と同様にIDirect3DIndexBuffer9::Lock関数を用います。

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

OffsetToLockはロックしたい位置をバイト単位で指定します。もし頂点インデックスバッファ全体をロックしたいのであれば、ここと次のSizeToLockを0に設定します。
SizeToLockはロックするサイズをバイト単位で指定します。一部分だけロックしたい場合にこの引数を用います。
ppbDataに指定した頂点インデックスバッファへのポインタが返ります。
Flagsにはロック目的をフラグで示します。大抵は書き込みのためにロックするので、ここにはD3DLOCK_DISCARDを指定します。0でもかまいません(制約無くロックする意味になります)。

 ppbDataに有効なポインタを取得できれば、後は通常の配列処理となんら変わりません。書き込みが終わったらIDirect3DIndexBuffer::Unlock関数を呼び出してロックを解除します。

HRESULT IDirect3DIndexBuffer::Unlock();

 頂点バッファと頂点インデックスバッファを作成し終われば、後はもう描画するだけです。描画手順は、まず頂点をストリームに登録し、その頂点フォーマットを設定します。

d3dDevice->SetStreamSource( 0, VB, sizeof(CUSTOMVERTEX) );
d3dDevice->SetFVF( CUSTOMVERTEX );

これはインデックス無しプリミティブの描画と一緒ですね。次からがちょっと異なり,
インデックス番号をデバイスに設定します。

d3dDevice->SetIndices( IB );

これにより、デバイスは頂点と頂点インデックスを結びつけるわけです。とっても重要な関数です。インデックス付きプリミティブを実際に描画するためにはIDirect3DDevice9::DrawIndexedPrimitive関数を用います。DrawPrimitive関数とは違いますので注意してください。

HRESULT IDirect3DDevice9::DrawIndexedPrimitive(
   D3DPRIMITIVETYPE Type,
   INT  BaseVertexIndex,
   UINT MinIndex,
   UINT NumVertices,
   UINT StartIndex,
   UINT PrimitiveCount
);

TypeにはD3DPRIMITIVETYPEで指定される「頂点の繋ぎ方」をフラグで指定します。三角形ストリップや三角形ファンなどをやっぱり指定するわけです。大きなメッシュになると、ここは通常D3DPT_TRIANGLELIST(三角形リスト)になるでしょうね。
BaseVertexIndexは頂点インデックスの一番最初までのオフセット値を指定します。頂点インデックスの先頭にヘッダーを入れてある場合など、この値を調節して頂点インデックスの最初になるようにオフセットするわけです。バッファの最初からインデックスがあるのならば、ここは0になります。
MinIndexは描画に使用する最小のインデックス番号を指定します。10000番から描画しだそうというのに、0番を検索するのは無駄なわけで、そういう無駄を省くための値です。多少無駄でもいいやと思うなら0にしてかまいません。
NumVerticesはMinIndex以降の頂点の数を指定します。MinIndexを変更しているとちょっとややこしいことにはなりますが、与えるのは通常全部の頂点の数-MinIndexです。
StartIndexは描画を開始する頂点インデックスまでのオフセット値を指定します。この位置から実際に描画を始めますから、この値は重要です。これは、途中から描画したい時などに使います。これと次のPrimitiveCountをうまく駆使すると、1つのメッシュに含まれるポリゴンを分けて描画できるようになります。
PrimitiveCountはStartIndexを先頭として描画するポリゴンの数を指定します。ポリゴンの数です。頂点の数ではありませんからご注意下さい。

 ここに適切な値を入れることで、実際の描画が行われます。

 インデックス付きプリミティブの描画の流れは以上です。いったんイメージができてしまえば、描画までのプロセスは大して難しいものではありません。



B Xファイルはインデックス付きプリミティブです

 3Dモデリングツールで作成されるXファイルは、まず間違いなくインデックス付きプリミティブとして設定されています。実際にXファイル内を見てみましょう。

Mesh Mesh_Plane1 {
   6;          // 頂点の数
   -1.000000;0.000000;0.000000;,          // 頂点の座標
   -1.000000;1.500000;0.000000;,
   -1.000000;3.000000;0.000000;,
   1.000000;3.000000;0.000000;,
   1.000000;1.500000;0.000000;,
   1.000000;0.000000;0.000000;;
   4;          // ポリゴン数
   3;1,3,2;,   // 頂点の繋がり ← これが頂点インデックス!
   3;0,5,4;,
   3;1,4,3;,
   3;0,4,1;;

 単純な板ポリゴンのXファイルの一部です。これはLightWaveで作成したいたポリゴンをXファイルにエクスポートしました。頂点が宣言された後に、頂点の繋がりが記述されています。このことから、ID3DXMeshインターフェイスは、インデックスを利用して描画しているだろう事が容易に想像できます。それを、ユーザの目からうまく隠蔽しているわけです。実際にID3DXMeshインターフェイスのメンバ関数を見ると、インデックス関連の関数が沢山あります。これら関数を用いれば、DrawSubset関数も用いなくても描画ができてしまいます(DrawSubset関数を用いた方がはるかに楽ですが)。ようやく、描画回りがすっきりした気が、私はしています(^-^)


 頂点インデックスは複雑なポリゴンオブジェクトを作成するだけでなく、頂点を直接動かすアニメーションで絶大な威力を発揮します。それというのも、頂点の座標とポリゴン生成を分離してくれているお陰です。これまで気になっていた方も、オブジェクトを作る時に、頂点インデックスを使うか使わないかの判断が出来るようになったのではないでしょうか。うまく活用したいものです。