ホーム < ゲームつくろー! < DirectX技術編
その53 StreamSourceを複数使って描画してみよう
メッシュを描画する時に便利なID3DXMeshを用いずにプリミティブやモデルを描画する時には、次のようなお決まりの流れを用います:
通常描画 pDev->SetStreamSource( 0, pVertexBuffer, sizeof(CUSTOMVERTEX) );
pDev->SetFVF( CUSTOMVERTEX_FVF );
pDev->DrawPrimitive( D3DPT_TRIANGLELIST, 0, n );
これは「柔軟な頂点フォーマット」を用いたDirectX9の典型的な描画方法です。チュートリアルに載っている方法なので誰でも最初は右にならえ描画をこうだと学びます。ところで、上のソースの殆どは理解できますが、きっと1つだけ微妙に良く分からない部分があるはずなんです。最初の行にある「SetStreamSouceメソッド」。これ何だろうと思った事はありませんでしょうか?私はそうでした(^-^;。しかも、マニュアルを読んでも良く分からない・・・。
いったいSetStreamSourceって何をしているのでしょうか?というより「ストリームソース」って何なのでしょうか?この章ではその辺りの疑問を解きほぐし、「頂点情報をいったんばらばらに登録して結合して描画する」というステップアップした描画方法を見ていくことにしましょう。
@ SetStreamSourceメソッドのマニュアルを見る
何はともかくSetStreamSourceメソッドのマニュアルを見てみます:
SetStreamSourceメソッドの説明(DirectX9マニュアル日本語版) 頂点バッファをデバイスのデータストリームにバインドする。詳細については、「ストリームソースの設定」を参照すること。
この言葉にやられるんですよね。「データストリームにバインドする」・・・名詞がさっぱりわかりません(^-^;。そこで、マニュアルが指示している「ストリームソースの設定」を見てみます:
データストリームの設定(DirectX9マニュアル日本語版) IDirect3DDevice9::SetStreamSource メソッドは、頂点バッファをデバイスデータストリームにバインドし、頂点データと、プリミティブ処理関数にデータを供給するデータストリームポートの1つとを関連付ける。ストリームデータへの実際の参照は、IDirect3DDevice9::DrawPrimitiveなどの描画メソッドが呼び出されるまでは発生しない。
ストリームは成分データの一様な配列として定義されており、各成分は、位置・法線・色などの単一エンティティを表す1つ以上の要素で構成されている。Strideパラメータは、成分のサイズをバイト単位で指定する。
「データストリームポート」というのがプリミティブ処理関数に頂点データを供給するとあります。どうやらデータストリームというのは頂点情報を描画関数を流し込む管のような物のようです。そしてSetStreamSourceメソッドはデータストリームの入り口に頂点データをセットする役目をしていそうです。
そこで、SetStreamSourceメソッドの定義について確認してみます:
SetStreamSourceメソッド HRESULT SetStreamSource(
UINT StreamNumber,
IDirect3DVertexBuffer9 *pStreamData,
UINT OffsetInBytes,
UINT Stride
);
StreamNumberはデータストリームをIDで指定します。これは0から最大数-1までとあります。
pStreamDataは頂点バッファへのポインタです。
OffsetInBytesはストリームの先頭から頂点データの先頭までのオフセットを指定します。これは頂点バッファに複数のモデルを格納している場合などに重宝します。
Strideは1つの頂点のサイズを指定します。プリミティブ関数はこのストライドを用いて頂点をGPUに流すわけです。
StreamNumberは言ってみればデータを送る管の番号です。ここからデータストリームが複数ある事もわかりました。でも、チュートリアルにある方法だと0番のデータストリームしか使っていませんし、そもそも複数の管に頂点データをセットするというのはどういう事なのでしょうか?それについて次節で説明します。
A データストリームの真意
データストリームが複数ある事の真意について直接答えてくれるのはマニュアルの「プログラマブルなストリームモデル」です(検索してみて下さい)。次の記述が真意を物語っています:
ストリームの真意 ・ 頂点はn個のストリームで構成される。
・ ストリームはm個の要素で構成される。
・ 要素は [位置、色、法線、テクスチャ座標] である。
私たちは今まで1つの頂点は「位置、色、法線、テクスチャ座標(UV)」などの複数の要素で構成されていると思っていました。しかし、実はこの要素は分割する事が可能で、その分割単位が「ストリーム」なんです。1つの頂点は「n個のストリーム」で構成され、1つのストリームはm個の要素で構成される。この概念は次のようにイメージできます:
1つの頂点を構成する要素をストリームに分割しておきます。SetStreamSourceメソッドでこれらのストリームを各番号に登録してプリミティブ描画関数を呼ぶと各ストリームの情報が再び1つになって(=バインドされて)頂点シェーダに入力されます。これがストリームの真意です。
このように頂点情報をストリーム単位に分割すると、実はかなり旨みが出てきます。
B 頂点情報を分割する利便性
例えば、ビルボードを表示するためにMYBILLBOARD_VERTEXという頂点を定義したとしましょう:
MYBILLBOARD_VERTEX struct MYBILLBOARD_VERTEX {
float x, y, z; // 座標
float nx, ny, nz; // 法線
float u, v; // テクスチャUV座標
};
位置座標と向きとテクスチャ座標があれば最低限のビルボードを表示可能です。さて、このビルボードに2枚目のテクスチャを貼りたくなりました。しかも1枚目と2枚目とで独立したテクスチャ座標にしたいんです。そこで次のような新しいUV座標を定義した頂点構造体を定義します:
MYBILLBOARD_VERTEX_2TEX struct MYBILLBOARD_VERTEX_2TEX {
float x, y, z; // 座標
float nx, ny, nz; // 法線
float u1, v1; // テクスチャUV座標1
float u2, v2; // テクスチャUV座標2
};
チュートリアルに載っているようなストリームを1つしか使わない描画方法だと、テクスチャ座標を追加するためにわざわざもう1つ構造体を宣言しなければならないわけです。ビルボードに色を追加したい場合はそれがあるバージョンと無いバージョンで・・・と要素を追加する度に構造体が倍々に増えてしまうわけです。
これを解決する1つの方法としては、考えうるすべての要素を盛り込んだ大きな頂点構造体を定義する事です。しかし、頂点座標と法線しか使わなくても良いのに点のサイズとかスペキュラカラーとか不要なデータを頂点バッファに埋め込むのは明らかにメモリを浪費します。大きな構造体ではだめなんです。
そこで、ストリームを複数使う事を考えるわけです。先の頂点構造体をもっと細かく分割します:
ストリーム用の頂点構造体 struct VERTEX_COORD {
float x, y, z; // 座標
float nx, ny, nz; // 法線
};
struct VERTEX_COLOR {
float color; // カラー
};
struct VERTEX_TEXCOORD {
float u, v; // テクスチャ座標
};
この構造体レベルでそれぞれの頂点バッファを作成し、それを個別のストリームに流し込めば自由な組み合わせでポリゴンを描画する事ができます。もしテクスチャUVが3つになったとしてもVERTEX_TEXCOORD型配列で構成された3つ目の頂点バッファを作れば良いだけです。
もうお気付きになられているかもしれませんが、ストリームに分割すると、頂点バッファの構成ががらりとかわってしまいます。これまでのFVFを用いた頂点バッファと、ストリームに分割する頂点バッファの構成の違いを以下に示します:
ご覧のようにFVFの頂点バッファは1つの構造体を1単位として構成されていますが、ストリームに分配する方法だと究極的には1つの要素ごとに独立した配列で頂点バッファを定義できます。これは、例えば外部からメッシュのデータを読み込む時に各要素別に読み込めるため非常に重宝します。もちろん、わざわざFVFの頂点バッファのように組み直す必要はありません。
C 複数のストリームを用いた描画
ストリームの真意が見えてきたところで、複数のストリームを用いた描画について見ていくことにしましょう。例として頂点座標と頂点カラーを個別に扱ってみます。
まず、頂点座標と頂点カラーの構造体を宣言します:
ストリーム用の頂点宣言 struct VERTEX_COORD {
float x, y, z; // 座標
float nx, ny, nz; // 法線
};
struct VERTEX_COLOR {
DWORD color; // カラー
};
続いて各構造体ごとに頂点バッファを作成します:
頂点バッファ作成 VERTEX_COORD coordArray[] = {
{ 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f }, // 0番目
{ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f }, // 1番目
{ 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f } // 2番目
};
VERTEX_COLOR colorArray[] = {
0xffff0000, // 0番目
0xff00ff00, // 1番目
0xff0000ff // 2番目
};
IDirect3DVertexBuffer9 *pCoordBuf, *pColorBuf;
pDev->CreateVertexBuffer( sizeof( VERTEX_COORD ) * 3, 0, D3DFVF_XYZ | D3DFVF_NORMAL, D3DPOOL_MANAGED, &pCoordBuf, 0 );
pDev->CreateVertexBuffer( sizeof( VERTEX_COLOR ) * 3, 0, D3DFVF_COLOR, D3DPOOL_MANAGED, &pColorBuf, 0 );
// 後はバッファにコピーして下さい・・・
頂点座標とディフューズカラーの2つの頂点バッファが作成できました。さて次です。実はストリームを複数使う時にはFVFを用いた便利な描画ができません。IDirect3DDevice9::SetFVFメソッドでストリームを総合した表現ができないためです。そこで「頂点宣言」を用いた方法で描画します。
まず、D3DVERTEXELEMENT9構造体の配列で先ほどの3つの要素を宣言します:
頂点宣言 D3DVERTEXELEMENT9 vertexElem[] = {
{ 1, 0 , D3DDECLTYPE_FLOAT3 , D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 }, // 頂点座標
{ 1, sizeof(float)*3, D3DDECLTYPE_FLOAT3 , D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL , 0 }, // 頂点座標
{ 0, 0 , D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR , 0 }, // ディフューズ
D3DDECL_END()
};
ポイントは最初の数字(D3DVERTEXELEMENT9::Stream)です。ここにストリームのIDを指定します。上の場合、頂点座標と法線はストリーム1に、カラーはストリーム0に渡す事にしています。そして、続くsizeofの部分(D3DVERTEXELEMENT9::Offset)。ここには各データへのオフセット値を指定します。法線は頂点座標の次に並んでいる事にしているためsizeof(float)*3とオフセット値を計算しています。
この構造体の配列から次にIDirect3DVertexDeclarationオブジェクトを作成します:
頂点宣言オブジェクト生成 IDirect3DVertexDeclaration9 *pvertexDec;
pDev->CreateVertexDeclaration( vertexElem, &pvertexDec );
第1引数にD3DVERTEXELEMENT9配列を渡すと、第2引数にオブジェクトが返ります。これで、頂点座標と法線とカラーを持った頂点宣言が完了です。
描画時に、まず頂点宣言オブジェクトをデバイスにセットし、ストリーム0にはカラーバッファを、ストリーム1には頂点座標・法線バッファをそれぞれ教えてあげます:
ストリームに設定 pDev->SetVertexDeclaration( pvertexDec ); // 頂点宣言を通知
pDev->SetStreamSource( 1, pCoordBuf, 0, sizeof( VERTEX_COORD ) );
pDev->SetStreamSource( 0, pColorBuf, 0, sizeof( VERTEX_COLOR ) );
謎だったSetStreamsourceメソッドの第1引数を触る時が来ました(^-^)。もう意味は十分にお分かりになったと思います。
この後はSetFVFメソッドを呼ぶ必要はありません(使っていないので)。よって、描画関数を読んで終了です:
描画関数呼び出しで終了〜 gDev->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 1 );
もし頂点カラーを無視したい時は、ストリームをクリアするだけです:
ストリームをクリア pDev->SetStreamSource( 0, NULL, 0, 0 );
他のメッシュを描画する時にも必要の無いストリームはクリアしておきます。
ちなみに、インデックスを用いて描画する時も頂点の数を揃えておけばほぼ同様です。
D ストリームの数
ストリームの数は有限で、デバイス能力によって変わります。ストリーム数を調べるにはIDirect3DDevice9::GetDeviceCapsメソッドで得られるD3DCAPS9構造体のMaxStreamsメンバを見ます:
ストリーム数を取得 D3DCAPS9 Caps;
pDev->GetDeviceCaps( &Caps );
int MaxStreamNum = Caps.MaxStreams;
DirectX9では最大16個のストリームが使えます。私の大分に古い(ピクセルシェーダが無いほど)ノートパソコンでも16個使えるので、よっぽどで無い限りはマックスで使えるのかなと思います。16個あれば、頂点要素を相当に分割してしまっても問題ありませんね。
ストリームを複数用いて描画する事で、本当にフレキシブルに頂点情報を扱う事ができるようになります。モデルの読み込みなどもシンプルになりますし、必要な情報だけを描画関数に流せるのでメモリの節約にも一役買います。そろそろFVFから卒業したいなと思う方はぜひ取り込んでみて下さい。