steps3D - Tutorials - Расширения ARB_indirect_rendering и ARB_shader_draw_parameters и их применение в indirect рендеринге

Расширения ARB_indirect_rendering и ARB_shader_draw_parameters и их применение в indirect рендеринге

Обычно мы формируем запросы на рендеринг в виде команд glDraw* на стороне CPU и передаем на GPU всю информацию о том, что и как нужно выводить. Однако такой подход не всегда оказывается удобным - в ряде случае хочется иметь возможность прямо на GPU (например при помощи вычислительных шейдеров) сформировать запросы на рендеринг геометрии и выполнить его без передачи данных на CPU и обратно.

В качестве примера подобной задачи давайте рассмотрим отсечение заведомо невидимых объектов - у нас есть большой массив мешей (и их AABB) и мы хотим сформировать запрос на рендеринг только тех мешей, которые попадают в усеченную пирамиду видимости (viewing frustum) для нашей камеры. Можно эффективно и быстро проверить видимость каждого меша (точнее, его AABB) при помощи вычислительного шейдера, однако хочется избежать передачи данных о видимости на CPU.

Расширение ARB_multi_draw_indirect дает возможность строить данные для управления рендерингом на GPU и выполнять рендеринг без копирования этих данных на CPU. Для этого с помощью вычислительного шейдера мы заполняем этими данные буфер типа GL_DRAW_INDIRECT_BUFFER. Он представляет из себя массив структур DrawArraysIndirectCommand/DrawElementsIndirectCommand. Каждая такая структура соответствует одному блоку геометрии/мешу.

После того, как такой буфер заполнен, мы можем запустить рендеринг из него при помощи команды glMultiDraw*Indirect. Давайте сейчас вернемся к примеру с большим количеством мешей,видимость которых мы хотим проверять. Тогда мы в буфер складываем массив структур DrawElementsIndirectCommand, каждая структура соответствует одному мешу.

После этого вычислительный шейдер для каждого такого меша получает его AABB (читает из другого буфера или же строит в runtime) и проверяет его на попадание в усеченную пирамиду видимости. Далее для невидимого меша в соответствующей ему структуре DrawElementsIndirectCommand поле instanceCount устанавливается в 0, для видимого - в единицу.

Далее для рендеринга мы используем команду glMultiDrawElementsIndirect, передавая в качестве primCount общее число мешей. При рендеринге нам может помочь расширение ARB_shader_draw_parameters (вошедшее в состав OpenGL 4.6). Оно вводит в GLSL несколько новых встроенных переменных - gl_BaseVertex(ARB), gl_BaseInstance(ARB) и gl_DrawID(ARB). Наиболее полезным для нас является переменная gl_DrawID, задающая фактически номер элемента в indirect-буфере (т.е. номер меша). По ней мы можем извлечь из UBO/SSBO какие-то дополнительные параметры для рендеринга меша, что может позволить нам вывести все меши (с разными параметрами) за один вызов glMultiDraw*Indirect.

На самом деле при таком подходе мы вынуждены хранить структуре в буфере для каждого меша вне зависимости от его видимости. А хотелось бы построить буфер, содержащий записи только для тех мешей, которые видимы. Это легко можно организовать - и далее мы покажем как именно - но при этом возникает следующая проблема - у нас число записанных элементов в буфер будет храниться в памяти GPU, а оно нам нужно для команды glMultiDraw*Indirect. И было бы нежелательно (и медленно) каждый раз копировать его обратно в память CPU.

И тут нам на помощь приходит расширение ARB_indirect_parameters, вошедшее в состав OpenGL 4.6. Оно вводит новый тип буфера - GL_PARAMETER_BUFFER. Этот буфер может использоваться для задания параметра drawCount для команд indirect-рендеринга. Для этого в дополнение к уже имеющемся командам glMultiDraw*Indirect вводятся две новые команды, показанные ниже.

void glMultiDrawArraysIndirectCount ( GLenum mode,
                                      const void * indirect,
                                      GLintptr drawCount,
                                      GLsizei maxDrawCount,
                                      GLsizei stride );
 
void glMultiDrawElementsIndirectCount ( GLenum mode,
                                        GLenum type,
                                        const void * indirect,
                                        GLintptr drawCount,
                                        GLsizei maxDrawCount,
                                        GLsizei stride );

В этих командах параметр drawCount задает смещение в байтах (и оно должно быть кратным 4) в буфере GL_PARAMETER_BUFFER откуда необходимо взять значение типа GLsizei, содержащее реальное число элементов в GL_DRAW_INDIRECT_BUFFER, которые нужно вывести. Параметр maxDrawCount задает максимальное число элементов для indirect-буфера, если прочитанное значение будет больше этого максимального, то будет выведено именно максимальное значение. Обратите внимание, что по стандарту GLsizei это 32-битовое беззнаковое число (в GLSL ему соответствует тип uint).

Давайте теперь рассмотрим как мы можем использовать это для создания и рендеринга буфера с видимыми мешами. Для этого нам понадобится несколько SSBO-буферов. В первом из них мы будем хранить исходные структуры DrawElementsIndirectCommand для всех мешей (он задается и мы не будем его изменять). Во втором буфере мы буем хранить AABB для всех мешей (в том же порядке, что и в первом SSBO). Третий буфер будет содержать счетчик видимых мешей и он будет содержать только одно значение - 32-битовое беззнаковое число. И последний, четвертый, буфер будет использоваться для хранения DrawElementsIndirectCommand только для видимых мешей. Он и будет заполняться вычислительным шейдером.

Обратите внимание, как будет осуществляться добавление элементов в четвертый буфер - мы берем число элементов в нем (т.е. значение из третьего буфера) - оно определяет на какое место мы должны записать новый элемент. И само это значение должно быть увеличено на единицу. Поскольку добавление новых элементов осуществляется набором параллельно исполняемых нитей, то чтобы избежать race condition, мы будет увеличение на единицы выполнять атомарно, т.е. при помощи функции atomicAdd.

struct DrawElementsIndirectCommand
{
    uint    count;
    uint    instanceCount;
    uint    firstIndex;
    uint    baseInstance;
};
 
struct Box
{
    vec4    minPt;
    vec4    maxPt;
};
 
layout ( std430, binding = 0 ) readonly buffer InMeshes
{
    DrawElementsIndirectCommand    inMeshes [];
};
 
layout ( std430, binding = 1 ) readonly buffer Boxes
{
    Box     boxes [];
};
 
layout ( std430, binding = 2 ) buffer Counter
{
    uint    counter;
};
 
layout ( std430, binding = 3 ) writeonly buffer OutMeshes
{
    DrawElementsIndirectCommand    outMeshes [];
};
 
. . .
 
void    main ()
{
    int    id = gl_GlobalInvocationID.x;
    
    if ( id == 0 )
        counter = 0;
        
    memoryBarrierBuffer ();        // wait till counter change became visible
    
    if ( isVisible ( boxes [id] )
    {
        int    pos = atomicAdd ( counter, 1 );
        
        outMeshes [pos] = inMeshes [id];
    }
}