Введение
Теорию для данной заметки можно изучить в заметке посвященной освещению в трехмерных приложениях.
Данная заметка продолжает тему, поднятую в заметке по работе с тенями в OpenGL (направленный источник), и охватывает тени от точечных источников и прожекторов.
Задача расчета теней от точечных источников является весьма трудозатратной. Ранее в заметках использовались 300 точечных источников/прожекторов, но содержание такого количества источников на сцене без оптимизации алгоритма расчета теней является неподъемным. Оптимизации расчета теней будут рассмотрены в следующих заметках, так что для упрощения задачи сократим их количество до 64. Для этого изменим константу MAX_LIGHTS в файле include/Lights.h:
#define MAX_LIGHTS 64
А так же требуется изменить размер массива uniform-блока в файле shaders/lighting.frag:
layout(std140, binding = 2) uniform Light
{
LightData data[64];
int count;
} light_f;
Примечание: в заметке каждый источник будет отбрасывать тень, но можно использовать прежнее число источников добавив флаг наличия у источника тени и организовав хранение.
Для хранения теневых карт будут использоваться текстурный атлас — большая текстура, которая должна содержать в себе все расчеты глубин. Один атлас состоит из 3D текстуры с параметром GL_TEXTURE_CUBE_MAP_ARRAY, где глубина равна произведению числа сторон кубической текстуры (6) на количество источников освещения (64). Детализацию текстуры (фрагмент атласа), которая отвечает за конкретный источник, по сравнению с тенями от солнца, можно снизить ради экономии памяти.
Содержание заметки:
Создание класса кубической текстуры
Тень от точечного источника обычно хранится в кубической текстуре, пример такой текстуры с индексами представлен на рисунке 1.
Так как источников больше одного, то необходимо использовать несколько кубических текстур, но такой подход усложняет работу. OpenGL, начиная с версии 4.0, позволяет создавать массив кубических текстур (3D текстура разделенная на кубические карты). Для дальнейшей работы поменяем версию спецификации OpenGL в файле src/main.cpp:
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); // Мажорная версия спецификаций OpenGL
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); // Минорная версия спецификаций OpenGL
Примечание: данная функциональность доступна на спецификациях 3 мажорной версии для некоторых GPU Nvidia и AMD, но для корректной работы лучше использовать подходящую версию.
В файле include/Texture.h добавим класс TextureCubeArray, дочерний от BaseTexture:
// Класс 3D кубической текстуры
class TextureCubeArray : public BaseTexture
{
public:
TextureCubeArray(GLuint levels, GLuint width, GLuint height, GLuint attachment, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Конструктор текстуры заданного размера для использования в буфере
TextureCubeArray(const TextureCubeArray& other); // Конструктор копирования
TextureCubeArray& operator=(const TextureCubeArray& other); // Оператор присваивания
void reallocate(GLuint levels, GLuint width, GLuint height, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Пересоздает текстуру для имеющегося дескриптора
virtual void use(); // Привязка текстуры
};
Реализация конструктора и методов данного класса (жирным выделена разница с реализацией класса TextureArray):
// Конструктор текстуры заданного размера для использования в буфере
TextureCubeArray::TextureCubeArray(GLuint levels, GLuint width, GLuint height, GLuint attachment, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
type = texType;
// Генерация текстуры заданного размера
glGenTextures(1, &handler);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, handler);
glTexImage3D(
GL_TEXTURE_CUBE_MAP_ARRAY, 0, internalformat, width, height, 6*levels, 0, format, dataType, 0);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Привязка к буферу кадра
glFramebufferTexture(GL_FRAMEBUFFER, attachment, handler, 0);
// Создаем счетчик использований дескриптора
handler_count[handler] = 1;
}
// Конструктор копирования
TextureCubeArray::TextureCubeArray(const TextureCubeArray& other)
{
handler = other.handler;
type = other.type;
// Делаем копию и увеличиваем счетчик
handler_count[handler]++;
}
// Оператор присваивания
TextureCubeArray& TextureCubeArray::operator=(const TextureCubeArray& other)
{
// Если это разные текстуры
if (handler != other.handler)
{
this->~TextureCubeArray(); // Уничтожаем имеющуюся
// Заменяем новой
handler = other.handler;
handler_count[handler]++;
}
type = other.type;
return *this;
}
// Пересоздает текстуру для имеющегося дескриптора
void TextureCubeArray::reallocate(GLuint levels, GLuint width, GLuint height, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
use();
glTexImage3D(
GL_TEXTURE_CUBE_MAP_ARRAY, 0, internalformat, width, height, 6*levels, 0, format, dataType, 0);
}
// Привязка текстуры
void TextureCubeArray::use()
{
glActiveTexture(type + GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, handler); // Привязка текстуры как активной
}
Текущая версия доступна на теге v0.1 в репозитории 13.
Расчет теней для источников
В файле src/main.cpp создадим буфер для рендера теней от источников:
// Размер одной стороны кубической карты
const GLuint pointShadow_resolution = 500;
// Создадим буфер кадра для рендера теней от источников света
FBO pointShadowBuffer;
// Создадим текстуры для буфера кадра
TextureCubeArray pointShadowDepth(MAX_LIGHTS, pointShadow_resolution, pointShadow_resolution, GL_DEPTH_ATTACHMENT, 5, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT);
// Отключим работу с цветом
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
// Активируем базовый буфер кадра
FBO::useDefault();
Для вычисления буфера глубины источника требуется рассчитать шесть произведений матриц вида для каждого направления источника на матрицу проекции. В качестве матрицы проекции используется перспективная с углом обзора в 90°, соотношением сторон 1 к 1 (aspect), ближним отсечением со значением 0.1 и дальним, ограниченным радиусом действия источника.
Для точечных источников и их частного случая — прожекторов, используется перспективная матрица проекции. Перспективная матрица при расчете теней дает искажение геометрии с удалением от камеры, что является аналогичным при приближении объекта к реальному источнику освещения.
Дополним класс Light методом recalcVP и структуру LightData полем-массивом из шести матриц vp в файле include/Lights.h:
// Точечный источник света
struct LightData
{
alignas(16) glm::vec3 position; // Позиция
alignas(16) glm::vec3 color; // Цвет
alignas(16) glm::vec3 attenuation; // Радиус действия источника, линейный и квадратичный коэф. угасания
alignas(16) glm::vec4 direction_angle; // Направление и половинный угол освещенности
alignas(16) glm::mat4 vp[6]; // Матрицы проекции и трансформации в пространство источника
};
// Источник света
class Light : public Node
{
...
private:
...
virtual void recalcMatrices(); // Метод пересчета матрицы трансформации по необходимости, должен сбрасывать флаг changed
void recalcVP(); // Пересчитывает по необходимости матрицу вида-проекции
...
};
Реализация метода Light::recalcVP в файле src/Lights.cpp, который вызывается в методе Light::toData:
// Преобразует информацию об источнике в структуру LightData
void Light::toData()
{
check_id(); // Проверка на работу с корректным индексом
// Если позиция изменилась
if (data[index].position.x != result_transform[3].x
|| data[index].position.y != result_transform[3].y
|| data[index].position.z != result_transform[3].z
)
{
data[index].position = glm::vec3(result_transform[3]); // Позиция из матрицы трансформации
recalcVP(); // Пересчет матрицы вида-проекции для расчета теней
}
data[index].color = color; // Цвет
...
}
// Пересчитывает по необходимости матрицу вида-проекции
void Light::recalcVP()
{
float near_plane = 0.1f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), 1.0f, near_plane, radius);
data[index].vp[0] = shadowProj * glm::lookAt(position, position + glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f));
data[index].vp[1] = shadowProj * glm::lookAt(position, position + glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f));
data[index].vp[2] = shadowProj * glm::lookAt(position, position + glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
data[index].vp[3] = shadowProj * glm::lookAt(position, position + glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f));
data[index].vp[4] = shadowProj * glm::lookAt(position, position + glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f));
data[index].vp[5] = shadowProj * glm::lookAt(position, position + glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f));
}
Важное замечание: компилятор может выдавать ошибку, если не подключен заголовочный файл <GLM/ext/matrix_transform.hpp>.
Благодаря такому подходу матрицы вида-проекции конкретного источника рассчитываются только в случае изменения его позиции, что позволяет избежать ненужных пересчетов и отправок массивов на видеокарту.
Необходимо добавить к структуре uniform-блока информацию о матрицах проекции и вида источника (6 штук) в файле shaders/lighting.frag:
struct LightData
{
vec3 position;
vec3 color;
float angle;
vec3 direction;
float radius;
vec2 K;
mat4 vp[6];
};
Далее требуется добавить геометрический и фрагментный шейдеры для расчета теней от источников. В качестве вершинного шейдера можно использовать шейдер от расчета теней направленного источника (shaders/sun_shadow.vert).
Геометрический шейдер выполняет 6 вызовов (invocations) для каждой стороны кубической карты. В каждом вызове проходит вычисления для выбранного индекса источника света (light_i), который записан в uniform-переменную. Для каждой вершины запоминаются значения для фрагментного шейдера: позиции вершины в мировых координатах (FragPos), позиции источника в мировых координатах (lightPos) и радиуса его действия (radius); а так же вычисляется позиция вершины в пространстве источника (gl_Position) и слоя текстуры (gl_Layer). Содержимое файла shaders/point_shadow.geom:
#version 420 core
layout (triangles, invocations = 6) in; // здесь invocations соответствует числу сторон кубической карты теней
layout (triangle_strip, max_vertices=18) out; // здесь max_vertices = 3 вершины * 6 вызовов на стороны куба
struct LightData
{
vec3 position;
vec3 color;
vec3 attenuation;
vec4 direction_angle;
mat4 vp[6];
};
layout(std140, binding = 2) uniform Light
{
LightData data[64];
int count;
} light_g;
uniform int light_i;
out vec4 FragPos;
out vec3 lightPos;
out float radius;
void main()
{
for(int i = 0; i < 3; ++i)
{
FragPos = gl_in[i].gl_Position;
lightPos = light_g.data[light_i].position;
radius = light_g.data[light_i].attenuation.r;
gl_Position = light_g.data[light_i].vp[gl_InvocationID] * gl_in[i].gl_Position;
gl_Layer = gl_InvocationID + light_i*6;
EmitVertex();
}
EndPrimitive();
}
Фрагментный шейдер вычисляет расстояние между источником и фрагментом (в диапазоне [0;1]) и записывает его в качестве значения буфера глубины. Содержимое файла shaders/point_shadow.frag:
#version 330 core
in vec4 FragPos;
in vec3 lightPos;
in float radius;
void main()
{
// Расстояние между источником и фрагментом
float lightDistance = length(FragPos.xyz - lightPos);
// Приведение к диапазону [0;1]
lightDistance = lightDistance / radius;
// Замена значения глубины
gl_FragDepth = lightDistance;
}
Теперь загрузим шейдеры в файле src/main.cpp:
// Шейдер для расчета теней
ShaderProgram pointShadowShader;
// Загрузим шейдер
pointShadowShader.load(GL_VERTEX_SHADER, "shaders/sun_shadow.vert");
pointShadowShader.load(GL_GEOMETRY_SHADER, "shaders/point_shadow.geom");
pointShadowShader.load(GL_FRAGMENT_SHADER, "shaders/point_shadow.frag");
pointShadowShader.link();
Далее требуется подключить атлас теней к шейдеру расчета освещения:
const char* gtextures_shader_names[] = {"gPosition", "gNormal", "gDiffuseP", "gAmbientSpecular", "sunShadowDepth", "pointShadowDepth"};
lightShader.bindTextures(gtextures_shader_names, sizeof(gtextures_shader_names)/sizeof(const char*));
// Загрузка данных о границах каскадов
glUniform1fv(lightShader.getUniformLoc("camera_cascade_distances"), CAMERA_CASCADE_COUNT, &camera_cascade_distances[1]);
А так же использовать текстуру при вычислении освещения в цикле:
...
gAmbientSpecular.use();
// Подключаем текстуры теней
sunShadowDepth.use();
pointShadowDepth.use();
// Загружаем информацию о направленном источнике
sun.upload(lightShader);
...
Так как поле количества источников является приватным (Light::count), требуется добавить метод доступа к значению Light::getCount в файле include/Lights.h:
// Источник света
class Light : public Node
{
public:
...
static int getCount(); // Возвращает количество источников
...
};
Реализация данного метода в файле src/Lights.cpp:
// Возвращает количество источников
int Light::getCount()
{
return count;
}
Теперь можно выполнить рендер теневых карт в атлас после рендера теней от направленного источника, задав значения индекса источника и произведя рендер сцены для каждого источника (файл src/main.cpp):
// Изменим размер вывода для стороны кубической карты точечного источника
glViewport(0, 0, pointShadow_resolution, pointShadow_resolution);
// Активируем буфер кадра для теней от солнца
pointShadowBuffer.use();
// Подключим шейдер для расчета теней
pointShadowShader.use();
// Очистка буфера глубины
glClear(GL_DEPTH_BUFFER_BIT);
// Для каждого источника вызывается рендер сцены
for (int i = 0; i < Light::getCount(); i++)
{
glUniform1i(pointShadowShader.getUniformLoc("light_i"), i);
// Рендерим геометрию в буфер глубины
scene.render(pointShadowShader, material_data);
rectangle.render(pointShadowShader, material_data);
}
Перед вычислением затененности в фрагментном шейдере shaders/lighting.frag необходимо добавить uniform-переменную с семплером pointShadowDepth:
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gDiffuseP;
uniform sampler2D gAmbientSpecular;
uniform sampler2DArray sunShadowDepth;
uniform samplerCubeArray pointShadowDepth;
А так же три переменные:
int x, y, z; // Счетчик для PCF
float pcfDepth; // Глубина PCF
int cascade_index; // Индекс текущего каскада для вычисления теней
float cubemap_offset = 0.05f; // Отступ в текстурных координатах для PCF
float cubemap_depth; // Дистанция между фрагментом и источником в диапазоне [0;1]
В цикле по индексам источников света (i) для каждого источника значение тени обнуляется, после чего вычисляется позиция фрагмента относительно источника (вычитание векторов), дистанция между фрагментом и источником (длина вектора, деленная на радиус источника). К дистанции применяется сдвиг для решения проблемы теневого акне. Далее три вложенных цикла по осям выполняют фильтрацию на процент ближе (PCF). Если значение текстуры меньше, чем дистанция между фрагментом и источником, то следует инкрементировать значение тени. После цикла значение тени делится на 27 (3 образца по каждой оси для PCF). Если значение меньше единицы, то следует приступать к вычислению освещенности. Итоговую сумму цветов диффузного и зеркального отражений следует умножить на (1.0 — shadowValue).
Для получения значения из массива кубических текстур (3D кубическая текстура) в функцию texture передается вектор от 4 компонентов, где первые три компоненты определяют координату кубической текстуры, а четвертый задает индекс в атласе (индекс источника).
Содержимое файла shaders/lighting.frag:
// Цикл по источникам света
int i;
for (i = 0; i < light_f.count; i++)
{
// Обнулим значение тени
shadowValue = 0;
// Позиция фрагмента относительно источника
fragPosLightSpace = fragPos - light_f.data[i].position;
// Дистанция между фрагментом и источником в диапазоне [0;1]
cubemap_depth = length(fragPosLightSpace) / light_f.data[i].attenuation.r;
// Сдвиг для решения проблемы акне
cubemap_depth -= max(0.05 * (1.0 - dot(N, light_f.data[i].direction_angle.xyz)), 0.005);
for(x = -1; x <= 1; ++x)
{
for(y = -1; y <= 1; ++y)
{
for(z = -1; z <= 1; ++z)
{
// Значение из кубической текстуры с учетом источника (i)
pcfDepth = texture(pointShadowDepth, vec4(fragPosLightSpace + vec3(x, y, z)*cubemap_offset, i)).r;
if(cubemap_depth > pcfDepth)
shadowValue += 1.0;
}
}
}
shadowValue /= (27);
if (shadowValue < 1.0)
{
// Данные об источнике относительно фрагмента
L_vertex = light_f.data[i].position - fragPos;
// Расстояние от поверхности до источника
L_distance = length(L_vertex);
...
color += ( vec4(light_f.data[i].color*kd*diffuse * attenuation, 1)
+ vec4(light_f.data[i].color*ks*specular * attenuation, 1) ) * (1.0 - shadowValue);
}
}
Важное замечание: для прожекторов тени рассчитываются как для точечных источников, но результат выводится с отсеканием по окрашиванию незатененных участков. Можно добавить отсекание на этапе генерации матриц вида.
Текущая версия доступна на теге v0.2 в репозитории 13.
Изменение параметров сцены
Для наглядности изображения результатов полученных теней изменим параметры источников на сцене в файле src/main.cpp:
// Источники света
Light& first = Light::getNew();
first.e_color() = {1.0f, 0.0f, 0.0f}; // цвет
first.e_position() = {0.3f, 0.0f, 0.6f}; // Позиция
first.e_angle() = 100.0f;
Light& second = Light::getNew();
second.e_color() = {0.0f, 0.0f, 1.0f}; // цвет
second.e_position() = {-0.3f, 0.3f, 0.5f}; // Позиция
А так же характеристики материала плоскости, на которую проецируются тени — убавив фоновую освещенность и увеличив коэффициент диффузного отражения:
// Параметры материала
rectangle.material.ka = {0.2, 0.2, 0.2};
rectangle.material.kd = {0.9, 0.9, 0.9};
Результат работы приложения представлен на рисунках 2-4.
Текущая версия доступна на теге v0.3 в репозитории 13.
Заключение
В данной заметке рассмотрена работа с массивами кубических текстур (3D кубические текстуры), создан класс для упрощения работы с ними, реализован алгоритм расчета теней для точечного источника.
Проект доступен в публичном репозитории: 13
Библиотеки: dependencies
Ресурсы: resources