DrawPrimitive周り

「DrawPrimitive系は遅いので呼び出し回数を減らせ」という言葉をよく耳にする。

DrawPrimitive系とは言わずもがなDirectXの代表的な描画関数で、頂点データから線やポリゴンを描画する。系の仲間として DrawPrimitive / DrawIndexedPrimitive / DrawPrimitiveUP / DrawIndexedPrimitiveUP がある。

Indexedが付いてる方はインデックスバッファを用いるもの。同じ頂点を共有するポリゴンを描くときに頂点バッファ内で同じデータが重複するのはムダなので、使うデータをインデックスバッファで指定する。

UPが付いていない方は頂点バッファとインデックスバッファをグラフィックスカードが管理する領域に置き、UPが付いている方はアプリケーション側で確保したものをそのまま使う。

DrawPrimitiveを使って板ポリをいっぱい描画するようなプログラムを何も理解せずに書くと次のような感じになる。

struct CUSTOMVERTEX {
	float x,y,z;
	DWORD dwColor;
	float u1,v1;
};

struct DRAWINFO {
	LPDIRECT3DVERTEXBUFFER9		pVB_;		// 頂点バッファ(設定済み)
	LPDIRECT3DVERTEXDECLARATION9	pVD_;		// 頂点宣言
	LPDIRECT3DTEXTURE9		pTexture_;	// テクスチャ
	D3DXMATRIX			worldMatrix_;	// 変換行列
};

LPDIRECT3DDEVICE9 pDevice;
CEffect* pEffect;		// ID3DXEffectのラッパーだと思ってくれ
D3DXMATRIX view, proj;
vector<DRAWINFO> drawList;

pEffect->Begin();
pEffect->SetViewMatrix(&view);
pEffect->SetProjMatrix(&proj);
for(int i=0; i<drawList.size(); i++){
	pEffect->SetTexture(drawList[i].pTexture_);
	pEffect->SetWorldMatrix(&(drawList[i].worldMatrix_));
	pDevice->SetStreamSource(0, drawList[i].pVB_, 0, sizeof(CUSTOMVERTEX));
	pDevice->SetVertexDeclaration(drawList[i].pVD_);
	
	pEffect->BeginPass();
	pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
	pEffect->EndPass();
}
pEffect->End();

ご覧のとおり、板ポリの数だけDrawPrimitiveが呼ばれている。
よくわかっていないのだが、DrawPrimitiveは溜まっているプリミティブデータの描画開始を合図する関数なので、1回呼んでしまうと帰ってくるまで次の描画が開始できない。なのでポリゴン1枚とかの少ないデータを描画するだけで1回呼んでしまうとオーバーヘッドが大きすぎて遅くなるようだ。

そこで複数のポリゴンデータを1つの頂点バッファにまとめてしまい、1回のDrawPrimitiveですませる。

struct DRAWINFO {
	CUSTOMVERTEX	pVertex_[4];
	D3DXMATRIX	worldMatrix_;
};

CUSTOMVERTEX			pAllVertex[VERTEX_MAX];	// まとめて転送するための元バッファ
LPDIRECT3DVERTEXBUFFER9		pVB;
LPDIRECT3DVERTEXDECLARATION9	pVD;
LPDIRECT3DTEXTURE9		pTexture;

for(int i=0; i<drawList.size(); i++){
	// しょうがないので自前で座標変換しておく
	CUSTOMVERTEX* p = drawList[i].pVertex_;
	D3DXVECTOR4 pos[] = {
		D3DXVECTOR4(p[0].x, p[0].y, p[0].z, 1.0f),
		D3DXVECTOR4(p[1].x, p[1].y, p[1].z, 1.0f),
		D3DXVECTOR4(p[2].x, p[2].y, p[2].z, 1.0f),
		D3DXVECTOR4(p[3].x, p[3].y, p[3].z, 1.0f)
	};
	
	D3DXVECTOR4 trans[4];
	for(int j=0; j<4; j++){
		D3DXVec4Transform(&(trans[j]), &(pos[j]), &(drawList[i].worldMatrix_));
	}
	
	// 三角形リストの頂点順にバッファを構成
	CUSTOMVERTEX tempVertex[] = {
		{ trans[0].x, trans[0].y, trans[0].z, p[0].dwColor, p[0].u1, p[0].v1 },
		{ trans[1].x, trans[1].y, trans[1].z, p[1].dwColor, p[1].u1, p[1].v1 },
		{ trans[2].x, trans[2].y, trans[2].z, p[2].dwColor, p[2].u1, p[2].v1 },
		{ trans[3].x, trans[3].y, trans[3].z, p[3].dwColor, p[3].u1, p[3].v1 },
		{ trans[2].x, trans[2].y, trans[2].z, p[2].dwColor, p[2].u1, p[2].v1 },
		{ trans[1].x, trans[1].y, trans[1].z, p[1].dwColor, p[1].u1, p[1].v1 }
	};
	memcpy(pAllVertex + i*6, tempVertex, sizeof(CUSTOMVERTEX)*6);
}

void* pData;
pVB->Lock(0, 0, (void**)&pData, 0);
memcpy(pData, pAllVertex, sizeof(CUSTOMVERTEX) * 6 * drawList.size());
pVB->Unlock();

pEffect->Begin();
pEffect->SetViewMatrix(&view);
pEffect->SetProjMatrix(&proj);
pEffect->SetTexture(pTexture);
pDevice->SetStreamSource(0, pVB, 0, sizeof(CUSTOMVERTEX));
pDevice->SetVertexDeclaration(pVD);
pEffect->BeginPass();
pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, drawList.size() * 2);	// 三角形リストを使ってばらばらに描画
pEffect->EndPass();
pEffect->End();

このやり方にもあまりよろしくない点がある。
描画が1回ということはオブジェクトごとに個別の変換行列を指定することができないので、ローカル頂点座標を直接書き換えなければならないのである。
先に書いたようにDrawPrimitiveでは頂点バッファはグラフィックスカードが管理する領域に置かれるため、それを書き換えるためにはロックしなければならず、それがかなり重い(らしい)。

そこで今度はユーザー領域の頂点バッファを直接転送できるDrawPrimitiveUPを使う。

pEffect->Begin();
pEffect->SetViewMatrix(&view);
pEffect->SetProjMatrix(&proj);
pEffect->SetTexture(pTexture);
pEffect->BeginPass();
pDevice->DrawPrimitiveUP(
    D3DPT_TRIANGLELIST, 
    drawList.size() * 2, 
    pAllVertex, 
    sizeof(CUSTOMVERTEX)
);
pEffect->EndPass();
pEffect->End();

こうすることによってそこそこ高速化される。RADEON HD 4850でも20%くらい速くなった。古いカードだともっと恩恵があるかもしれない。
ネットで見てるとDrawPrimitiveUPを使っている人が多い印象があったのはこういう事情なのかと想像される。


ん、ちょっと待てよ。と思った方もいるだろう。


これだとテクスチャとか1つしか使えないんじゃ?


そうなのだ。
描画呼び出しを1回にするためオブジェクトごとの変換行列が設定できなくなったのと同様に、テクスチャの個別設定もできないのである。
つまり一度に叩く描画リストの中ではテクスチャなどの描画パラメータが共通している必要があるということ。いろいろなキャラのテクスチャでもなるべく1枚にまとめておいたほうがいいのは、こういう理由のようだ(読み込むときに1回ですむからというのもあるが)。

というわけなので、全く別個のテクスチャを使うメッシュとか、他にもアルファ値やらなんやらのパラメータを個別に設定したいという場合には、それぞれ別の描画リストを用意しなければならない。

むむむ・・・なかなかうまくいかないもんだな。

まとめ。

  • DrawPrimitive

頂点データがいっぱいあって呼び出しオーバーヘッドが無視でき、テクスチャとかのパラメータも個別にあるメッシュの描画に使う。

  • DrawPrimitiveUP

頂点データの書き換えが比較的容易でテクスチャなどのパラメータが共通しやすい板ポリ系に使う。

  • 描画リスト

異なる描画パラメータごとに別々の描画リストを作る。描画情報をバッファリングしておいて、後で該当する描画リストに振り分けてまとめて描画するとたぶん効率がいい。