Desenhando com eficiência várias instâncias de geometria (Direct3D 9)

Dada uma cena que contém muitos objetos que usam a mesma geometria, você pode desenhar muitas instâncias dessa geometria em diferentes orientações, tamanhos, cores e assim por diante com um desempenho drasticamente melhor, reduzindo a quantidade de dados que você precisa fornecer ao renderizador.

Isso pode ser feito por meio do uso de duas técnicas: a primeira para desenhar geometria indexada e a segunda para geometria não indexada. Ambas as técnicas usam dois buffers de vértice: um para fornecer dados de geometria e outro para fornecer dados de instância por objeto. Os dados da instância podem ser uma ampla variedade de informações, como uma transformação, dados de cores ou dados de iluminação , basicamente qualquer coisa que você possa descrever em uma declaração de vértice. Desenhar muitas instâncias de geometria com essas técnicas pode reduzir drasticamente a quantidade de dados enviados ao renderizador.

Desenhando geometria indexada

O buffer de vértice contém dados por vértice definidos por uma declaração de vértice. Suponha que parte de cada vértice contenha dados de geometria e que parte de cada vértice contenha dados de instância por objeto, conforme mostrado no diagrama a seguir.

diagrama de um buffer de vértice para geometria indexada

Essa técnica requer um dispositivo que dê suporte ao modelo de sombreador de vértice 3_0. Essa técnica funciona com qualquer sombreador programável, mas não com o pipeline de função fixa.

Para os buffers de vértice mostrados acima, aqui estão as declarações de buffer de vértice correspondentes:

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

Essas declarações definem dois buffers de vértice. A primeira declaração (para o fluxo 0, indicado pelos zeros na coluna 1) define os dados de geometria que consistem em: posição, normal, tangente, binormal e dados de coordenadas de textura.

A segunda declaração (para o fluxo 1, indicado pelos da coluna 1) define os dados da instância por objeto. Cada instância é definida por quatro números de ponto flutuante de quatro componentes e uma cor de quatro componentes. Os quatro primeiros valores podem ser usados para inicializar uma matriz 4x4, o que significa que esses dados dimensionarão, posicionarão e girarão exclusivamente cada instância da geometria. Os quatro primeiros componentes usam uma semântica de coordenada de textura que, nesse caso, significa "este é um número geral de quatro componentes". Ao usar dados arbitrários em uma declaração de vértice, use uma semântica de coordenada de textura para marcá-los. O último elemento no fluxo é usado para dados de cor. Isso pode ser aplicado no sombreador de vértice para dar a cada instância uma cor exclusiva.

Antes da renderização, você precisa chamar SetStreamSourceFreq para associar os fluxos de buffer de vértice ao dispositivo. Aqui está um exemplo que associa os dois buffers de vértice:

// 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 usa D3DSTREAMSOURCE_INDEXEDDATA para identificar os dados de geometria indexados. Nesse caso, o fluxo 0 contém os dados indexados que descrevem a geometria do objeto. Esse valor é combinado logicamente com o número de instâncias da geometria a ser desenhada.

Observe que D3DSTREAMSOURCE_INDEXEDDATA e o número de instâncias a serem desenhadas sempre devem ser definidos no fluxo zero.

Na segunda chamada, SetStreamSourceFreq usa D3DSTREAMSOURCE_INSTANCEDATA para identificar o fluxo que contém os dados da instância. Esse valor é combinado logicamente com 1, pois cada vértice contém um conjunto de dados de instância.

As duas últimas chamadas para SetStreamSource associam os ponteiros de buffer de vértice ao dispositivo.

Quando terminar de renderizar os dados da instância, lembre-se de redefinir a frequência de fluxo de vértice de volta para seu estado padrão (que não usa instanciação). Como este exemplo usou dois fluxos, defina ambos os fluxos, conforme mostrado abaixo:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

Comparação de desempenho de geometria indexada

Embora não seja possível chegar a uma única conclusão sobre o quanto essa técnica poderia reduzir o tempo de renderização em cada aplicativo, considere a diferença na quantidade de dados transmitidos para o runtime e o número de alterações de estado que serão reduzidas se você usar a técnica de instanciação. Essa sequência de renderização aproveita o desenho de várias instâncias da mesma geometria:

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

Observe que o loop de renderização é chamado uma vez, os dados de geometria são transmitidos uma vez e n instâncias são transmitidas uma vez. Essa próxima sequência de renderização é idêntica na funcionalidade, mas não aproveita a instanciação:

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

Observe que todo o loop de renderização é encapsulado por um segundo loop para desenhar cada objeto. Agora, os dados de geometria são transmitidos para o renderizador n vezes (em vez de uma vez) e todos os estados de pipeline também podem ser definidos com redundância para cada objeto desenhado. É muito provável que essa sequência de renderização seja significativamente mais lenta. Observe também que os parâmetros para DrawIndexedPrimitive não foram alterados entre os dois loops de renderização.

Desenhando geometria não indexada

Em Geometria Indexada de Desenho, buffers de vértice foram configurados para desenhar várias instâncias de geometria indexada com maior eficiência. Você também pode usar SetStreamSourceFreq para desenhar geometria não indexada. Isso requer um layout de buffer de vértice diferente e tem restrições diferentes. Para desenhar geometria não indexada, prepare os buffers de vértice como o diagrama a seguir.

diagrama de um buffer de vértice para geometria não indexada

Essa técnica não é suportada pela aceleração de hardware em nenhum dispositivo. Ele só tem suporte pelo processamento de vértice de software e funcionará apenas com sombreadores vs_3_0 .

Como essa técnica funciona com geometria não indexada, não há nenhum buffer de índice. Como mostra o diagrama, o buffer de vértice que contém geometria contém n cópias dos dados de geometria. Para cada instância desenhada, os dados de geometria são lidos do primeiro buffer de vértice e os dados da instância são lidos do segundo buffer de vértice.

Aqui estão as declarações de buffer de vértice correspondentes:

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

Essas declarações são idênticas às declarações feitas no exemplo de geometria indexada. Mais uma vez, a primeira declaração (para o fluxo 0) define os dados de geometria e a segunda declaração (para o fluxo 1) define os dados da instância por objeto. Ao criar o primeiro buffer de vértice, carregue-o com o número de instâncias dos dados de geometria que você desenhará.

Antes de renderizar, você precisa configurar o divisor que informa ao runtime como dividir o primeiro buffer de vértice em n instâncias. Em seguida, defina o divisor usando SetStreamSourceFreq da seguinte maneira:

// 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 ));

A primeira chamada para SetStreamSourceFreq diz que o fluxo 0 contém n instâncias de vértices m. SetStreamSource associa o fluxo 0 ao buffer de vértice de geometria.

Na segunda chamada, SetStreamSourceFreq identifica o fluxo 1 como a origem dos dados da instância. O segundo parâmetro é o número de vértices em cada objeto (m). Lembre-se de que o fluxo de dados da instância sempre deve ser declarado como o segundo fluxo. SetStreamSource associa o fluxo 1 ao buffer de vértice que contém os dados da instância.

Quando terminar de renderizar os dados da instância, lembre-se de redefinir a frequência de fluxo de vértice de volta para seu estado padrão. Como este exemplo usou dois fluxos, defina ambos os fluxos, conforme mostrado abaixo:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

Comparação de desempenho de geometria não indexada

A principal vantagem desse estilo de instanciação é que ele pode ser usado em geometria não indexada. Embora não seja possível chegar a uma única conclusão sobre o quanto essa técnica poderia reduzir o tempo de renderização em cada aplicativo, considere a diferença na quantidade de dados transmitidos para o runtime e o número de alterações de estado que serão reduzidas para a seguinte sequência de renderização:

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

Observe que o loop de renderização é chamado uma vez. Os dados de geometria são transmitidos uma vez, embora não haja nenhuma instância da geometria sendo transmitida. Os dados do buffer de vértice de instância são transmitidos uma vez. Essa próxima sequência de renderização é idêntica na funcionalidade, mas não aproveita a instanciação:

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

Sem instanciar, o loop de renderização precisa ser encapsulado por um segundo loop para desenhar cada objeto. Ao eliminar o segundo loop de renderização, você deve esperar um melhor desempenho devido a menos alterações de estado de renderização que são chamadas dentro do loop.

No geral, é razoável esperar que a técnica indexada (Geometria Indexada de Desenho) tenha um desempenho melhor do que a técnica não indexada (Desenhando Geometria Não Indexada) porque a técnica indexada transmite apenas uma cópia dos dados de geometria. Observe que os parâmetros para DrawIndexedPrimitive não foram alterados para nenhuma das sequências de renderização.

Tópicos Avançados

Exemplo de instanciação