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

В этом уроке мы рассмотрим метод устранения необходимости привязки текстуры к однородному текстурному блоку. Если вы еще не ознакомились с учебными пособиями Shader Storage Buffer Objects (SSBOs) и Programmable Vertex Pulling, поскольку мы будем использовать их оба здесь.

Что такое безбиндинговые текстуры?

OpenGL 4.5 требует, чтобы шейдер мог использовать как минимум 16 текстурных блоков, хотя многие драйверы поддерживают 32 и более. С этим ограничением очень сложно справиться, если мы пытаемся сделать что-то вроде слияния множества разных вызовов отрисовки с разными текстурами материала или доступа ко многим картам теней на этапе отложенного освещения. В прошлом было два основных способа обойти это:

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

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

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

Недостатки Bindless текстур

Есть два основных недостатка текстур без привязки:

1) Renderdoc не поддерживает их на момент написания, поэтому вам нужно будет использовать графический отладчик, например Nvidia NSight.

2) Текстуры без привязки так и не вошли в ядро ​​OpenGL, поэтому даже в версии 4.6 это все еще расширение. У Nvidia и AMD есть много оборудования, которое полностью их поддерживает, но помимо этих двух вам нужно будет дважды проверить, поддерживает ли их поставщик.

Взгляд на API

В этом уроке мы сосредоточимся на 3 основных функциях API, которые мы рассмотрим сейчас.

Получение дескрипторов текстуры

GLuint64 glGetTextureHandleARB(GLuint texture​);

Цель этой функции — взять допустимую текстуру и вернуть 64-битный дескриптор, представляющий эту текстуру. Этот дескриптор может быть вставлен непосредственно в юниформ-буферы или буферы хранения шейдеров.

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

Важно! Khronos заявляет следующее на своей вики: «После создания дескриптора для текстуры/сэмплера ни одно из его состояний не может быть изменено. Для текстур буфера это включает в себя объект буфера, который в настоящее время прикреплен к нему (что также означает, что вы не можете создать дескриптор для текстуры буфера, к которой не прикреплен буфер). Мало того, в таких случаях сам объект буфера становится неизменяемым; его нельзя перераспределить с помощью glBufferData. Хотя, как и в случае с текстурами, его хранилище по-прежнему можно отображать и изменять его данные с помощью других функций, как обычно».

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

Создание резидентных дескрипторов текстур

// handle is what is returned by glGetTextureHandleARB
void glMakeTextureHandleResidentARB(GLuint64 handle​);

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

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

Делаем дескрипторы текстур нерезидентными

// handle is what is returned by glGetTextureHandleARB
void glMakeTextureHandleNonResidentARB(GLuint64 handle​);

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

Пример: 1600 текстур в одном шейдере.

Чтобы показать, как это использовать, мы собираемся случайным образом сгенерировать 1600 текстур (по одной для каждого из 1600 экземпляров куба) и сделать их резидентными для использования шейдером. Мы будем использовать только один экземпляр вызова отрисовки для отрисовки 1600 кубов.

Сначала мы создадим данные для куба и вставим его вершины в SSBO.

struct VertexData {
    float position[3];
    float uv[2];
    float normal[3];
};

// In main or some other function
std::vector<VertexData> vertices;

... code to insert a single cube data into vertices ... 

const int numVertices = data.size();
GLuint verticesBuffer;

glCreateBuffers(1, &verticesBuffer);
glNamedBufferStorage(
    verticesBuffer,
    sizeof(VertexData) * vertices.size(),
    (const void *)vertices.data(),
    GL_DYNAMIC_STORAGE_BIT
);

Далее мы создадим 1600 различных преобразований, по одному для каждого экземпляра куба. Они также будут вставлены в SSBO.

std::vector<glm::mat4> instancedMatrices;

// Each one will be 5 units apart from the others in the x/y direction
for (size_t x = 0; x < 200; x += 5) {
    for (size_t y = 0; y < 200; y += 5) {
        glm::mat4 mat(1.0f);
        mat[3].x = float(x);
        mat[3].y = float(y);
        mat[3].z = 0.0f;
        instancedMatrices.push_back(std::move(mat));
    }
}

const int numInstances = instancedMatrices.size();
GLuint modelMatricesBuffer;

glCreateBuffers(1, &modelMatricesBuffer);
glNamedBufferStorage(
    modelMatricesBuffer,
    sizeof(glm::mat4) * instancedMatrices.size(),
    (const void *)instancedMatrices.data(),
    GL_DYNAMIC_STORAGE_BIT
);

Теперь мы будем случайным образом генерировать 1600 текстур.

Настройка векторов

std::vector<GLuint> textures;
std::vector<GLuint64> textureHandles;

// Each texture has a width and height of 32x32, 
// with 3 channels of data (RGB) per pixel
const size_t textureSize = 32 * 32 * 3;
unsigned char textureData[textureSize];

У нас будет вектор для текстур и для возвращаемых хэндлов.

Создание текстур и получение дескрипторов

for (int i = 0; i < numInstances; ++i) {
    const unsigned char limit = unsigned char(rand() % 231 + 25);
    // Randomly generate an unsigned char per RGB channel
    for (int j = 0; j < textureSize; ++j) {
        textureData[j] = unsigned char(rand() % limit);
    }

    GLuint texture;
    glCreateTextures(GL_TEXTURE_2D, 1, &texture);
    glTextureStorage2D(texture, 1, GL_RGB8, 32, 32);
    glTextureSubImage2D(
        texture, 
        // level, xoffset, yoffset, width, height
        0, 0, 0, 32, 32, 
        GL_RGB, GL_UNSIGNED_BYTE, 
        (const void *)&textureData[0]);
    glGenerateTextureMipmap(texture);

    // Retrieve the texture handle after we finish creating the texture
    const GLuint64 handle = glGetTextureHandleARB(texture);
    if (handle == 0) {
        std::cerr << "Error! Handle returned null" << std::endl;
        exit(-1);
    }

    textures.push_back(texture);
    textureHandles.push_back(handle);
}

Должна быть какая-то проверка, чтобы убедиться, что возвращенный дескриптор действительно имеет смысл. Если случилось что-то плохое, он вернет 0.

Далее нам нужно упаковать все дескрипторы текстур в другой SSBO.

Упаковка дескрипторов в SSBO

GLuint textureBuffer;
glCreateBuffers(1, &textureBuffer);
glNamedBufferStorage(
    textureBuffer,
    sizeof(GLuint64) * textureHandles.size(),
    (const void *)textureHandles.data(),
    GL_DYNAMIC_STORAGE_BIT
);

Когда у нас есть все это, мы готовы войти в цикл рендеринга!

Создание резидентных маркеров и рисование

glEnable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Mark all as resident
for (GLuint64 handle : textureHandles) {
    glMakeTextureHandleResidentARB(handle);
}

// 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 the SSBOs
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, verticesBuffer);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, modelMatricesBuffer);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, textureBuffer);

glDrawArraysInstanced(GL_TRIANGLES, 0, numVertices, numInstances);

glUseProgram(0);

// Mark all as non-resident - can be skipped if you know the same textures
// will all be used for the next frame
for (GLuint64 handle : textureHandles) {
    glMakeTextureHandleNonResidentARB(handle);
}

Взгляд на шейдеры

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

Еще одна очень важная вещь — не забывать использовать #extension GL_ARB_bindless_texture : require, поскольку текстуры без привязки никогда не попадали в ядро ​​OpenGL. Нам нужно будет поместить это как в вершинный, так и во фрагментный шейдер.

shader.vs

#version 460 core

#extension GL_ARB_bindless_texture : require

// This matches the C++ definition
struct VertexData {
    float position[3];
    float uv[2];
    float normal[3];
};

// readonly SSBO containing the data
layout(binding = 0, std430) readonly buffer ssbo1 {
    VertexData data[];
};

layout(binding = 1, std430) readonly buffer ssbo2 {
    mat4 modelTransforms[];
};

uniform mat4 projection;
uniform mat4 view;

// Helper functions to manually unpack the data into vectors given an index
vec3 getPosition(int index) {
    return vec3(
        data[index].position[0], 
        data[index].position[1], 
        data[index].position[2]
    );
}

vec2 getUV(int index) {
    return vec2(
        data[index].uv[0], 
        data[index].uv[1]
    );
}

vec3 getNormal(int index) {
    return vec3(
        data[index].normal[0], 
        data[index].normal[1], 
        data[index].normal[2]
    );
}

smooth out vec2 fsUv;
flat out vec3 fsNormal;
flat out int fsInstance;

void main()
{
    mat4 vp = projection * view;
    vec4 position = vec4(getPosition(gl_VertexID), 1.0);
    gl_Position = vp * modelTransforms[gl_InstanceID] * position;

    fsUv = getUV(gl_VertexID);
    fsNormal = getNormal(gl_VertexID);
    // Fragment shader needs this to select one of the 1600 available textures
    fsInstance = gl_InstanceID;
}

В приведенном ниже фрагментном шейдере вы заметите, что SSBO только для чтения имеет внутри себя тип «sampler2D». В зависимости от типа текстуры, которую вы используете, это должно работать с sampler2D, sampler3D, samplerCube и т. д. Все они требуют только передачи 64-битных дескрипторов в качестве данных SSBO.

фрагмент.vs

#version 460 core

#extension GL_ARB_bindless_texture : require

// SSBO containing the textures
layout(binding = 2, std430) readonly buffer ssbo3 {
    sampler2D textures[];
};

smooth in vec2 fsUv;
flat in vec3 fsNormal;
flat in int fsInstance;

out vec4 color;

void main() {
    // Select texture based on instance
    sampler2D tex = textures[fsInstance];
    // Read from the texture with the normal texture() GLSL function
    color = vec4(texture(tex, fsUv).rgb, 1.0);
}

Запуск этой программы дает следующий результат на GTX 1060:

Заключение

Текстуры без привязки — очень мощный инструмент для увеличения количества текстур, к которым может получить доступ шейдер. Он дополняет старые методы использования массивов текстур или атласов текстур, а для программ, которые тратят много времени на привязку/отвязку текстур между вызовами отрисовки, может предложить множество улучшений производительности.

Основные недостатки связаны с тем, что: а) он не является основным в GL 4.6, б) не все графические отладчики поддерживают его, и в) за исключением Nvidia и AMD, которые имеют сильную поддержку без привязки, не все поставщики (особенно мобильные) будут их поддерживать.

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