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

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

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

Старый метод

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

main.cpp

// Data is laid out as 3 positions, 2 uvs, 3 normals, repeat
std::vector<float> vertices;

... code to fill vertices with vertex data ...

GLuint vbo;
glGenBuffers(1, &vbo);

// Bind and upload data
glBindBuffer(GL_ARRAY_BUFFER, vbo);  
glBufferData(
    GL_ARRAY_BUFFER, 
    vertices.size(), 
    (const void *)vertices.data(), 
    GL_STATIC_DRAW
);

// Tell OpenGL how to interpret the data and where it should be linked to

// Positions
glVertexAttribPointer(
    0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0
);
glEnableVertexAttribArray(0);

// UVs
glVertexAttribPointer(
    1, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))
);
glEnableVertexAttribArray(1);

// Normals
glVertexAttribPointer(
    2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(5 * sizeof(float))
);
glEnableVertexAttribArray(2);

// Bind program and draw
glUseProgram(program);

glDrawArrays(GL_TRIANGLES, 0, vertices.size());

// Unbind
glUseProgram(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);

Тогда шейдер будет выглядеть примерно так:

shader.vs

#version 330 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 uv;
layout (location = 2) in vec3 normal;

uniform mat4 projection;
uniform mat4 view;

out vec2 fsUv;
out vec3 fsNormal;

void main()
{
    gl_Position = projection * view * vec4(position, 1.0);

    fsUv = uv;
    fsNormal = normal;
}

Новый метод — программируемое извлечение вершин с помощью SSBO

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

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

main.cpp

// We use arrays of floats since they will be tightly packed with the
// layout std430 qualifier
struct VertexData {
    float position[3];
    float uv[2];
    float normal[3];
};

// (inside either main or a different function)
std::vector<VertexData> vertices;
... populate vertices with either hardcoded data or data you load from a file ...

// Create and fill the mesh data buffer
GLuint vertexDataBuffer;
glCreateBuffers(1, &vertexDataBuffer);

glNamedBufferStorage(vertexDataBuffer, 
                     sizeof(VertexData) * vertices.size(),
                     (const void *)vertices.data(), 
                     GL_DYNAMIC_STORAGE_BIT);

// Bind the buffer to location 0 - matches (binding = 0) for ssbo1 in the
// vertex shader listed below
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, vertexDataBuffer);

// Bind program and draw
glUseProgram(program);

glDrawArrays(GL_TRIANGLES, 0, vertices.size());

glUseProgram(0);

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

Внутри вершинного шейдера мы будем использовать встроенный ввод под названием gl_VertexID. При использовании неиндексированного рисунка, такого как glDrawArrays, gl_VertexID будет равен текущему индексу вершины. При использовании индексированного рисунка, такого как glDrawElements, gl_VertexID будет равен текущему индексу из буфера массива элементов.

shader.vs

#version 460 core

// 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[];
};

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

out vec2 fsUv;
out vec3 fsNormal;

void main()
{
    gl_Position = projection * view * vec4(getPosition(gl_VertexID), 1.0);

    fsUv = getUV(gl_VertexID);
    fsNormal = getNormal(gl_VertexID);
}

Больше никаких VBO или VAO. Хороший!

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

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