Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Обычно мы формируем запросы на рендеринг в виде команд 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];
}
}