Эффективное рисование нескольких экземпляров геометрии (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 содержит индексированные данные, описывающие геометрию объекта. Это значение логически сочетается с количеством экземпляров геометрии для рисования.

Обратите внимание, что D3DSTREAMSOURCE_INDEXEDDATA и количество экземпляров для рисования всегда должны быть заданы в нулевом потоке.

Во втором вызове SetStreamSourceFreq использует D3DSTREAMSOURCE_INSTANCEDATA для идентификации потока, содержащего данные экземпляра. Это значение логически объединяется с 1, так как каждая вершина содержит один набор данных экземпляра.

Последние два вызова SetStreamSource привязывают указатели буфера вершин к устройству.

Завершив отрисовку данных экземпляра, обязательно сбросьте частоту потока вершин в состояние по умолчанию (которое не использует instancing). Так как в этом примере использовались два потока, задайте оба потока, как показано ниже:

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

Сравнение производительности индексированных геометрических объектов

Хотя невозможно сделать один вывод о том, насколько этот метод может сократить время отрисовки в каждом приложении, учитывайте разницу в объеме данных, передаваемых в среду выполнения, и количество изменений состояния, которое будет уменьшено при использовании метода instancing. Эта последовательность отрисовки использует преимущества рисования нескольких экземпляров одной геометрии:

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 экземпляров передаются в потоковую передачу один раз. Следующая последовательность отрисовки идентична по функциональности, но не использует преимущества instancing:

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 шейдерами.

Так как этот метод работает с неиндексированной геометрией, буфер индекса отсутствует. Как показано на схеме, буфер вершин, содержащий geometry, содержит 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 содержит n экземпляров вершин m. Затем 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 экземпляров геометрии, которые передаются в потоковую передачу. Данные из буфера вершин экземпляра передаются однократно. Следующая последовательность отрисовки идентична по функциональности, но не использует преимущества instancing:

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

Без instancing цикл отрисовки необходимо упаковать во второй цикл для рисования каждого объекта. Исключив второй цикл отрисовки, следует ожидать повышения производительности из-за меньшего количества изменений состояния отрисовки, вызываемых внутри цикла.

В целом, разумно ожидать, что индексированные методы (рисование индексированных геометрических объектов) будут работать лучше, чем неиндексированные методы (рисование неиндексированных геометрических объектов), так как индексированные методы передают только одну копию геометрических данных. Обратите внимание, что параметры DrawIndexedPrimitive не изменились ни для одной из последовательностей отрисовки.

Дополнительные разделы

Пример instancing