Рубрики
3D мир Программирование

OpenGL 8: буфер кадра

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

Введение

По умолчанию рендер фрагментов в библиотеке OpenGL происходит в базовый буфер кадра, который копируется на поверхность окна, когда происходит вызов функции переноса (для GLFW3 это glfwSwapBuffers). Такой буфер создается автоматически оконной библиотекой при создании окна.

Буферы кадров могут использоваться для создания эффектов зеркал или постобработки.

Создание кадрового буфера

Для создания буферов кадров используется функция glGenFramebuffers, которая принимает в качестве параметров количество создаваемых буферов и адрес массива для записи дескрипторов. Пример создания одного буфера кадра:

GLuint fbo;
glGenFramebuffers(1, &fbo);

Для использования пользовательского буфера кадра необходимо вызвать функцию glBindFramebuffer, которая принимает в качестве аргументов:

  • target — режим работы с буфером кадра:
    • GL_DRAW_FRAMEBUFFER — доступ только для записи,
    • GL_READ_FRAMEBUFFER — доступ только для чтения,
    • GL_FRAMEBUFFER — полный доступ (чтение и запись в данный буфер);
  • framebuffer — дескриптор используемого буфера.

Пример привязки созданного буфера для рендера:

glBindFramebuffer(GL_FRAMEBUFFER, fbo);  

Для использования базового буфера кадра необходимо передать значение 0 в качестве дескриптора:

glBindFramebuffer(GL_FRAMEBUFFER, 0);  

После создания буфера необходимо:

  • создать текстуры и привязать их к буферу;
  • сообщить точки привязки текстур для записи результата (если текстур больше чем одна);
  • сгенерировать рендер-буфер и связать его с буфером кадра.

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

Создание текстур для буфера кадра

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

class Texture
{
    public:
...
        Texture(GLuint width, GLuint height, GLuint attachment, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Конструктор текстуры заданного размера для использования в буфере
...

Данный конструктор принимает в качестве аргументов размеры текстуры, точку привязки, внутренний формат (конфигурация каналов — по умолчанию GL_RGBA), формат пикселя (по умолчанию GL_RGBA) и тип данных для конкретного канала (по умолчанию GL_FLOAT). Его реализация:

Texture::Texture(GLuint width, GLuint height, GLuint attachment, GLuint texType, GLint internalformat, GLint format, GLenum dataType) : type(texType)
{
    // Генерация текстуры заданного размера
    glGenTextures(1, &handler);
    glBindTexture(GL_TEXTURE_2D, handler);
    glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, dataType, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // Привязка к буферу кадра
    glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, handler, 0);

    // Создаем счетчик использований дескриптора
    handler_count[handler] = 1;
}

Добавим метод Texture::setType для переключения типа текстуры, который влияет на точку привязки:

// Задает тип текстуры
void Texture::setType(GLuint type)
{
    this->type = type;
}

После создания такой текстуры необходимо привязать её к созданному буферу кадра с помощью функции glFramebufferTexture2D (для двумерной текстуры), которая принимает следующие аргументы:

  • target — режим работы с буфером кадра:
    • GL_DRAW_FRAMEBUFFER — доступ только для записи,
    • GL_READ_FRAMEBUFFER — доступ только для чтения,
    • GL_FRAMEBUFFER — полный доступ (чтение и запись в данный буфер);
  • attachment — точка привязки текстуры:
    • GL_COLOR_ATTACHMENT0GL_COLOR_ATTACHMENT12 — для текстур с цветами,
    • GL_DEPTH_ATTACHMENT — для текстур со значениями буфера глубины,
    • GL_STENCIL_ATTACHMENT — для текстур со значениями буфера трафарета;
  • textarget — тип текстуры (= GL_TEXTURE_2D);
  • texture — дескриптор текстуры;
  • level — уровень mipmap-текстуры (= 0).

Пример использования этой функции:

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, texture_handler, 0);

Примечание: режим работы с буфером кадра может быть полезен для копирования содержимого (например глубины) между двумя буферами — один подключается в режиме GL_READ_FRAMEBUFFER, а второй GL_DRAW_FRAMEBUFFER, после чего вызывается функция glBlitFramebuffer.

Функция glBlitFramebuffer служит для переноса содержимого между буферами и принимает следующие аргументы:

  • srcX0, srcY0, srcX1, srcY1 — координаты, описывающие позицию и размер переносимого прямоугольника;
  • dstX0, dstY0, dstX1, dstY1 — координаты, описывающие позицию и размер назначения переноса;
  • mask — битовая маска флагов, определяющая переносимые данные:
    • GL_COLOR_BUFFER_BIT — цвет,
    • GL_DEPTH_BUFFER_BIT — глубина,
    • GL_STENCIL_BUFFER_BIT — трафарет;
  • filter — определяет интертероляцию при растяжении прямоугольника:
    • GL_NEAREST — значение берется от ближайшего пикселя,
    • GL_LINEAR — значение смешивается линейно.

Пример использования нового конструктора:

Texture colors(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT0);

Текстура может использоваться для хранения данных о буфере глубины или трафарета, тогда её формат должен быть одним из следующих вариантов:

  • GL_DEPTH_COMPONENT;
  • GL_STENCIL_INDEX.

Для всех созданных текстур следует указать используемые точки привязки в кадровом буфере, вызвав функцию glDrawBuffers, которая принимает в качестве параметров количество элементов и адрес массива с данными о точках привязки. Пример использования:

GLuint attachments[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(sizeof(attachments) / sizeof(GLuint), attachments);

Для каждой точки привязки необходимо обозначить соответствие в шейдерной программе:

layout (location = 0) out vec4 colors;
layout (location = 1) out vec4 normals;

Буфер рендера

Для хранения данных буфера глубины или трафарета (stencil) используются буферы рендера, которые по сравнению с текстурой являются оптимизированными для хранения внеэкранных данных, так как не производится приведение к формату текстуры. Явного доступа к таким данных нет, но они являются необходимыми.

Для создания таких буферов используется функция glGenRenderbuffers, которая принимает в качестве параметров количество создаваемых буферов и адрес массива для записи дескрипторов. Пример создания одного рендер-буфера:

GLuint rbo;
glGenRenderbuffers(1, &rbo);

Функция glBindRenderbuffer позволяет сделать буфер рендера активным, первым аргументом обязательно является значение GL_RENDERBUFFER, а вторым дескриптор буфера.

После активации буфера необходимо задать формат данных и размеры буфера с помощью функции glRenderbufferStorage, которая принимает следующие параметры:

  • target — должно быть GL_RENDERBUFFER;
  • internalformat — формат хранения данных:
    • GL_DEPTH_COMPONENT — буфер глубины,
    • GL_STENCIL_INDEX — буфер трафарета,
    • GL_DEPTH24_STENCIL8 — 24 бита на буфер глубины, 8 бит на буфер трафарета;
  • width и height — размеры буфера.

Теперь необходимо подключить буфер рендера к буферу кадра с помощью функции glFramebufferRenderbuffer, которая принимает следующие аргументы (почти аналогичны функции привязки текстуры):

  • target — режим работы с буфером кадра:
    • GL_DRAW_FRAMEBUFFER — доступ только для записи,
    • GL_READ_FRAMEBUFFER — доступ только для чтения,
    • GL_FRAMEBUFFER — полный доступ (чтение и запись в данный буфер);
  • attachment — точка привязки буфера рендера:
    • GL_DEPTH_ATTACHMENT — для текстур со значениями буфера глубины,
    • GL_STENCIL_ATTACHMENT — для текстур со значениями буфера трафарета;
  • renderbuffertarget — должно быть GL_RENDERBUFFER;
  • renderbuffer — дескриптор буфера рендера.

Пример буфера рендера для хранения данных для тестов глубины и трафарета:

glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); 
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); 

Пример буфера рендера для хранения данных для глубинного теста:

glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo);

Пример использования буфера кадра

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

Создадим буфер с двумя текстурами и буфером рендера (глубина), одна будет содержать данные о цвете, а вторая о нормалях в кадре:

    // Создадим буфер кадра и сделаем его активным
    GLuint fbo;
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    // Создадим текстуры для буфера кадра
    Texture colors(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT0);
    Texture normals(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT1, 0, GL_RGBA16F);
    // Укажем буферу используемые точки привязки текстур
    GLuint attachments[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(sizeof(attachments) / sizeof(GLuint), attachments);
    // Создадим буфер рендера под буфер глубины и привяжем его
    unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, WINDOW_WIDTH, WINDOW_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo);
    // Базовый буфер кадра
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

Важное замечание: для текстуры нормалей используется форма GL_RGBA16F, так как стандартный GL_RGBA отсекает записываемые значения канала, которые выходят за пределы диапазона [0;1].

Результат будет переноситься простым шейдером, который будет рендерить прямоугольник и переносить на него результаты работы буфера кадра.

Текст вершинного шейдера shaders/quad.vert:

#version 420 core 

layout(location = 0) in vec3 pos; 

out vec2 texCoord;

void main() 
{ 
    gl_Position = vec4(pos, 1.0);
    texCoord = (pos.xy + vec2(1.0)) / 2; // Переход от [-1;1] к [0;1]
}

Текст фрагментного шейдера shaders/quad.frag:

#version 420 core 

in vec2 texCoord;

uniform sampler2D tex;

out vec4 color; 

void main() 
{ 
    color = texture(tex, texCoord);
} 

Загрузим данные шейдеры в файле src/main.cpp:

    // Шейдер для переноса текстуры на прямоугольник
    ShaderProgram quadProgram;
    // Загрузка и компиляция шейдеров
    quadProgram.load(GL_VERTEX_SHADER, "shaders/quad.vert");
    quadProgram.load(GL_FRAGMENT_SHADER, "shaders/quad.frag");
    quadProgram.link();

В фрагментном шейдере shaders/shader.frag, отвечающем за цвет, необходимо задать расположение для выходных результатов и произвести запись данных:

...
uniform sampler2D tex_diffuse;
uniform sampler2D tex_ambient;
uniform sampler2D tex_specular;

layout (location = 0) out vec4 colors;
layout (location = 1) out vec4 normals;

void main() 
{ 
...
    colors = vec4(light_f.color*ka, 1)*texture(tex_ambient, texCoord) + vec4(light_f.color*kd*diffuse, 1)*texture(tex_diffuse, texCoord) + vec4(light_f.color*ks*specular, 1)*texture(tex_specular, texCoord);
    normals = vec4(N, 1.0);
}

Создадим модель прямоугольника в файле src/main.cpp:

    glm::vec3 quadVertices[] = { {-1.0f,  1.0f, 0.0f}
                               , {-1.0f, -1.0f, 0.0f}
                               , { 1.0f,  1.0f, 0.0f}
                               , { 1.0f, -1.0f, 0.0f}
                               };

    GLuint quadIndices[] = {0,1,2,1,2,3};

    Model quadModel;
    quadModel.load_verteces(quadVertices, 4);
    quadModel.load_indices(quadIndices, 6);

Для рендера объекта без загрузки uniform-данных добавим вспомогательный метод Model::render без параметров в файле src/Model.cpp:

void Model::render()
{
    // Подключаем VAO
    vao.use();
    // Если есть индексы - рисуем с их использованием
    if (indices_count)
    {
        index_vbo.use();
        glDrawElements(GL_TRIANGLES, indices_count, GL_UNSIGNED_INT, (void*)(first_index_byteOffset));
    }
    // Если есть вершины - рисуем на основании массива вершин
    else if (verteces_count)
        glDrawArrays(GL_TRIANGLES, 0, verteces_count);
}

В файле include/Model.h:

// Класс модели
class Model : public Node
{
    public:
...
        void render(); // Вызов отрисовки без uniform-данных
        void render(const GLuint &model_uniform, UBO &material_buffer); // Вызов отрисовки
...
};

Замечание: для обеспечения повторного использования кода необходимо заменить вызовом нового метода аналогичный фрагмент из метода рендера с отправкой uniform-данных.

Активируем буфер кадра, в который будет производится рендер сцены, и активируем «базовый» шейдер перед очисткой экрана:

...
    while(!glfwWindowShouldClose(window))
    {
        // Активируем буфер кадра
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        // Используем шейдер с освещением
        base.use();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
...

Примечание: для каждого буфера кадра необходим свой вызов функции очистки.

Если сейчас запустить программу то экран будет полностью черным.

После рендера сцены (перед вызовом функции glfwSwapBuffers) активируем базовый буфер кадра и подключим шейдер для прямоугольника, после чего вызовем функцию очистки:

        // Активируем базовый буфер кадра
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        // Подключаем шейдер для прямоугольника
        quadProgram.use();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Далее необходимо подключить текстуру, которая будет использоваться для поверхности рисуемого прямоугольника, и отправить прямоугольник на рендер:

        // Подключаем текстуру цветов
        colors.use();
        // Рендерим прямоугольник
        quadModel.render();

Далее идут рисунки с результатами работы буфера кадра, для который объект расположен в координатах (0;0;1), а камера смотрит на него из координат (0;0;0).

Результат работы примера представлен на рисунке 1.

Рисунок 1 — Результат работы примера с выводом текстуры цветов

Для вывода нормалей необходимо заменить используемую текстуру:

        // Подключаем текстуру нормалей
        normals.use();
        // Рендерим прямоугольник
        quadModel.render();

На рисунке 2 изображен результат вывода нормалей, когда объект находится на координатах (0;0;0), а камера смотрит на него из координат (0;0;-1) — позади объекта (ось X направлена влево, Z — по направлению взгляда камеры). Текстура нормалей содержит отрицательные значения, но они отсекаются текстурой стандартного буфера кадра, которая переносится на поверхность окна. Ошибки тут нет, только специфичная позиция камеры, которая влияет на конечный результат.

Рисунок 2 — Вывод нормалей, когда камера расположена позади объекта

Для получения корректного результата при прямом переносе из текстуры нормалей можно переместить камеру в координаты (0;0;1) — перед объектом (направив её на объект). В данном случае ось X направлена вправо, а ось Z направлена в камеру. Результат перестановки камеры представлен на рисунке 3.

Рисунок 3 — Вывод нормалей, направленных по направлению осей, когда камера расположена перед объектом

Рисунки 2 и 3 следует трактовать следующим образом: компонента X отвечает за красный цвет (R), чем больше значение красного, тем больше нормаль направлена по оси Х. Аналогично для компонент Y — зеленый цвет (G) и Z — синий цвет (B).

Текущая версия доступна на теге v0.1 в репозитории 08.

Класс FBO

Для простоты работы с буферами кадра объявим класс FBO в файле include/Buffers.h:

class FBO
{
    public:
        FBO(GLuint *attachments, int count); // Создает буфер кадра с нужным числом прикреплений текстур
        ~FBO(); // Уничтожение буфера

        void use(GLuint mode = GL_FRAMEBUFFER); // Активирует буфер кадра в заданном режиме
        static void useDefault(GLuint mode = GL_FRAMEBUFFER); // Активирует базовый буфер в заданном режиме
        void assignRenderBuffer(GLuint hander, GLuint attachment = GL_DEPTH_ATTACHMENT); // Привязка рендер буфера
    protected:
        GLuint handler; // Дескриптор
};

Реализация методов класса FBO:

// Создает буфер кадра с нужным числом прикреплений текстур
FBO::FBO(GLuint *attachments, int count)
{
    glGenFramebuffers(1, &handler);
    use();
    glDrawBuffers(count, attachments);
}

// Уничтожение буфера
FBO::~FBO()
{
    glDeleteFramebuffers(1, &handler);
}

// Активирует буфер кадра в заданном режиме
void FBO::use(GLuint mode) 
{
    glBindFramebuffer(mode, handler);
}

// Активирует базовый буфер в заданном режиме
void FBO::useDefault(GLuint mode)
{
    glBindFramebuffer(mode, 0);
} 

// Привязка рендер буфера
void FBO::assignRenderBuffer(GLuint hander, GLuint attachment)
{
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, hander);
}

Пример использования класса при создании и настройке буфера кадра:

    // Создадим буфер кадра с данными о используемых привязках
    GLuint attachments[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    FBO fbo(attachments, sizeof(attachments) / sizeof(GLuint));
    // Создадим текстуры для буфера кадра
    Texture colors(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT0);
    Texture normals(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT1, 0, GL_RGBA16F);
    // Создадим буфер рендера под буфер глубины и привяжем его
    unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, WINDOW_WIDTH, WINDOW_HEIGHT);
    fbo.assignRenderBuffer(rbo);
    // Активируем базовый буфер кадра
    FBO::useDefault();

Пример использования в цикле рисования:

...
        // Активируем буфер кадра
        fbo.use();
        // Используем шейдер с освещением
        base.use();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
...
        // Активируем базовый буфер кадра
        FBO::useDefault();
        // Подключаем шейдер для прямоугольника
        quadProgram.use();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
...

Объявим класс RBO в файле include/Buffers.h:

class RBO
{
    public:
        RBO(int w, int h, GLuint component = GL_DEPTH_COMPONENT); // Создает буфер рендера с заданными параметрами размеров и используемых компонент
        ~RBO(); // Уничтожение буфера

        GLuint getHandler(); // Возвращает дескриптор буфера рендера
    protected:
        GLuint handler; // Дескриптор
};

Реализация методов класса RBO:

// Создает буфер рендера с заданными параметрами размеров и используемых компонент
RBO::RBO(int w, int h, GLuint component)
{
    glGenRenderbuffers(1, &handler);
    glBindRenderbuffer(GL_RENDERBUFFER, handler);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h);
}

// Уничтожение буфера
RBO::~RBO()
{
    glDeleteRenderbuffers(1, &handler);
}

// Возвращает дескриптор буфера рендера
GLuint RBO::getHandler()
{
    return handler;
}

Пример использования при создании буфера кадра:

    Texture normals(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT1);
    // Создадим буфер рендера под буфер глубины и привяжем его
    RBO rbo(WINDOW_WIDTH, WINDOW_HEIGHT);
    fbo.assignRenderBuffer(rbo.getHandler());
    // Активируем базовый буфер кадра
    FBO::useDefault();

Текущая версия доступна на теге v0.2 в репозитории 08.

Изменение метода render

Теперь методы Model::render и Scene::render необходимо модифицировать, так как он может вызываться для разных шейдеров, то необходимо заменить расположение uniform-переменной в списке аргументов метода ссылкой на используемую шейдерную программу. В файле include/Model.h:

...
#include "Shader.h"
...
class Model : public Node
{
    public:
...
        void render(ShaderProgram &shaderProgram, UBO &material_buffer); // Вызов отрисовки
...

В файле include/Scene.h:

class Scene
{
    public:
...
        void render(ShaderProgram &shaderProgram, UBO &material_buffer); // Рендер сцены
...

В файле src/Model.cpp:

void Model::render(ShaderProgram &shaderProgram, UBO &material_buffer) 
{
    // Загрузим матрицу трансформации
    glUniformMatrix4fv(shaderProgram.getUniformLoc("model"), 1, GL_FALSE, &this->getTransformMatrix()[0][0]);
...

В файле src/Scene.cpp:

// Рендер сцены
void Scene::render(ShaderProgram &shaderProgram, UBO &material_buffer) 
{
    for (auto & model : models)
        model.render(shaderProgram, material_buffer);
}

Тогда в функции main (src/main.cpp) необходимо убрать 108 строку (получение расположения uniform-переменной) и изменить вызов метода отрисовки у модели:

        // Тут производится рендер
        scene.render(base, material_data);  

Текущая версия доступна на теге v0.3 в репозитории 08.

Заключение

В данной заметке были рассмотрена теория и создан пример использования буфера кадра, модифицированы классы текстур и моделей.

Проект доступен в публичном репозитории: 08
Библиотеки: dependencies
Ресурсы: resources

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.