ジオメトリの複数のインスタンスを効率的に描画する (Direct3D 9)

同じジオメトリを使用する多数のオブジェクトを含むシーンでは、そのジオメトリの多くのインスタンスをさまざまな向き、サイズ、色で描画でき、レンダラーに提供する必要があるデータ量を減らすことで、パフォーマンスが大幅に向上します。

これは、インデックス付きジオメトリを描画するための 1 つ目とインデックスのないジオメトリの場合の 2 つの手法を使用して実現できます。 どちらの手法でも、2 つの頂点バッファーが使用されます。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 x 4 行列を初期化できます。つまり、このデータはジオメトリの各インスタンスのサイズ、位置、回転を一意に行います。 最初の 4 つのコンポーネントはテクスチャ座標セマンティックを使用します。この場合、"これは一般的な 4 成分の数値です" を意味します。頂点宣言で任意のデータを使用する場合は、テクスチャ座標セマンティックを使用してマークします。 ストリームの最後の要素は、色データに使用されます。 これは頂点シェーダーに適用して、各インスタンスに一意の色を与える可能性があります。

レンダリングする前に、 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 ));

SetStreamSourceFreq では 、D3DSTREAMSOURCE_INDEXEDDATA を使用してインデックス付きジオメトリ データを識別します。 この場合、ストリーム 0 には、オブジェクトジオメトリを記述するインデックス付きデータが含まれます。 この値は、描画するジオメトリのインスタンス数と論理的に組み合わされます。

D3DSTREAMSOURCE_INDEXEDDATAと描画するインスタンスの数は、常にストリーム 0 に設定する必要があることに注意してください。

2 番目の呼び出しでは、 SetStreamSourceFreqD3DSTREAMSOURCE_INSTANCEDATA を使用して、インスタンス データを含むストリームを識別します。 各頂点にはインスタンス データのセットが 1 つ含まれるため、この値は論理的に 1 と組み合わされます。

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();
}

render ループが 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 番目のループによってラップされ、各オブジェクトが描画されます。 これで、ジオメトリ データがレンダラーに n 回 (1 回ではなく) ストリーミングされ、描画されるオブジェクトごとにパイプラインの状態を冗長に設定することもできます。 このレンダリング シーケンスは、大幅に遅くなる可能性が非常に高くなります。 また、DrawIndexedPrimitive のパラメーターは、2 つのレンダー ループ間で変更されていないことにも注意してください。

インデックスのないジオメトリの描画

Drawing Indexed Geometry では、インデックス付きジオメトリの複数のインスタンスを効率的に描画するように頂点バッファーが構成されました。 SetStreamSourceFreq を使用して、インデックスのないジオメトリを描画することもできます。 これには、異なる頂点バッファー レイアウトが必要であり、制約が異なります。 インデックスのないジオメトリを描画するには、次の図のように頂点バッファーを準備します。

インデックスのないジオメトリの頂点バッファーの図

この手法は、どのデバイスでもハードウェア アクセラレーションではサポートされていません。 これはソフトウェア頂点処理でのみサポートされ、 vs_3_0 シェーダーでのみ機能します。

この手法はインデックスのないジオメトリで動作するため、インデックス バッファーはありません。 図に示すように、geometry を含む頂点バッファーには、ジオメトリ データの 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()
};

これらの宣言は、インデックス付き geometry の例で行われた宣言と同じです。 もう一度、最初の宣言 (ストリーム 0 の場合) はジオメトリ データを定義し、2 番目の宣言 (ストリーム 1 の場合) はオブジェクトごとのインスタンス データを定義します。 最初の頂点バッファーを作成するときは、描画するジオメトリ データのインスタンス数でバッファーを読み込みます。

レンダリングする前に、最初の頂点バッファーを n 個のインスタンスに分割する方法をランタイムに指示する分割線を設定する必要があります。 次に、次のように 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 ));

SetStreamSourceFreq の最初の呼び出しでは、ストリーム 0 に m 個の頂点の n 個のインスタンスが含まれていることが示されます。 次に、SetStreamSource はストリーム 0 をジオメトリ頂点バッファーにバインドします。

2 番目の呼び出しでは、 SetStreamSourceFreq はストリーム 1 をインスタンス データのソースとして識別します。 2 番目のパラメーターは、各オブジェクトの頂点の数 (m) です。 インスタンス データ ストリームは、常に 2 番目のストリームとして宣言する必要があります。 その後、SetStreamSource はストリーム 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 回呼び出されていることに注意してください。 ジオメトリ データは 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 番目のループでラップする必要があります。 2 番目のレンダー ループを排除することで、ループ内で呼び出されるレンダリング状態の変更が少なくなり、パフォーマンスが向上することが期待されます。

インデックス付き手法ではジオメトリ データのコピーが 1 つしかストリーミングされないため、インデックス付き手法 (Drawing Indexed Geometry) の方がインデックス付けされていない手法 (インデックス付きジオメトリ以外のジオメトリの描画) よりもパフォーマンスが高いと予想するのが合理的です。 DrawIndexedPrimitive のパラメーターは、どのレンダリング シーケンスにも変更されていないことに注意してください。

高度なトピック

インスタンス化のサンプル