Это часть серии руководств по продвинутому современному OpenGL. Чтобы использовать все функции в полной мере, вам понадобится OpenGL 4.6. В этих статьях предполагается, что вы знакомы с OpenGL.

При написании графического кода, использующего OpenGL, в конце концов мы достигаем стадии, когда нам нужен гибкий способ чтения и записи в память графического процессора способами, которые лучше всего подходят для нашего приложения. Здесь в игру вступает объект буфера хранения шейдеров (SSBO).

В чем разница между юниформ-буфером и буфером хранения шейдеров?

  1. Юниформ-буфер доступен только для чтения. Буфер хранения шейдеров доступен как для чтения, так и для записи. Сюда входят атомарныеоперации чтения и записи для синхронизации данных между ядрами графического процессора.
  2. Буферы хранения шейдеров могут быть намного больше, чем универсальные буферы хранения, на практике для использования может быть выделен общий размер всей доступной памяти графического процессора.
  3. Буферы хранилища шейдеров имеют доступ к макету std430 в GLSL, который является улучшением по сравнению со старым макетом std140.
  4. Внутри шейдера, написанного на GLSL, SSBO можно указать с переменной длиной вместо того, чтобы заранее знать, какой будет размер.
  5. Спецификация предупреждает, что скорость доступа к данным в SSBO может быть ниже, чем для юниформ-буфера.

Создание и использование буфера хранения шейдеров

В этом примере предположим, что мы выполняем отрисовку экземпляров с использованием glDrawArraysInstanced и хотим передать все матрицы модели для каждого экземпляра в качестве буфера хранения шейдера.

Создание буфера:

GLuint modelMatricesBuffer;
glCreateBuffers(1, &modelMatricesBuffer);

Вставка данных в буфер с использованием прямого доступа к состоянию (DSA):

std::vector<glm::mat4> instancedModelMatrices;
... code to insert a mat4 per instance into the vector ...

glNamedBufferStorage(modelMatricesBuffer, 
                     sizeof(glm::mat4) * instancedModelMatrices.size(), 
                     (const void *)instancedModelMatrices.data(), 
                     GL_DYNAMIC_STORAGE_BIT);

Важно иметь в виду, что после использования glNamedBufferStorage размер области памяти графического процессора для буфера фиксируется на оставшуюся часть modelMatricesBuffer's срока службы. Нам разрешено динамически читать и записывать данные в эту область, но мы не можем изменять ее размер.

Последним параметром glNamedBufferStorage является флаг использования. Доступны следующие варианты:

Для получения дополнительной информации см. эту страницу.

Чтобы использовать буфер для экземплярного рендеринга в вершинном шейдере, мы настроим его следующим образом:

#version 460 core

// Passed in like normal using a vertex array object
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoords;

uniform mat4 projection;
uniform mat4 view;

// SSBO containing the instanced model matrices
layout(binding = 2, std430) readonly buffer ssbo1 {
    mat4 modelMatrices[];
};

smooth out vec2 fsTexCoords;

void main() {
    gl_Position = 
        projection * view * modelMatrices[gl_InstanceID] * vec4(position, 1.0);
    fsTexCoords = texCoords;
}

Разбивка вершинного шейдера

layout(binding = 2, std430) readonly buffer ssbo1 {
    mat4 modelMatrices[];
};

Здесь мы указываем точку привязки буфера хранения шейдера ssbo1. Мы помечаем его как readonly, чтобы шейдер не мог писать в него. Если бы мы оставили «только для чтения», чтобы он читал layout(binding = 2, std430) buffer ssbo1, шейдер мог бы как читать, так и записывать в буфер (дополнительную информацию см. ниже).

Мы устанавливаем точку привязки на 2, чтобы, когда мы приступим к привязке буфера в коде C++, мы привязали его к местоположению 2.

Мы устанавливаем макет упаковки на std430, который определяет, как данные размещаются в памяти (дополнительную информацию см. ниже).

Наконец, обратите внимание, что modelMatrices[] указывается без точного размера. Это сделано намеренно, поскольку SSBO не требуют, чтобы мы сообщали шейдеру размер массива заранее. Это означает, что массив любой длины может быть привязан к точке привязки 2.

void main() {
    gl_Position = 
        projection * view * modelMatrices[gl_InstanceID] * vec4(position, 1.0);
    fsTexCoords = texCoords;
}

Здесь важно отметить, что мы индексируем modelMatrices с помощью встроенного ввода шейдера gl_InstanceID. gl_InstanceID всегда будет хранить текущий экземпляр отрисовки при использовании такой команды, как glDrawArraysInstanced, или будет равен 0, если использовалась команда отрисовки без экземпляра.

SSBO может быть проиндексирован другими целочисленными переменными, включая универсальные целые числа, которые передаются шейдеру.

некоторые из доступных квалификаторов памяти

Полный список квалификаторов см. на этой странице.

правила компоновки упаковки std430

Смотрите здесь и здесь для получения дополнительной информации.

Макет упаковки std430 доступен только для буферов хранения шейдеров. Он использует следующие правила:

Из этой таблицы видно, что скаляры и массивы скаляров будут плотно упакованы, поэтому, например, float array[3] на стороне процессора будет равно float array[3] массиву на стороне графического процессора.

vec3 необходимо выровнять по 16-байтовой границе. vec4 также является 16-байтовой границей. Mat3 необходимо выровнять по границе 48 байт. Mat4 необходимо выровнять по границе 64 байта.

Чтобы облегчить себе задачу, избегайте использования vec3 и mat3 в буферах хранения шейдеров.

Переплет и рисунок

Сейчас мы находимся на этапе рисования, который будет выглядеть примерно так:

// Bind the storage buffer
// Note the 2 matches our binding = 2 in the vertex shader
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, modelMatricesBuffer);

// Bind our shader program
glUseProgram(shader);

// Set up matrices
glUniformMatrix4fv(
    glGetUniformLocation(shader, "projection"), 1, GL_FALSE, projectionMat
);
glUniformMatrix4fv(
    glGetUniformLocation(shader, "view"), 1, GL_FALSE, viewMat
);

// Bind VAO for positions and uv coordinates
glBindVertexArray(vao);

// Perform instanced drawing
glDrawArraysInstanced(
    GL_TRIANGLES, 0, numVertices, instancedModelMatrices.size()
);

glBindVertexArray(0);
glUseProgram(0);

// Uncomment if you want to explicitly unbind the resouce
//glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0);

Отвязка? Использование с несколькими шейдерами?

Часто нет необходимости явно отвязывать буфер после использования `glBindBufferBase`. Вы можете перезаписать существующую привязку в любой момент, снова вызвав функцию с тем же индексом привязки, но с другим объектом буфера хранилища шейдера. Явное отсоединение можно выполнить, передав 0 в качестве аргумента буфера для определенного индекса привязки.

Если вы хотите, чтобы один и тот же буфер хранилища шейдеров использовался несколькими шейдерами, просто укажите одну и ту же привязку хранилища шейдеров в каждом из этих шейдеров. Затем вы можете вызвать glBindBufferBase один раз с индексом и продолжить использовать его с несколькими шейдерными программами подряд.

Можно ли использовать SSBO для хранения данных вершин, uv и нормалей?

Да! См. туториал Programmable Vertex Pulling, в котором показано, как это сделать.

Будущие статьи

Помимо предстоящей статьи о программируемом извлечении вершин, будет также статья, посвященная вычислительным шейдерам и показывающая, как использовать SSBO, чтобы позволить графическому процессору выполнять высокопараллельную работу и записывать результаты в память, к которой позже сможет получить доступ процессор.

Изучите основы OpenGL

Рекомендации