Введение
Рассмотренные в седьмой заметке модели затенения плохо работают с множеством источников освещения, так как расчеты в цикле по массиву источников занимают много ресурсов и вычислительного времени. Для решения данной проблемы придуман подход с использованием отложенного рендера.
Данный подход заключается в разделении рендера на два этапа:
- проход геометрии (geometry pass);
- проход освещения (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.
Множество источников освещения
Есть два способа обработки множества источников света:
- индивидуальный вызов прямоугольника для каждого источника с применением смешивания;
- загрузка массива в 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.
По рисунку 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.
Текущая версия доступна на теге 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)
Текущая версия доступна на теге 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) | Klinear | Kquadratic |
---|---|---|
7 | 0.7 | 1.8 |
13 | 0.35 | 0.44 |
20 | 0.22 | 0.20 |
32 | 0.14 | 0.07 |
50 | 0.09 | 0.032 |
65 | 0.07 | 0.017 |
100 | 0.045 | 0.0075 |
160 | 0.027 | 0.0028 |
200 | 0.022 | 0.0019 |
325 | 0.014 | 0.0007 |
600 | 0.007 | 0.0002 |
3250 | 0.0014 | 0.000007 |
На основании таблицы 1 можно вывести приблизительные формулы для вычисления значений
K_{linear} = 4.5 / r ,
K_{quadratic} = (2 * K_{linear})^2 = (9 / r)^2,
где r — радиус действия источника света.
В таблице 2 записаны значения, вычисленные по предложенным выше формулам.
Радиус источника (r) | Klinear | Kquadratic |
---|---|---|
7 | 0.64286 | 1.65306 |
13 | 0.34615 | 0.47929 |
20 | 0.225 | 0.2025 |
32 | 0.14063 | 0.07910 |
50 | 0.09 | 0.0324 |
65 | 0.06923 | 0.01917 |
100 | 0.045 | 0.0081 |
160 | 0.02813 | 0.00316 |
200 | 0.0225 | 0.00203 |
325 | 0.01385 | 0.00077 |
600 | 0.0075 | 0.000225 |
3250 | 0.00139 | 0.0000077 |
Среднеквадратичное отклонение для данных формул составляет 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 можно сделать вывод, что ранее программа работала как будто источник лежал на поверхности, так как учитывался только угол падения лучей.
Важное замечание: можно оптимизировать расчеты, вычислив значения 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.
Для удобства работы с источником света можно добавить рисование сферы, за пределами которой источник теряет свою силу. Для этого добавим функцию 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.
Важное замечание: сейчас можно заметить, что затухание съедает интенсивность источника слишком быстро (источник не достигает радиуса сферы), данное поведение можно объяснить отсутствием гамма-коррекции, которая будет рассмотрена в следующих заметках.
Текущая версия доступна на теге v0.5 в репозитории 09.
Проблема больших координат
При расположении сцены на больших координатах в мировом пространстве могут возникать артефакты, вызываемые потерей точности двухбайтного вещественного, используемого как компонента цвета для текстуры gPosition.
Для примера сдвинем каплю, камеру и источники на 500 единиц по оси X. Пример изображен на рисунке 7.
Для решения данной проблемы достаточно изменить размер компоненты пикселя на четырехбайтное вещественное:
// Создадим текстуры для буфера кадра
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.
Дополним класс текстуры методом, который изменит её размер в файле 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