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

OpenGL 9: освещение ч.2 — отложенный рендер и множество источников освещения

Данная заметка посвящена технологии отложенного рендера и расчету освещенности от множества источников освещения

Введение

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

Данный подход заключается в разделении рендера на два этапа:

  1. проход геометрии (geometry pass);
  2. проход освещения (lighting pass).

Первый этап рендерится в G-буфер (geometry buffer), в котором сохраняются данные фрагментов (в каждом пикселе соответствующих текстур):

  • позиция в пространстве;
  • нормали;
  • коэффициент фонового освещения;
  • коэффициент диффузного освещения;
  • коэффициент зеркального освещения;
  • коэффициент глянцевости.

Некоторые данные можно записать в канал прозрачности (alpha) для уменьшения количества буферов.

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

Содержание заметки:

Реализация отложенного рендера

Реализация основывается на прошлой заметке, посвященной буферам. Перед началом работы необходимо заменить объект base класса Shader на объект gShader:

    // Шейдер для G-буфера
    ShaderProgram gShader;
    // Загрузка и компиляция шейдеров
    gShader.load(GL_VERTEX_SHADER, "shaders/gshader.vert");
    gShader.load(GL_FRAGMENT_SHADER, "shaders/gshader.frag");
    // Установим значения текстур
    const char* textures_shader_names[] = {"tex_diffuse", "tex_ambient", "tex_specular"};
    gShader.bindTextures(textures_shader_names, sizeof(textures_shader_names)/sizeof(const char*));

А так же в цикле рисования:

        // Используем шейдер для G-буфера
        gShader.use();

Вершинному шейдеру, используемому в G-буфере, необходимы данные о камере из uniform-буфера Camera. В результате работы он будет передавать на фрагментный шейдер данные о позиции вершины в пространстве, трансформированной нормали и текстурных координатах. Содержимое файла shaders/gshader.vert:

#version 420 core 

layout(location = 0) in vec3 pos; 
layout(location = 1) in vec2 inTexCoord;
layout(location = 2) in vec3 normals; 

layout(std140, binding = 0) uniform Camera
{
    mat4 projection;
    mat4 view;
    vec3 position;
} camera;

uniform mat4 model;

out vec3 vertex; // Позиция вершины в пространстве
out vec3 N; // Нормаль трансформированноая
out vec2 texCoord; // Текстурные координаты

void main() 
{ 
    vec4 P = model * vec4(pos, 1.0); // трансформация вершины
    vertex = P.xyz;

    N = normalize(mat3(model) * normals); // трансформация нормали

    texCoord = inTexCoord; // Текстурные координаты

    gl_Position = camera.projection * camera.view * P;
} 

Вершинному шейдеру, используемому в G-буфере, необходимы данные о материале из uniform-буфера Material. Данный шейдер использует диффузную, фоновую и зеркальную текстуры.

Данные (без дополнительных преобразований) о положении записываются в текстуру gPosition, нормали в gNormal. Для уменьшения количества буферов данные о глянцевости материала будут записаны в канал прозрачности (alpha) текстуры, содержащей данные о диффузной составляющей, с названием gDiffuseP, а данные с текстуры зеркальности (значения в каналах одинаковы, следовательно можно взять только один) в канал прозрачности текстуры, содержащей данные о фоновой составляющей, с названием gAmbientSpecular. Данные о материале перемножаются с цветовыми векторами соответствующих текстур.

Содержание фрагментного шейдера из файла shaders/gshader.frag:

#version 420 core 

layout(std140, binding = 1) uniform Material
{
    vec3 ka;
    vec3 kd;
    vec3 ks;
    float p;
};

layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gDiffuseP;
layout (location = 3) out vec4 gAmbientSpecular;

in vec3 vertex; // Позиция вершины в пространстве
in vec3 N; // Нормаль трансформированная
in vec2 texCoord; // Текстурные координаты

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

void main()
{    
    // Сохранение позиции фрагмента в G-буфере
    gPosition = vertex;
    // Сохранение нормали в G-буфере
    gNormal = N;
    // Сохранение диффузного цвета
    gDiffuseP.rgb = texture(tex_diffuse, texCoord).rgb * kd;
    // Сохранение глянцевости
    gDiffuseP.a = p;
    // Сохранение фоновой составляющей
    gAmbientSpecular.rgb = texture(tex_ambient, texCoord).rgb * ka;
    // Сохранение зеркальной составляющей
    gAmbientSpecular.a = texture(tex_specular, texCoord).r * ks.r;
}

В файле src/main.cpp заменим старый буфер fbo на gbuffer, а так же создадим необходимые для шейдеров текстуры:

    // Создадим G-буфер с данными о используемых привязках
    GLuint attachments[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
    FBO gbuffer(attachments, sizeof(attachments) / sizeof(GLuint));
    // Создадим текстуры для буфера кадра
    Texture gPosition(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT0, 0, GL_RGB16F, GL_RGB); // Позиция вершины
    Texture gNormal(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT1, 1, GL_RGB16F, GL_RGB); // Нормали
    Texture gDiffuseP(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT2, 2, GL_RGBA16F); // Диффузная составляющая и коэф. глянцевости
    Texture gAmbientSpecular(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT3, 3); // Фоновая составляющая и один канал зеркальной
    // Создадим буфер рендера под буфер глубины и привяжем его
    RBO grbo(WINDOW_WIDTH, WINDOW_HEIGHT);
    gbuffer.assignRenderBuffer(grbo.getHandler());

А так же в цикле рисования:

        // Активируем G-буфер
        gbuffer.use();

Для работы с освещением используются шейдер shaders/lighting.frag. В роли вершинного шейдера используется старый shaders/quad.vert.

Фрагментный шейдер использует данные о положении камеры из uniform-буфера Camera и информацию об источнике света Light.

Из текстур берутся данные записанные G-буфером:

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gDiffuseP;
uniform sampler2D gAmbientSpecular;

out vec4 color; 

void main() 
{ 
    // Получим данные из текстур буфера
    vec3 fragPos = texture(gPosition, texCoord).rgb;
    vec3 N = texture(gNormal, texCoord).rgb;
    vec3 kd = texture(gDiffuseP, texCoord).rgb;
    vec3 ka = texture(gAmbientSpecular, texCoord).rgb;
    float ks = texture(gAmbientSpecular, texCoord).a;
    float p = texture(gDiffuseP, texCoord).a;

Для модели Блинна-Фонга потребуется вычислить векторы камеры и источника света относительно фрагмента:

    // Данные о камере относительно фрагмента
    vec3 Cam_vertex = normalize(camera.position - fragPos);
    // Данные об источнике отностиельно фрагмента
    vec3 L_vertex = normalize(light_f.position - fragPos);

Далее вычисляется освещенность фрагмента:

    // Диффузная составляющая
    float diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
    
    // Вектор половины пути
    vec3 H = normalize(L_vertex + Cam_vertex);
    // Зеркальная составляющая
    float specular = pow(max(dot(H, N), 0.0), p*4); // скалярное произведение с отсеканием значений < 0 в степени p

    color = vec4(ka, 1)
          + vec4(light_f.color*kd*diffuse, 1) 
          + vec4(light_f.color*ks*specular, 1);
} 

В файле src/main.cpp заменим фрагмент с загрузкой старых шейдеров, используемых при рисовании прямоугольника:

    // Шейдер для расчета освещенности
    ShaderProgram lightShader;
    // Загрузка и компиляция шейдеров
    lightShader.load(GL_VERTEX_SHADER, "shaders/quad.vert");
    lightShader.load(GL_FRAGMENT_SHADER, "shaders/lighting.frag");
    const char* gtextures_shader_names[]  = {"gPosition", "gNormal", "gDiffuseP", "gAmbientSpecular"};
    lightShader.bindTextures(gtextures_shader_names, sizeof(gtextures_shader_names)/sizeof(const char*));

При привязки текстур определим к каким слотам относятся текстуры из G-буфера с помощью метода Texture::setType.

В цикле рисования используем новые шейдеры, подключив используемые текстуры:

        // Активируем базовый буфер кадра
        FBO::useDefault();
        // Подключаем шейдер для расчета освещения
        lightShader.use();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // Подключаем текстуры G-буфера
        gPosition.use();
        gNormal.use();
        gDiffuseP.use();
        gAmbientSpecular.use();
        // Рендерим прямоугольник с расчетом освещения
        quadModel.render();

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

Множество источников освещения

Есть два способа обработки множества источников света:

  1. индивидуальный вызов прямоугольника для каждого источника с применением смешивания;
  2. загрузка массива в uniform-буфер и последующая обработка в цикле на фрагментном шейдере.

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

Пример такого метода:

    Bulb bulb_array[60];
    glBlendFunc(GL_ONE, GL_ONE); // Смешивание цветов один к одному
    // Отдельный шейдер для фонового освещения
...
    while(!glfwWindowShouldClose(window))
    {
...
        // Отключаем тест глубины
        glDisable(GL_DEPTH_TEST);
        // Активируем базовый буфер кадра
        FBO::useDefault();
        // Очистка буфера цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // Подключаем шейдер для вывода фоновой составляющей из текстуры
        ambientShader.use();
        // Подключаем текстуру G-буфера
        gAmbientSpecular.use();
        // Рендерим прямоугольник с фоновым освещением
        quadModel.render();
        // Включаем смешивание
        glEnable(GL_BLEND);
        // Подключаем шейдер для расчета освещения
        lightShader.use();
        // Подключаем текстуры G-буфера
        gPosition.use();
        gNormal.use();
        gDiffuseP.use();
        gAmbientSpecular.use();

        // Цикл по источникам света
        for (int i = 0; i < 60; i++)
        {
            // Загружаем информацию об источнике света
            array[i].upload(light_data);
            // Рендерим прямоугольник с расчетом освещения
            quadModel.render();
        }
        // Выключаем смешивание и включаем тест глубины
        glDisable(GL_BLEND); 
        glEnable(GL_DEPTH_TEST);
...

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

Для реализации второго метода создадим массив источников света (LightData) с условным максимальным числом источников (например 300).

Добавим константу максимального числа источников в файле include/Lights.h:

// Максимальное число источников света
#define MAX_LIGHTS 300

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

// Источник света
class Light
{
    public:
        static int getUBOsize(); // Возвращает размер буфера в байтах
        static void upload(UBO& lights_data); // Загрузка данных в буфер

        static GLuint count; // количество используемых источников (должно быть <= MAX_LIGHTS)
        static LightData data[MAX_LIGHTS]; // Массив данных по источникам света
};

Необходимо подключить заголовочный файл Buffers.h для возможности работы с классом UBO.

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

В новом файле src/Lights.cpp определим статические переменные и методы:

#include "Lights.h"

GLuint Light::count = 0; // количество используемых источников (должно быть <= MAX_LIGHTS)
LightData Light::data[MAX_LIGHTS]; // Массив данных по источникам света

// возвращает размер буфера в байтах
int Light::getUBOsize()
{
    return sizeof(LightData) * MAX_LIGHTS + sizeof(GLuint);
}

// Загрузка данных в буфер
void Light::upload(UBO& lights_data)
{
    GLuint LightDataSize = sizeof(LightData); // Одного экземпляра структуры LightData

    if (count)
        lights_data.loadSub(data, sizeof(LightData)*count); // Загрузка данных об источниках

    // Загружаем кол-во источников
    lights_data.loadSub(&count, sizeof(count), LightDataSize*MAX_LIGHTS);
}

В файле src/main.cpp изменим работу с буфером источников:

    // Источник света
    LightData light = { {1.0f, 3.0f, 0.0f} // позиция
                      , {1.0f, 1.0f, 1.0f} // цвет
                      };
    // Источники света
    // Первый способ измений
    Light::data[0] = { {0.3f, 0.1f, 0.5f} // позиция
                      , {1.0f, 0.0f, 0.0f} // цвет
                      };
    Light::count++;
    // Второй способ
    Light::data[Light::count].position = {-0.3f, -0.1f,  0.5f}; // позиция
    Light::data[Light::count++].color =  { 0.0f,  0.0f,  1.0f}; // цвет
    

    // Uniform-буферы
    UBO cameraUB(sizeof(CameraData), 0);
    UBO material_data(sizeof(Material), 1);
    UBO light_data(&light, sizeof(LightData), 2);
    UBO light_data(Light::getUBOsize(), 2);

По преведенному фрагменту можно заметить, что есть два подхода к организации источников света.

Добавим загрузку информации по источникам в начало цикла рисования:

    // Пока не произойдет событие запроса закрытия окна
    while(!glfwWindowShouldClose(window))
    {
        // Загрузим информацию об источниках света
        Light::upload(light_data);

        // Активируем G-кадра
        gbuffer.use();

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

В фрагментном шейдере shaders/lighting.frag необходимо изменить uniform-блок:

struct LightData
{
    vec3 position;
    vec3 color;
};

layout(std140, binding = 2) uniform Light
{
    LightData data[300];
    int count;
} light_f;

Теперь можно изменить вид функции main из файла shaders/lighting.frag:

void main() 
{ 
    // Получим данные из текстур буфера
    vec3 fragPos = texture(gPosition, texCoord).rgb;
    vec3 N = texture(gNormal, texCoord).rgb;
    vec3 kd = texture(gDiffuseP, texCoord).rgb;
    vec3 ka = texture(gAmbientSpecular, texCoord).rgb;
    float ks = texture(gAmbientSpecular, texCoord).a;
    float p = texture(gDiffuseP, texCoord).a;
    
    // Переменные используемые в цикле:
    vec3 L_vertex; // Данные об источнике относительно фрагмента
    float L_distance; // Расстояние от поверхности до источника
    vec3 Cam_vertex; // Данные о камере относительно фрагмента
    float diffuse; // Диффузная составляющая
    vec3 H; // Вектор половины пути
    float specular; // Зеркальная составляющая
    float attenuation; // Угасание с учетом расстояния

    
    // Фоновая освещенность
    color = vec4(ka, 1);

    // Цикл по источникам света
    int i;
    for (i = 0; i < light_f.count; i++)
    {
        // Данные об источнике относительно фрагмента
        L_vertex = light_f.data[i].position - fragPos;

        // Нормирование вектора
        L_vertex = normalize(L_vertex);

        // Данные о камере относительно фрагмента
        Cam_vertex = normalize(camera.position - fragPos);

        // Диффузная составляющая
        diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
        
        // Вектор половины пути
        H = normalize(L_vertex + Cam_vertex);
        // Зеркальная составляющая
        specular = pow(max(dot(H, N), 0.0), p*4); // скалярное произведение с отсеканием значений < 0 в степени p

        color += vec4(light_f.data[i].color*kd*diffuse, 1) 
              +  vec4(light_f.data[i].color*ks*specular, 1);
    }
} 

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

Рисунок 1 — Красный и синий источники освещения с красной каплей

По рисунку 3 можно отметить отсутствие вклада синего источника света, так как материал модели в диффузной составляющей отражает только 2% зеленой и синей компонент, в то время как красная отражает 53% падающего света. Дополнительно можно изменить фоновую освещенность капли на серый цвет. Для решения данной проблемы можно изменить файл с материалами, либо изменить материал после загрузки. Для демонстрации проведем изменения после загрузки модели в файле src/main.cpp:

    // Загрузка сцены из obj файла
    Scene scene = loadOBJtoScene("../resources/models/blob.obj", "../resources/models/", "../resources/textures/"); 
    scene.root.e_scale() = glm::vec3(0.01);
    scene.root.e_position().z = 1;
    scene.models.begin()->material.kd = {0.5,0.5,0.5};
    scene.models.begin()->material.ka = {0.2,0.2,0.2};

Результат после изменения материала капли изображен на рисунке 2.

Рисунок 2 — Красный и синий источники освещения с серой каплей

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

Источник света как часть сцены

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

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

В файле include/Lights.h дополним класс Light наследованием от Node:

#include "Buffers.h"
#include "Model.h"
...

// Источник света
class Light : public Node
{
...

Теперь класс Light может являться частью сцены и заполнять собственные данные в массив на отправку. Для доступа к нужному элементу массива добавим приватное поле Light::index, которое будет указывать на нужный индекс в статическом поле-массиве по LightData. Через этот индекс будет производится дефрагментация массива на отправку источников в случае удаления источника из середины.

Для соблюдения правила «ленивого» дерева сцены (расчет данных только по необходимости) добавим приватный вспомогательный флаг Light::uploadReq, так же следует перенести статические счетчик источников и массив в приватную область видимости:

// Источник света
class Light : public Node
{
    public:
        static int getUBOsize(); // Возвращает размер буфера в байтах
        static void upload(UBO& lights_data); // Загрузка данных в буфер

    private:
        int index; // Индекс в массиве отправки (может не совпадать с lights) для дефрагментированного доступа

        bool uploadReq; // Необходимость загрузки в следствии изменений

        static GLuint count; // количество используемых источников (должно быть <= MAX_LIGHTS)
        static LightData data[MAX_LIGHTS]; // Массив данных по источникам света
};

Для контроля за флагом Light::uploadReq необходимо перегрузить метод Light::recalcMatrices (приватный):

// Источник света
class Light : public Node
{
...
    private:
...
        bool uploadReq; // Необходимость загрузки в следствии изменений

        virtual void recalcMatrices(); // Метод пересчета матрицы трансформации по необходимости, должен сбрасывать флаг changed
...
};

Реализация данного метода в файле src/Lights.cpp:

// Метод пересчета матрицы трансформации по необходимости, должен сбрасывать флаг changed
void Light::recalcMatrices()
{
    // Если были изменения - необходимо загрузить данные
    if (changed || parent_changed)
        uploadReq = true;
    
    // Выполняем вычисление матриц методом родительского класса
    Node::recalcMatrices();
}

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

Добавим поле цвета и методы, свойственные классу Node, для константного и неконстантного доступа к полю в файле include/Lights.h:

// Источник света
class Light : public Node
{
    public:
        static int getUBOsize(); // Возвращает размер буфера в байтах
        static void upload(UBO& lights_data); // Загрузка данных в буфер

        const glm::vec3& c_color() const; // Константный доступ к цвету
        glm::vec3& e_color(); // Неконстантная ссылка для изменений цвета
    private:
        glm::vec3 color; // Цвет
...

Реализация этих методов в файле src/Lights.cpp:

// Константный доступ к цвету
const glm::vec3& Light::c_color() const
{
    return color;
}

// Неконстантная ссылка для изменений цвета
glm::vec3& Light::e_color()
{
    uploadReq = true; // Изменение цвета требует загрузки в буфер

    return color;
}

Теперь можно добавить приватный метод Light::toData, который будет транслировать имеющийся источник в формат структуры, которая отправляется в uniform-буфер. Помимо этого потребуется добавить проверочный метод Light::check_id, что обращение по индексу идет в корректном диапазоне массива на отправку.
Изменения в файле include/Lights.h:

class Light : public Node
{
...
    private:
...
        bool uploadReq; // Необходимость загрузки в следствии изменений
        void check_id(); // Проверка что не взаимодествуем с пустым источником
        void toData(); // Преобразует информацию об источнике в структуру LightData
};

Реализация данных методов в файле src/Lights.cpp:

#include <stdexcept>

// Проверка что не взаимодествуем с пустым источником
void Light::check_id()
{
    if (index < 0
    ||  index >= count)
        throw std::runtime_error("Попытка использовать ссылку на пустой или некорректный источник");
}

// Преобразует информацию об источнике в структуру LightData
void Light::toData()
{
    check_id(); // Проверка на работу с корректным индексом

    data[index].position = glm::vec3(result_transform[3]); // Позиция из матрицы трансформации
    data[index].color = color; // Цвет
}

В случае обращения по индексу, выходящему за пределы используемой части массива активных источников [0; count-1], следует выдать исключение. Для работы с исключениями необходимо подключить заголовочный файл <stdexcept>.

Важное замечание: внимательный читатель мог заметить, что класс в текущей реализации не получает индекс и не инициализирует флаг необходимости загрузки в буфер. Это является следствием проблемы по контролю за изменениями в объектах. Если в случае с деревом можно однозначно сказать какой элемент где находится, то источники должны иметь однозначное сопоставление 1 источник -> 1 индекс в массиве на отправку, в противном случае может возникнуть необходимость отложенной загрузки изменений для объекта который уже был удален. Для решения данной проблемы ограничим возможность создания экземпляров класса Light путем сокрытия конструктора и создания отдельных методов для получения и уничтожения объектов. Чтоб убедиться что у объекта не появится копий следует возвращать пользователю ссылку на объект из массива объектов Light.

Помимо этого потребуется вспомогательный метод для комфортного поиска источника по индексу в массив на отправку, который будет использоваться для поиска источника с заданным индексом или пустого источника с индексом -1.

Дополним класс Light в файле include/Lights.h:

// Источник света
class Light : public Node
{
    public:
        static int getUBOsize(); // Возвращает размер буфера в байтах
        static void upload(UBO& lights_data); // Загрузка данных в буфер

        static Light& getNew(); // Возвращает ссылку на новый источник света
        void destroy(); // Уничтожает источник света
...
    private:
        Light(); // Конструктор без параметров
        Light(const Light& copy) = delete; // Конструктор копирования ОТКЛЮЧЕН
        Light& operator=(const Light& other); // Оператор  присваивания
        virtual ~Light(); 

        static Light& findByIndex(GLuint index); // Возвращает ссылку на источник с нужным индексом

...

        static GLuint count; // количество используемых источников (должно быть <= MAX_LIGHTS)
        static LightData data[MAX_LIGHTS]; // Массив данных по источникам света
        static Light lights[MAX_LIGHTS]; // Массив источников-узлов сцены
};

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

Важное замечание: индекс Light::index используется только для доступа к элементам массива Light::data (структура LightData) и не может быть применен к массиву Light::lights в виду дефрагментации, осуществляемой путем замены индексов между экземплярами Light.

Реализация конструктора, деструктора и методов в файл
е src/Lights.cpp:

Light Light::lights[MAX_LIGHTS]; // Массив источников-узлов сцены

// Возвращает ссылку на новый источник света
Light& Light::getNew() 
{
    Light& refNew = findByIndex(-1);

    refNew.index = count++;
    refNew.uploadReq = true;

    return refNew;
}

// Уничтожает источник света
void Light::destroy()
{
    check_id(); // Проверка на работу с корректным индексом
    // Если удаляемый элемент не последний
    if (count-1 != index)
    {
        // Найдем элемент для замены
        Light& replace = findByIndex(--count);

        replace.uploadReq = true; // Требуется загрузить данные
        replace.index = index; // Заменяем индекс данных
    }
    
    operator=(Light()); // Обнулим источник путем замены на новый
}

// Возвращает ссылку на источник с нужным индексом
Light& Light::findByIndex(GLuint index)
{
    // Если нет источников - возвращаем нулевой
    if (!count)
        return lights[0];

    // Цикл по перебору источников
    for (int i = 0; i < MAX_LIGHTS; i++)
        if (lights[i].index == index)
            return lights[i];

    throw std::runtime_error("Запрашиваемый источник освещения не найден, либо достигнут лимит");
}

// Конструктор без параметров
Light::Light() : Node(), index(-1), uploadReq(false), color(1.0f)
{
    
}

// Оператор  присваивания
Light& Light::operator=(const Light& other)
{
    // Проверка на самоприсваивание
    if (this != &other) 
    {
        index = other.index; // Переносим индекс
        uploadReq = other.uploadReq; // Необходимость загрузки 
        color = other.color;

        Node::operator=(other);
    }
    return *this;
}

Light::~Light()
{

}

Осталось обновить метод загрузки массива в буфер, выполнив заполнение элемента, когда это требуется. Файл src/Lights.cpp:

// Загрузка данных в буфер
void Light::upload(UBO& lights_data)
{
    GLuint LightDataSize = sizeof(LightData); // Одного экземпляра структуры LightData

    if (count)
    {
        for (int i = 0; i < MAX_LIGHTS; i++)
        {
            lights[i].recalcMatrices(); // Пересчитаем матрицы по необходимости (проверка внутри метода)

            // Если требуется загрузка
            if (lights[i].uploadReq)
            {
                lights[i].toData(); // Перевод ноды в данные для шейдера

                // Определение диапазона загрузки 
                if (first > lights[i].index)
                    first = lights[i].index;
                if (last < lights[i].index)
                    last = lights[i].index; 

                lights[i].uploadReq = false; // Сброс флага
            }
        }

        // Загрузка данных об источниках
        lights_data.loadSub(data, sizeof(LightData)*count); 
    }

    // Загружаем кол-во источников
    lights_data.loadSub(&count, sizeof(count), LightDataSize*MAX_LIGHTS);
}

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

// Загрузка данных в буфер
void Light::upload(UBO& lights_data)
{
    GLuint LightDataSize = sizeof(LightData); // Одного экземпляра структуры LightData
    int first = MAX_LIGHTS, last = -1; // Начало и конец диапазона загрузки источников
    
    if (count)
    {
        for (int i = 0; i < MAX_LIGHTS; i++)
        {
            lights[i].recalcMatrices(); // Пересчитаем матрицы по необходимости (проверка внутри метода)

            // Если требуется загрузка
            if (lights[i].uploadReq)
            {
                lights[i].toData(); // Перевод ноды в данные для шейдера

                // Определение диапазона загрузки 
                if (first > lights[i].index)
                    first = lights[i].index;
                if (last < lights[i].index)
                    last = lights[i].index; 

                lights[i].uploadReq = false; // Сброс флага
            }
        }

        // Загрузка данных об источниках
        lights_data.loadSub(data, sizeof(LightData)*count); 
    }

    // Загружаем кол-во источников
    lights_data.loadSub(&count, sizeof(count), LightDataSize*MAX_LIGHTS);
}

А так же можно проверить на изменение счетчик количества и отправлять только в том случае, если количество изменилось:

// Загрузка данных в буфер
void Light::upload(UBO& lights_data)
{
    GLuint LightDataSize = sizeof(LightData); // Одного экземпляра структуры LightData
    int first = MAX_LIGHTS, last = -1; // Начало и конец диапазона загрузки источников
    static GLuint prev_count = -1; // Кол-во источников в прошлую посылку

    if (count)
    {
        for (int i = 0; i < MAX_LIGHTS; i++)
        {
            lights[i].recalcMatrices(); // Пересчитаем матрицы по необходимости (проверка внутри метода)

            // Если требуется загрузка
            if (lights[i].uploadReq)
            {
                lights[i].toData(); // Перевод ноды в данные для шейдера

                // Определение диапазона загрузки 
                if (first > lights[i].index)
                    first = lights[i].index;
                if (last < lights[i].index)
                    last = lights[i].index; 

                lights[i].uploadReq = false; // Сброс флага
            }
        }
        
        // Если есть что загрузить (определен диапазон)
        if (last > -1)
            lights_data.loadSub(data + first, LightDataSize*(last - first +1), LightDataSize*(first)); // Загрузка данных об источниках
    }

    // Если кол-во изменилось
    if (prev_count != count)
    {
        prev_count = count;

        // Загружаем кол-во источников
        lights_data.loadSub(&count, sizeof(count), LightDataSize*MAX_LIGHTS);
    }
}

В файле src/main.cpp работа с иточниками будет иметь следующий вид:

    // Источники света
    Light& first = Light::getNew();
    first.e_color() = {0.7f, 0.0f, 0.0f}; // цвет
    first.e_position() = {0.3f, 0.1f, 0.5f}; // Позиция
    Light& second = Light::getNew();
    second.e_color() = {0.0f, 0.0f, 0.7f}; // цвет
    second.e_position() = {-0.3f, -0.1f, 0.5f}; // Позиция

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

Рисование отладочных лампочек

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

В файле include/Lights.h заменим добавим вспомогательный статический метод для отрисовки отладочных лампочек:


// Источник света
class Light : public Node
{
    public:
...
        static void render(ShaderProgram &shaderProgram, UBO &material_buffer); // Рисование отладочных лампочек
..
};

Данный метод будет рисовать модель на основании массива источников света для uniform-буфера. Его реализация в файле src/Lights.cpp:

// Рисование отладочных лампочек
void Light::render(ShaderProgram &shaderProgram, UBO &material_buffer)
{
    // Загрузка модели лампочки при первом вызове функции
    static Scene bulb = loadOBJtoScene("../resources/models/bulb.obj", "../resources/models/", "../resources/textures/");

    // Цикл по источникам света
    for (int i = 0; i < count; i++)
    {
        // Сдвиг на позицию источника
        bulb.root.e_position() = data[i].position;
        // Задание цвета
        bulb.models.begin()->material.ka = data[i].color;

        // Вызов отрисовки
        bulb.render(shaderProgram, material_buffer);
    }
}

При первом вызове данного метода производится загрузка модели лампочки bulb.obj и её библиотеки материалов bulb.mtl, которые доступны в репозитории resources.

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

Для отрисовки такой лампочки необходим упрощенные шейдеры, которые обрабатывают минимум информации.

Содержание вершинного шейдера shader/bulb.vert:

#version 420 core 

layout(location = 0) in vec3 pos; 

layout(std140, binding = 0) uniform Camera
{
    mat4 projection;
    mat4 view;
    vec3 position;
} camera;

uniform mat4 model;

void main() 
{ 
    gl_Position = camera.projection * camera.view * model * vec4(pos, 1.0);
} 

Примечание: можно использовать вершинный шейдер shader/quad.vert, но он вычисляет текстурные координаты, которые не потребуются для дальнейших вычислений.

Содержание фрагментного шейдера shader/bulb.frag:

#version 420 core 

layout(std140, binding = 1) uniform Material
{
    vec3 ka;
    vec3 kd;
    vec3 ks;
    float p;
};

out vec4 color;

void main()
{   
    color = vec4(ka, 1);
}

Цвет рассеивателя лампочки задается материалом, который устанавливается перед отправкой на рисование.

В файле src/main.cpp загрузим шейдеры перед циклом рисования:

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

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

...
        // Перенос буфера глубины
        FBO::useDefault(GL_DRAW_FRAMEBUFFER); // Базовый в режиме записи
        gbuffer.use(GL_READ_FRAMEBUFFER); // Буфер геометрии в режиме чтения
        // Копирование значений глубины 
        glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
        FBO::useDefault(); // Использование базового буфера для дальнейших работ

        // Представление содержимого буфера цепочки показа на окно
        glfwSwapBuffers(window);
        // Обработка системных событий
        glfwPollEvents();

Теперь можно включить новый шейдер и вызвать метод рисования:

        FBO::useDefault(); // Использование базового буфера для дальнейших работ

        // Отрисовка отладочных лампочек со специальным шейдером
        bulbShader.use();
        Light::render(bulbShader, material_data);

        // Представление содержимого буфера цепочки показа на окно
        glfwSwapBuffers(window);
        // Обработка системных событий
        glfwPollEvents();

Пример рисования отладочных лампочек изображен на рисунке 3. Капля расположена на координатах (0; 0; 1), красный источник (0.3; 0.1; 0.5), синий (-0.3; -0.1; 0.5)

Рисунок 3 — Два источника света рядом с каплей

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

Угасание источника

На данный момент источники света обладают абсолютной мощностью и светят на всей сцене с одинаковой интенсивностью, что является некорректным.

Сила света, образуемая из коэффициента интенсивности света, деленного на коэффициент затухания (attenuation):
\overrightarrow{F}_{light} = \overrightarrow{i} / A = (\overrightarrow{i}_{diffuse} + \overrightarrow{i}_{specular}) / (K_{const} + K_{linear} * d + K_{quadratic} * d^2),
где \overrightarrow{i} — интенсивность свечения поверхности, d — дистанция от источника до поверхности, K_{const} — постоянный член угасания (обычно =1.0 для того, чтоб знаменатель не становился меньше 1), K_{linear} — линейный член угасания, K_{quadratic} — квадратичный член угасания.

Важное замечание: \overrightarrow{i}_{ambient} не участвует в учете расстояния до источника, так как является составляющей, определяемой материалом модели.

Разработчики движка Ogre3D предлагают таблицу значений, представленных в таблице 1, но такой подход не является простым для пользователя, размещающего источник света.

Радиус источника (r)KlinearKquadratic
70.71.8
130.350.44
200.220.20
320.140.07
500.090.032
650.070.017
1000.0450.0075
1600.0270.0028
2000.0220.0019
3250.0140.0007
6000.0070.0002
32500.00140.000007
Таблица 1 — Значения коэф. угасания в зависимости от радиуса освещенности источника

На основании таблицы 1 можно вывести приблизительные формулы для вычисления значений
K_{linear} = 4.5 / r ,
K_{quadratic} = (2 * K_{linear})^2 = (9 / r)^2,
где r — радиус действия источника света.

В таблице 2 записаны значения, вычисленные по предложенным выше формулам.

Радиус источника (r)KlinearKquadratic
70.642861.65306
130.346150.47929
200.2250.2025
320.140630.07910
500.090.0324
650.069230.01917
1000.0450.0081
1600.028130.00316
2000.02250.00203
3250.013850.00077
6000.00750.000225
32500.001390.0000077
Таблица 2 — Значения коэф. угасания в зависимости от радиуса освещенности источника, рассчитанные по формуле

Среднеквадратичное отклонение для данных формул составляет 0.017 для K_{linear} и 0.044 для K_{quadratic}.

Добавим приватное поле Light::radius и публичные методы доступа к классу в файле include/Lights.h:

// Источник света
class Light : public Node
{
    public:
...
        const glm::vec3& c_color() const; // Константный доступ к цвету
        glm::vec3& e_color(); // Неконстантная ссылка для изменений цвета

        const float& c_radius() const; // Константный доступ к радиусу
        float& e_radius(); // Неконстантная ссылка для изменений радиуса
...
    private:
...
        glm::vec3 color; // Цвет
        float radius; // Радиус действия источника
...
};

А так же добавим поле в структуру LightData:

// Точечный источник света
struct LightData 
{
    alignas(16) glm::vec3 position; // Позиция
    alignas(16) glm::vec3 color; // Цвет 
    float radius; // Радиус действия источника   
};

Реализация методов, а так же инициализация в конструкторе и операторе присваивания (по умолчанию радиус будет 10.0f) в файле src/Lights.cpp:

// Конструктор без параметров
Light::Light() : Node(), index(-1), uploadReq(false), color(1.0f), radius(10.0f)
{
    
}


// Оператор  присваивания
Light& Light::operator=(const Light& other)
{
    // Проверка на самоприсваивание
    if (this != &other) 
    {
        index = other.index; // Переносим индекс
        uploadReq = other.uploadReq; // Необходимость загрузки 
        color = other.color;
        radius = other.radius;

        Node::operator=(other);
    }
    return *this;
}

// Константный доступ к радиусу
const float& Light::c_radius() const
{
    return radius;
}

// Неконстантная ссылка для изменений радиуса
float& Light::e_radius()
{
    uploadReq = true;

    return radius;
}

В методе Light::toData необходимо добавить запись радиуса:

// Преобразует информацию об источнике в структуру LightData
void Light::toData()
{
    check_id(); // Проверка на работу с корректным индексом

    data[index].position = glm::vec3(result_transform[3]); // Позиция из матрицы трансформации
    data[index].color = color; // Цвет
    data[index].radius = radius; // Радиус действия источника
}

В файле фрагментного шейдера, рассчитывающего освещенность, shaders/lighting.frag добавим радиус к uniform-буферу:

struct LightData
{
    vec3 position;
    vec3 color;
    float radius;
};

Перед циклом по источникам добавим две переменные:

    float L_distance; // Расстояние от поверхности до источника
    float attenuation; // Коэф. угасания

На основании вектора L_vertex получим расстояние между поверхностью фрагмента и источником:

        // Данные об источнике относительно фрагмента
        L_vertex = light_f.data[i].position - fragPos;

        // Расстояние от поверхности до источника
        L_distance = length(L_vertex);

Дальнейшие вычисления стоит производить, если дистанция меньше радиуса действия источника:

        // Проверка на дистанцию
        if (L_distance < light_f.data[i].radius)
        {
            // Нормирование вектора
            L_vertex = normalize(L_vertex);

            // Данные о камере относительно фрагмента
            Cam_vertex = normalize(camera.position - fragPos);

            // Диффузная составляющая
            diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
            
            // Вектор половины пути
            H = normalize(L_vertex + Cam_vertex);
            // Зеркальная составляющая
            specular = pow(max(dot(H, N), 0.0), p*4); // скалярное произведение с отсеканием значений < 0 в степени p
...

Перед рассчетом освещенности вычислим коэффициент угасания и применим его к диффузной и зеркальной составляющим:

            // Угасание с учетом расстояния
            float kl = 4.5/light_f.data[i].radius;
            attenuation = 1 / (1 + kl * L_distance + 4 * kl * kl * L_distance * L_distance);

            color += vec4(light_f.data[i].color*kd*diffuse  * attenuation, 1) 
                  +  vec4(light_f.data[i].color*ks*specular * attenuation, 1);
        }

Для сравнения результатов расположим плоскость под источником, как в заметке по текстурам с добавлением нормалей (без которых не будет расчет освещения). На рисунке 4 представлены два окна, на одном из которых включено угасание.

Рисунок 4 — Работа программы без расчета затухания и с ним

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

Важное замечание: можно оптимизировать расчеты, вычислив значения K_{linear} и K_{quadratic} перед отправкой данных об источнике в uniform-буфер, так как они зависят от радиуса, который не изменяется в процессе вычислений.

Для исправления замечания изменим структуру LightData в файле include/Lights.h:

// Точечный источник света
struct LightData 
{
    alignas(16) glm::vec3 position; // Позиция
    alignas(16) glm::vec3 color; // Цвет 
    alignas(16) glm::vec3 attenuation; // Радиус действия источника, линейный и квадратичный коэф. угасания   
};

Вычисления коэффициентов перед отправкой следует проводить, если радиус изменился (файл src/Lights.cpp):

// Преобразует информацию об источнике в структуру LightData
void Light::toData()
{
    check_id(); // Проверка на работу с корректным индексом

    data[index].position = glm::vec3(result_transform[3]); // Позиция из матрицы трансформации
    data[index].color = color; // Цвет
    // Если радиус изменился
    if (data[index].attenuation.r != radius)
    {
        data[index].attenuation.r  = radius; // Радиус действия источника
        data[index].attenuation[1] = 4.5/radius;      // Линейный коэф. угасания
        data[index].attenuation[2] = 4 * data[index].attenuation[1] * data[index].attenuation[1]; // Квадратичный коэф. угасания
    }
}

Тогда в файле фрагментного шейдера shaders/lighting.frag будут следующие изменения:

struct LightData
{
    vec3 position;
    vec3 color;
    float radius;
    vec3 attenuation;
};

...
void main() 
{ 
...
    for (i = 0; i < light_f.count; i++)
    {
...
        if (L_distance < light_f.data[i].attenuation.r)
...
            // Угасание с учетом расстояния
            float kl = 4.5/light_f.data[i].radius;
            attenuation = 1 / (1 + light_f.data[i].attenuation[1] * L_distance + light_f.data[i].attenuation[2] * L_distance * L_distance);

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

Рисунок 5 — Результат работы программы с учетом угасания точечных источников.

Для удобства работы с источником света можно добавить рисование сферы, за пределами которой источник теряет свою силу. Для этого добавим функцию genSphere в файл src/Model.cpp:

// Генерирует сферу заданного радиуса с определенным количеством сегментов
void genShpere(Model& model, float radius, int sectorsCount, Node* parent)
{
    std::vector<glm::vec3> vertices;
    std::vector<glm::vec3> normals;
    std::vector<GLuint> indices;

    float x, y, z, xy; // Позиция вершины
    float nx, ny, nz, lengthInv = 1.0f / radius; // Нормаль вершины
    float PI = 3.14159265;
    float sectorStep = PI / sectorsCount; // Шаг сектора
    float longAngle, latAngle; // Углы

    for(int i = 0; i <= sectorsCount; ++i)
    {
        latAngle = PI / 2 - i * sectorStep; // Начиная с pi/2 до -pi/2
        xy = radius * cos(latAngle); // r * cos(lat)
        z = radius * sin(latAngle); // r * sin(lat)

        // добавляем (sectorCount+1) вершин на сегмент
        // Последняя и первая вершины имеют одинаковые нормали и координаты
        for(int j = 0; j <= sectorsCount; ++j)
        {
            longAngle = j * 2 * sectorStep; // Начиная с 0 до 2*pi

            // Положение вершины (x, y, z)
            x = xy * cos(longAngle); // r * cos(lat) * cos(long)
            y = xy * sin(longAngle); // r * cos(lat) * sin(long)
            vertices.push_back({x, y, z});

            // Нормали (nx, ny, nz)
            nx = x * lengthInv;
            ny = y * lengthInv;
            nz = z * lengthInv;
            normals.push_back({nx, ny, nz});
        }
    }
    int k1, k2;
    for(int i = 0; i < sectorsCount; ++i)
    {
        k1 = i * (sectorsCount + 1); // начало текущего сегмента
        k2 = k1 + sectorsCount + 1; // начало следующего сегмента

        for(int j = 0; j < sectorsCount; ++j, ++k1, ++k2)
        {
            // 2 треугольника на один сегмент
            // k1, k2, k1+1
            if(i != 0)
            {
                indices.push_back(k1);
                indices.push_back(k2);
                indices.push_back(k1 + 1);
            }

            // k1+1, k2, k2+1
            if(i != (sectorsCount-1))
            {
                indices.push_back(k1 + 1);
                indices.push_back(k2);
                indices.push_back(k2 + 1);
            }

        }
    }
    // Загрузка в модель
    model.load_verteces(&vertices[0], vertices.size());
    model.load_normals(&normals[0], normals.size());
    model.load_indices(&indices[0], indices.size());
}

В файле include/Model.h объявим новую функцию для возможности использования в других файлах:

class Model genShpere(float radius, int sectorsCount, class Node* parent = NULL); // Генерирует сферу заданного радиуса с определенным количеством сегментов

Тогда метод Light::render в файле src/Model.cpp будет иметь следующий вид:

// Рисование отладочных лампочек
void Light::render(ShaderProgram &shaderProgram, UBO &material_buffer)
{
    // Загрузка модели лампочки при первом вызове функции
    static Scene bulb = loadOBJtoScene("../resources/models/bulb.obj", "../resources/models/", "../resources/textures/");
    static Model sphere = genShpere(1, 16, &bulb.root);

    // Цикл по источникам света
    for (int i = 0; i < count; i++)
    {
        // Сдвиг на позицию источника
        bulb.root.e_position() = data[i].position;
        sphere.e_scale() = glm::vec3(data[i].attenuation.r); // Масштабирование сферы
        // Задание цвета
        bulb.models.begin()->material.ka = sphere.material.ka = data[i].color;

        // Вызов отрисовки
        bulb.render(shaderProgram, material_buffer);    

        // Рисование сферы покрытия источника в режиме линий
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        sphere.render(shaderProgram, material_buffer);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
}

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

Результат после изменения материала капли изображен на рисунке 6.

Рисунок 6 — Красный и синий источники освещения с серой каплей

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

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

Проблема больших координат

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

Для примера сдвинем каплю, камеру и источники на 500 единиц по оси X. Пример изображен на рисунке 7.

Рисунок 7 — Проблема больших координат в текстуре gPosition

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

    // Создадим текстуры для буфера кадра
    Texture gPosition(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT0, 0, GL_RGB32F, GL_RGB); // Позиция вершины
    Texture gNormal(WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_ATTACHMENT1, 1, GL_RGB16F, GL_RGB); // Нормали

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

Проблема изменения размеров окна

При изменении размеров окна возникает артефакт, который обусловлен старым размером текстур. Так же данная проблема проявляется с связи с использованием констант WINDOW_WIDTH и WINDOW_HEIGHT, которые определяют область копируемого буфера глубины. Проблема изображена на рисунке 8.

Рисунок 8 — Проблема при изменении размеров окна

Дополним класс текстуры методом, который изменит её размер в файле include/Texture.h:

class Texture
{
    public:
...
        void reallocate(GLuint width, GLuint height, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Пересоздает текстуру для имеющегося дескриптора
...
};

Реализация данного метода в файле src/Texture.cpp:

// Пересоздает текстуру для имеющегося дескриптора
void Texture::reallocate(GLuint width, GLuint height, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
    use();
    glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, dataType, NULL);
}

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

// Объект буфера рендера
class RBO
{
    public:
...
        void reallocate(int w, int h, GLuint component = GL_DEPTH_COMPONENT); // Изменяет размеры буфера рендера
...
};

Реализация данного метода в файле src/Buffers.cpp:

// Изменяет размеры буфера рендера
void RBO::reallocate(int w, int h, GLuint component)
{
    glBindRenderbuffer(GL_RENDERBUFFER, handler); // Привязка элементного буфера
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h);
}

В файле src/main.cpp добавим глобальные указатели на текстуры и объект буфера рендера, а так же заменим строковые константы с размерами окна на переменные:

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define WINDOW_CAPTION "OPENGL notes on rekovalev.site"

// Указатели на текстуры для изменения размеров окна
Texture* pgPosition = NULL;
Texture* pgNormal = NULL;
Texture* pgDiffuseP = NULL;
Texture* pgAmbientSpecular = NULL;
RBO*     pgrbo = NULL;
// Размеры окна
int WINDOW_WIDTH = 800;
int WINDOW_HEIGHT = 600;

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

    // Активируем базовый буфер кадра
    FBO::useDefault();

    // Сохраним указатели на текстуры для изменения размеров окна
    pgPosition = &gPosition;
    pgNormal = &gNormal;
    pgDiffuseP = &gDiffuseP;
    pgAmbientSpecular = &gAmbientSpecular;
    pgrbo = &grbo;

Осталось дополнить функцию framebuffer_size_callback:

// Функция-callback для изменения размеров буфера кадра в случае изменения размеров поверхности окна
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
    
    // Изменение размеров текстур для G-буфера
    if (pgPosition)
        pgPosition->reallocate(width, height, 0, GL_RGB32F, GL_RGB);
    if (pgNormal)
        pgNormal->reallocate(width, height, 1, GL_RGB16F, GL_RGB); 
    if (pgDiffuseP)
        pgDiffuseP->reallocate(width, height, 2, GL_RGBA16F); 
    if (pgAmbientSpecular)
        pgAmbientSpecular->reallocate(width, height, 3); 
    // И буфера глубины
    if (pgrbo)
        pgrbo->reallocate(width, height);

    // Запомним новые размеры окна
    WINDOW_WIDTH = width;
    WINDOW_HEIGHT = height;

    // Изменим параметры перспективной матрицы проекции для камеры
    Camera::current().setPerspective(CAMERA_FOVy, (float)width/height);
}

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

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

Заключение

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

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

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

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

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