複数のジオメトリのインスタンスの効率的な描画 (Direct3D 9)
同じジオメトリを使用するオブジェクトが複数あるシーンの場合、そのジオメトリのインスタンスをさまざまな向き、サイズ、色などで多数描画できます。また、レンダリングに必要なデータ量を減らすことでパフォーマンスを大幅に改善できます。
これを実現するには、2 つのテクニックを使用します。1 つはインデックス付きのジオメトリを描画するためのテクニック、もう 1 つはインデックスなしのジオメトリを描画するためのテクニックです。どちらのテクニックでも 2 つの頂点バッファーを使用します。1 つはジオメトリ データを提供する頂点バッファーで、もう 1 つはオブジェクト単位のインスタンス データを提供する頂点バッファーです。インスタンス データは、トランスフォーム、カラー データー、ライティング データなどの各種さまざまな情報 (基本的には頂点の宣言で記述できるあらゆるもの) を指定できます。これらのテクニックを使ってジオメトリのさまざまなインスタンスを描画すると、レンダラーに送信されるデータ量を劇的に削減できます。
- インデックス付きのジオメトリの描画
- インデックスなしのジオメトリの描画
- 関連項目
インデックス付きのジオメトリの描画
頂点バッファーは頂点の宣言で定義される頂点別データを含みます。各頂点の一部がジオメトリ データを含み、また、一部がオブジェクト別インスタンス データを含むとします。この場合は次のようになります。
図 1:インデックス付きのジオメトリ頂点バッファーのレイアウト
このテクニックでは 3_0 頂点シェーダー モデルをサポートするデバイスが必要です。このテクニックは、プログラム可能なシェーダーでは有効ですが、固定機能パイプラインでは有効ではありません。
上記の頂点バッファーの場合、対応する頂点バッファーの宣言は次のとおりです。
const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] = { {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, {0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT, 0}, {0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0}, {0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0}, D3DDECL_END() }; const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] = { {1, 0, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1}, {1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2}, {1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3}, {1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4}, {1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0}, D3DDECL_END() };
これらの宣言は 2 つの頂点バッファーを定義します。最初の宣言 (列 1 がゼロのストリーム 0) は、位置、法線、接線、従法線、およびテクスチャーの座標データで構成されるジオメトリ データを定義します。
2 番目の宣言 (列 1 が 1 のストリーム 1) は、オブジェクト インスタンス別のデータを定義します。各インスタンスは、4 つの 4 成分浮動小数点値、および 4 成分カラーによって定義されます。最初の 4 つの値は 4x4 行列の初期化に使用できます。つまり、このデータは各ジオメトリのインスタンスのサイズ、位置、向きを一意に処理します。最初の 4 つの成分はテクスチャー座標セマンティックを使用します。この場合、"これが一般的な 4 成分数値" であることを意味します。頂点宣言で任意のデータを使用する場合は、テクスチャー座標のセマンティックを使用してこれにマークを付けてください。ストリーム内の最後の要素はカラー データに使用されます。これは頂点シェーダーに適用でき、各インスタンスに固有のカラーが与えられます。
レンダリングする前に、IDirect3DDevice9::SetStreamSourceFreq を呼び出して頂点バッファーのストリームをデバイスにバインドする必要があります。次に、両方の頂点バッファーをバインドする例を示します。
// Set up the geometry data stream pd3dDevice->SetStreamSourceFreq(0, (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw)); pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); // Set up the instance data stream pd3dDevice->SetStreamSourceFreq(1, (D3DSTREAMSOURCE_INSTANCEDATA | 1)); pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));
IDirect3DDevice9::SetStreamSourceFreq は、D3DSTREAMSOURCE_INDEXEDDATA を使用してインデックス付きのジオメトリ データを識別します。この場合、ストリーム 0 は、オブジェクト ジオメトリを説明するインデックス付きのデータを含みます。この値は、描画するジオメトリのインスタンス数に論理的に結合されます。
D3DSTREAMSOURCE_INDEXEDDATA および描画するインスタンスの数は、必ずストリーム ゼロで設定する必要があります。
2 番目の呼び出しでは、IDirect3DDevice9::SetStreamSourceFreq は D3DSTREAMSOURCE_INSTANCEDATA を使用してインスタンス データを含むストリームを識別します。各頂点は一連のインスタンス データを含むため、この値は論理的に 1 に結合されます。
IDirect3DDevice9::SetStreamSource への最後の 2 つの呼び出しは、頂点バッファー ポインターをデバイスにバインドします。
インスタンス データのレンダリングが終了したら、頂点ストリームの頻度を必ずその既定のステート (インスタンス化を使用しないステート) にリセットしてください。この例では、2 つのストリームを使用したため、両方のストリームを次のように設定します。
pd3dDevice->SetStreamSourceFreq(0,1); pd3dDevice->SetStreamSourceFreq(1,1);
インデックス付きのジオメトリのパフォーマンス比較
このテクニックを使うことで、アプリケーションごとに削減できるレンダリング時間について 1 つの結論を下すことは不可能ですが、インスタンス化テクニックを使用した場合、ランタイムに読み込まれるデータの量、および削減されるステートの変更数がどのように違うか考えてみましょう。このレンダリング シーケンスでは、同じジオメトリの複数のインスタンスの描画が利用されます。
if( SUCCEEDED( pd3dDevice->BeginScene() ) ) { // Set up the geometry data stream pd3dDevice->SetStreamSourceFreq(0, (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw)); pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); // Set up the instance data stream pd3dDevice->SetStreamSourceFreq(1, (D3DSTREAMSOURCE_INSTANCEDATA | 1)); pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 )); pd3dDevice->SetVertexDeclaration( ... ); pd3dDevice->SetVertexShader( ... ); pd3dDevice->SetIndices( ... ); pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, g_dwNumVertices, 0, g_dwNumIndices/3 ); pd3dDevice->EndScene(); }
レンダリング ループは 1 度呼び出され、ジオメトリ データは 1 度読み込まれること、そして n インスタンスも 1 度読み込まれることに注目してください。この次のレンダリング シーケンスは機能上はまったく同じですが、インスタンス化は利用しません。
if( SUCCEEDED( pd3dDevice->BeginScene() ) ) { for(int i=0; i < g_numObjects; i++) { pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); pd3dDevice->SetVertexDeclaration( ... ); pd3dDevice->SetVertexShader( ... ); pd3dDevice->SetIndices( ... ); pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, g_dwNumVertices, 0, g_dwNumIndices/3 ); } pd3dDevice->EndScene(); }
各オブジェクトを描画するためにレンダリング ループ全体が 2 つめのループでラップされることに注目してください。これでジオメトリ データはレンダラーに (1 度ではなく) n 回読み込まれ、どのパイプライン ステートも描画されるオブジェクトごとに冗長的に設定できます。このレンダリング シーケンスは、かなり遅くなる可能性が非常に高くなります。また、2 つのレンダリング ループ間で、IDirect3DDevice9::DrawIndexedPrimitive へのパラメーターに変更がないことにも注目してください。
インデックスなしのジオメトリの描画
インデックス付きのジオメトリの描画では、頂点バッファーが設定され、インデックス付きのジオメトリの複数のインスタンスがはるかに効率的に描画されました。IDirect3DDevice9::SetStreamSourceFreq を使用してもインデックスなしのジオメトリを描画できます。これには、異なる頂点バッファー レイアウトが必要で、さまざまな制約があります。インデックスなしのジオメトリを描画するには、次のように頂点バッファーを準備します。
図 2:インデックスなしのジオメトリ頂点バッファーのレイアウト
どのデバイスでも、ハードウェア加速ではこのテクニックはサポートされていません。これはソフトウェア頂点の処理によってのみサポートされていて、vs_3_0 シェーダーのみで有効です。
このテクニックはインデックスなしのジオメトリでは機能しないため、インデックス バッファーはありません。図が示すとおり、ジオメトリを含む頂点バッファーは、ジオメトリ データのコピー を n 個含みます。描画されるインスタンスごとに、ジオメトリ データは最初の頂点バッファーから読み取られ、インスタンス データは 2 番目の頂点バッファーから読み取られます。
次に、対応する頂点バッファー宣言を示します。
const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] = { {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, {0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT, 0}, {0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0}, {0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0}, D3DDECL_END() }; const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] = { {1, 0, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1}, {1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2}, {1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3}, {1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4}, {1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0}, D3DDECL_END() };
これらの宣言は、インデックス付きのジオメトリ例で指定された宣言とまったく同じです。この場合も、最初の宣言 (ストリーム 0 の) がジオメトリ データを定義し、2 番目の宣言 (ストリーム 1 の) がオブジェクト インスタンス別のデータを定義します。最初の頂点バッファーを作成する場合は、描画するジオメトリ データのインスタンス数でロードするようにしてください。
レンダリング前に、分割値を設定する必要があります。ランタイムはこの分割値によって最初の頂点バッファーを n インスタンスに分割する方法を知ります。以下のように IDirect3DDevice9::SetStreamSourceFreq を使用して分割値を設定します。
// Set the divider pd3dDevice->SetStreamSourceFreq(0, 1); // Bind the stream to the vertex buffer pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); // Set up the instance data stream pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance); pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));
最初の IDirect3DDevice9::SetStreamSourceFreq の呼び出しで、ストリーム 0 には m 個の頂点のインスタンスが n 個あることが示されます。次に、IDirect3DDevice9::SetStreamSourceFreq でストリーム 0 をジオメトリ頂点バッファーにバインドします。
2 番目の呼び出しでは、IDirect3DDevice9::SetStreamSourceFreq はストリーム 1 をインスタンス データのソースとして識別します。2 番目のパラメーターは各オブジェクト (m) の頂点の数です。インスタンス データ ストリームは、2 番目のストリームとして常に宣言する必要があるので注意してください。IDirect3DDevice9::SetStreamSourceFreq は、インスタンス データを含む頂点バッファーにストリーム 1 をバインドします。
インスタンス データのレンダリングが終了したら、頂点ストリームの頻度を必ずその既定のステートにリセットしてください。この例では、2 つのストリームを使用したため、両方のストリームを次のように設定します。
pd3dDevice->SetStreamSourceFreq(0,1); pd3dDevice->SetStreamSourceFreq(1,1);
インデックスなしのジオメトリのパフォーマンス比較
このインスタンス化スタイルの主な利点は、インデックスなしのジオメトリで使用できるということです。このテクニックを使うことで、アプリケーションごとに削減できるレンダリング時間について 1 つの結論を下すことは不可能ですが、以下のレンダリング シーケンスを使用した場合、ランタイムに読み込まれるデータの量、および削減されるステートの変更数がどのように違うか考えてみましょう。
if( SUCCEEDED( pd3dDevice->BeginScene() ) ) { // Set the divider pd3dDevice->SetStreamSourceFreq(0, 1); pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); // Set up the instance data stream pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance)); pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 )); pd3dDevice->SetVertexDeclaration( ... ); pd3dDevice->SetVertexShader( ... ); pd3dDevice->SetIndices( ... ); pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, g_dwNumVertices, 0, g_dwNumIndices/3 ); pd3dDevice->EndScene(); }
レンダリング ループは 1 度呼び出されることに注目してください。読み込まれるジオメトリのインスタンス は n 個ありますが、ジオメトリ データは 1 度読み込まれます。インスタンス頂点バッファーからのデータは 1 度読み込まれます。この次のレンダリング シーケンスは機能上はまったく同じですが、インスタンス化は利用しません。
if( SUCCEEDED( pd3dDevice->BeginScene() ) ) { for(int i=0; i < g_numObjects; i++) { pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0, D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 )); pd3dDevice->SetVertexDeclaration( ... ); pd3dDevice->SetVertexShader( ... ); pd3dDevice->SetIndices( ... ); pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, g_dwNumVertices, 0, g_dwNumIndices/3 ); } pd3dDevice->EndScene(); }
インスタンス化なしでは、レンダリング ループを 2 番目のループでラップして、各オブジェクトを描画する必要があります。2 番目のレンダリング ループをなくすことで、ループ内で呼び出されるレンダリング ステートの変更が少なくなるため、パフォーマンスの向上が期待できます。
概して、インデックス付きのテクニック (インデックス付きのジオメトリの描画) がインデックスなしのテクニック (インデックスなしのジオメトリの描画) よりもパフォーマンス面で期待できることは理にかなっています。これはインデックス付きテクニックはジオメトリ データのコピーを 1 つだけ読み込むためです。IDirect3DDevice9::DrawIndexedPrimitive へのパラメーターは、いずれのレンダリング シーケンスについても変化しないことに注目してください。