(Direct3D 9) 高效绘制几何图形的多个实例

给定包含许多使用相同的几何图形的对象的场景,可以通过减少需要提供给呈现器的数据量,以不同的方向、大小、颜色等绘制该几何图形的许多实例,从而显著提高性能。

这可以通过使用两种技术来实现:第一种方法用于绘制索引几何图形,第二种技术用于绘制非索引几何图形。 这两种技术都使用两个顶点缓冲区:一个用于提供几何图形数据,一个用于提供每个对象的实例数据。 实例数据可以是各种信息,例如转换、颜色数据或照明数据- 基本上是可在顶点声明中描述的任何内容。 使用这些技术绘制几何图形的许多实例可以大大减少发送到呈现器的数据量。

绘图索引几何图形

顶点缓冲区包含由顶点声明定义的每个顶点数据。 假设每个顶点的一部分包含几何图形数据,而每个顶点的一部分包含每个对象的实例数据,如下图所示。

索引几何图形的顶点缓冲区示意图

此方法需要支持 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()
};

这些声明定义两个顶点缓冲区。 流 0 的第一个声明 (,由列 1 中的零指示) 定义由位置、法线、正切、二元和纹理坐标数据组成的几何图形数据。

流 1 的第二个声明 (,由列 1 中的声明指示) 定义每个对象的实例数据。 每个实例由四个四分量浮点数和一个四分量颜色定义。 前四个值可用于初始化 4x4 矩阵,这意味着此数据将唯一调整几何图形的每个实例的大小、位置和旋转。 前四个分量使用纹理坐标语义,在本例中表示“这是一个常规的四分量数”。在顶点声明中使用任意数据时,请使用纹理坐标语义对其进行标记。 流中的最后一个元素用于颜色数据。 这可以在顶点着色器中应用,为每个实例提供唯一的颜色。

在呈现之前,需要调用 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 包含描述对象几何图形的索引数据。 此值在逻辑上与要绘制的几何图形的实例数相结合。

请注意,必须始终在流 0 中设置D3DSTREAMSOURCE_INDEXEDDATA和要绘制的实例数。

第二次调用中, SetStreamSourceFreq 使用 D3DSTREAMSOURCE_INSTANCEDATA 标识包含实例数据的流。 此值在逻辑上与 1 组合,因为每个顶点包含一组实例数据。

最后两次对 SetStreamSource 的调用会将顶点缓冲区指针绑定到设备。

完成实例数据的呈现后,请确保将顶点流频率重置回其默认状态 (不使用实例化) 。 由于此示例使用了两个流,因此请设置两个流,如下所示:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(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();
}

请注意,呈现循环调用一次,几何数据流式传输一次,n 个实例流式传输一次。 下一个呈现序列在功能上是相同的,但不利用实例化:

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

请注意,整个呈现循环由第二个循环包装,以绘制每个对象。 现在,几何图形数据将流式传输到呈现器 n 次 (而不是一次) ,并且还可以为每个绘制的对象冗余设置任何管道状态。 此呈现序列可能会明显变慢。 另请注意, DrawIndexedPrimitive 的参数在两个呈现循环之间没有更改。

绘制非索引几何图形

绘图索引几何图形中,顶点缓冲区配置为以更高的效率绘制索引几何图形的多个实例。 还可以使用 SetStreamSourceFreq 绘制非索引几何图形。 这需要不同的顶点缓冲区布局,并且具有不同的约束。 若要绘制未编制索引的几何图形,请准备顶点缓冲区,如下图所示。

非索引几何图形的顶点缓冲区示意图

任何设备上的硬件加速都不支持此方法。 它仅受软件顶点处理支持,并且仅适用于 vs_3_0 着色器。

由于此方法适用于非索引几何图形,因此没有索引缓冲区。 如图所示,包含几何图形的顶点缓冲区包含几何数据的 n 个副本。 对于绘制的每个实例,从第一个顶点缓冲区读取几何数据,从第二个顶点缓冲区读取实例数据。

下面是相应的顶点缓冲区声明:

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 的第一个声明 () 定义几何图形数据,流 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 绑定到几何顶点缓冲区。

第二次调用中, SetStreamSourceFreq 将流 1 标识为实例数据的源。 第二个参数是每个对象中的顶点数, (m) 。 请记住,实例数据流必须始终声明为第二个流。 然后,SetStreamSource 将流 1 绑定到包含实例数据的顶点缓冲区。

完成实例数据的呈现后,请确保将顶点流频率重置回其默认状态。 由于此示例使用了两个流,因此请设置两个流,如下所示:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(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();
}

请注意,呈现循环被调用一次。 尽管有 n 个正在流式传输的几何实例,但几何数据将流式传输一次。 实例顶点缓冲区中的数据将流式传输一次。 下一个呈现序列在功能上是相同的,但不利用实例化:

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

如果不进行实例化,则呈现循环需要由第二个循环包装,以绘制每个对象。 通过消除第二个呈现循环,由于在循环中调用的呈现状态更改较少,因此应获得更好的性能。

总体而言,可以合理地期望索引技术 (绘图索引几何) 比绘制非索引几何) (非索引 技术的性能更好,因为索引技术仅流式传输几何数据的一个副本。 请注意,对于任何呈现序列, DrawIndexedPrimitive 的参数没有更改。

高级主题

实例化示例