Введение
Согласно заметке об освещении в трехмерных приложениях существуют четыре вида источников света:
- фоновое (ambient) — нулевая отметка при расчете освещения, обеспечивающая равномерное освещение при отсутствии других источников;
- точечное (omni) — точка в пространстве, которая светит одинаково во все стороны;
- прожектор (spot) — точечный источник, ограниченный конусом, задающим направление;
- направленное (direct) — бесконечно далеко удаленная точка, лучи которой идут параллельно (упрощает расчет освещения) благодаря расстоянию до неё.
Фоновое и точечное реализованы в рамках первой части заметок об освещении в OpenGL.
Прожектор
Прожектор (spot) — это точечный источник в пространстве, ограниченный конусом, задающим направление. На рисунке 1 изображен направленный источник освещения.
На рисунке 1 изображен угол angle, ограничивающий максимальное значение угла между векторами направления (direction) и обратным расположения источника относительно поверхности (L_vertex). Для всенаправленного источника (обычный точечный) значения угла будет равно 180°.
Для удобства восприятия пользователем в классе Light будет храниться двойноу угол angle, а при загрузке на шейдер будет отправляться половина.
Для задания конуса, ограничивающего область действия источника, необходимо добавить приватное поле и публичные методы доступа к нему для класса источника света: Light::angle (угол) в файле include/Lights.h:
// Источник света
class Light : public Node
{
public:
...
const float& c_angle() const; // Константный доступ к углу освещенности
float& e_angle(); // Неконстантная ссылка для изменений угла освещенности
...
private:
...
float angle; // Угол полный освещенности
...
};
А так же добавить поле direction_angle к структуре LightData:
// Точечный источник света
struct LightData
{
alignas(16) glm::vec3 position; // Позиция
alignas(16) glm::vec3 color; // Цвет
alignas(16) glm::vec3 attenuation; // Радиус действия источника, линейный и квадратичный коэф. угасания
alignas(16) glm::vec4 direction_angle; // Направление и половинный угол освещенности
};
Направление источника задается на основании кватерниона поворота родительского класса Node::rotation. Для перевода кватерниона в направление необходимо задать стандартное направление:
// Стандартное направление источника без поворота
#define DEFAULT_LIGHT_DIRECTION glm::vec4(0.0f, 0.0f, 1.0f, 0.0f)
Реализация методов доступа, а так же модификация конструктора и оператора присваиваниия:
// Конструктор без параметров
Light::Light() : Node(), index(-1), uploadReq(false), color(1.0f), radius(10.0f), angle(360.0f)
{
}
// Оператор присваивания
Light& Light::operator=(const Light& other)
{
// Проверка на самоприсваивание
if (this != &other)
{
index = other.index; // Переносим индекс
uploadReq = other.uploadReq; // Необходимость загрузки
color = other.color;
radius = other.radius;
angle = other.angle;
Node::operator=(other);
}
return *this;
}
// Константный доступ к углу освещенности
const float& Light::c_angle() const
{
return angle;
}
// Неконстантная ссылка для изменений угла освещенности
float& Light::e_angle()
{
uploadReq = true;
return angle;
}
Тогда метод загрузки в буфер будет иметь следующий вид:
// Преобразует информацию об источнике в структуру 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]; // Квадратичный коэф. угасания
}
// Направление и угол источника
data[index].direction_angle = glm::vec4( glm::normalize(glm::vec3(result_transform * DEFAULT_LIGHT_DIRECTION))
, angle / 2 // Половинный угол для вычислений на шейдере
);
}
В фрагментном шейдере, рассчитывающем освещенность фрагментов (shaders/lighting.frag), необходимо дополнить uniform-блок освещения и добавить проверку угла:
...
struct LightData
{
vec3 position;
vec3 color;
vec3 attenuation;
vec4 direction_angle;
};
layout(std140, binding = 2) uniform Light
{
LightData data[300];
int count;
} light_f;
...
void main()
{
...
// Переменные используемые в цикле:
float acosA; // Косинус между вектором от поверхности к источнику и обратным направлением источника
...
// Проверка на дистанцию
if (L_distance < light_f.data[i].radius)
{
// Нормирование вектора
L_vertex = normalize(L_vertex);
// арккосинус между вектором от поверхности к источнику и обратным направлением источника
acosA = degrees(acos(dot(-L_vertex, normalize(light_f.data[i].direction_angle.xyz))));
// Если угол меньше угла источника или угол источника минимален, то считаем освещенность
if(acosA <= light_f.data[i].direction_angle.a)
{
// Диффузная составляющая
diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
...
color += vec4(light_f.data[i].color*kd*diffuse * attenuation, 1)
+ vec4(light_f.data[i].color*ks*specular * attenuation, 1);
}
...
Функция degrees конвертирует значение угла из радиан в градусы.
Для отладочного вывода упростим задачу, рисуя только фрагменты, попадающие в конус. Из вершинного шейдера (shaders/bulb.vert) передадим в фрагментный информацию о локальном положении фрагмента:
...
out vec3 pos_local;
void main()
{
pos_local = pos;
gl_Position = camera.projection * camera.view * model * vec4(pos, 1.0);
}
В фрагментном шейдере (shaders/bulb.frag) добавим две uniform-переменные, которые заполняются в методе Bulb::render, и добавим проверку:
...
in vec3 pos_local;
out vec4 color;
uniform float angle;
uniform vec3 direction;
void main()
{
float cosA = dot(normalize(pos_local), normalize(direction));
if (degrees(acos(cosA)) <= angle)
color = vec4(ka, 1);
else
discard;
}
Важное замечание: вектор pos_local, выполняющий здесь роль L_vertex, берется без изменения для того, чтобы окрашивались только вершины, входящие в конус.
Ключевое слово discard отбрасывает обрабатываемый фрагмент.
Метод рисования отладочной лампочки в файле src/Lights.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);
GLuint angle_uniform = shaderProgram.getUniformLoc("angle");
GLuint direction_uniform = shaderProgram.getUniformLoc("direction");
// Цикл по источникам света
for (int i = 0; i < count; i++)
{
// Загрузим направление
glUniform3fv(direction_uniform, 1, &data[i].direction_angle.x);
// Угол для лампочки = 180 (рисуем целую модель)
glUniform1f(angle_uniform, 180); // Зададим параметры материала сфере действия
// Сдвиг на позицию источника
bulb.root.e_position() = data[i].position;
sphere.e_scale() = glm::vec3(data[i].attenuation.r); // Масштабирование сферы
// Задание цвета
bulb.models[0].material.ka = sphere.material.ka = data[i].color;
// Вызов отрисовки
bulb.render(shaderProgram, material_buffer);
// Угол для сферы (рисуем направленный конус)
glUniform1f(angle_uniform, data[i].direction_angle.a);
// Рисование сферы покрытия источника в режиме линий
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
sphere.render(shaderProgram, material_buffer);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
}
Результат работы представлен на рисунке 2.
На рисунке 2 можно заметить жесткую границу при прохождении отсекающего конуса.
Для её смягчения добавим вычисление интенсивности в фрагментном шейдере shaders/lighting.frag, если источник является прожектора:
...
// Переменные используемые в цикле:
float acosA; // Косинус между вектором от поверхности к источнику и обратным направлением источника
float intensity; // Интенсивность для прожектора источника
...
// Угасание с учетом расстояния
attenuation = 1 / (1 + light_f.data[i].K[0] * L_distance + light_f.data[i].K[1] * L_distance * L_distance);
// Если источник - прожектор, то добавим смягчение
if (light_f.data[i].angle < 180)
{
intensity = clamp((light_f.data[i].direction_angle.a - acosA) / 5, 0.0, 1.0);
diffuse *= intensity;
specular *= intensity;
}
...
Здесь в знаменателе число 5 — значение угла между векторами направления источника и обратным расположения поверхности относительно источника, для которых будет рассчитываться угасание.
Результат смягчения границ представлен на рисунке 3.
Текущая версия доступна на теге v0.1 в репозитории 10.
Направленный источник освещения
Направленным источником освещения называется источник, который бесконечно далеко удален от камеры, что его положением относительно фрагмента (все лучи падают параллельно) и затуханием его лучей можно пренебречь. Таким источником может выступать солнце. Такой источник представлен на рисунке 4.
Для реализации такого источника добавим класс-синглтон Sun в файле include/Lights.h:
// Класс направленного источника освещения
class Sun
{
public:
static Sun& get(); // Доступ к синглтону
static void upload(UBO& sun_data); // Загрузка данных об источнике в буфер
const glm::vec3& c_direction() const; // Константный доступ к направлению лучей источника
glm::vec3& e_direction(); // Неконстантная ссылка для изменений направления лучей источника
const glm::vec3& c_color() const; // Константный доступ к цвету
glm::vec3& e_color(); // Неконстантная ссылка для изменений цвета
private:
Sun(const glm::vec3 &direction = glm::vec3(0.0f, 1.0f, 0.0f), const glm::vec3 &color = glm::vec3(0.4f, 0.4f, 0.4f));
alignas(16) glm::vec3 direction; // Направление лучей источника
alignas(16) glm::vec3 color; // Цвет
static Sun instance; // Экземпляр синглтона
static bool uploadReq; // Необходимость загрузки в следствии изменений
};
Данный класс учитывает наличие изменений по параметрам источника и позволяет пропускать загрузку в uniform-буфер в случае, если изменений не было.
Реализация конструктора и методов в файле src/Lights.cpp:
// Конструктор направленного источника с параметрами направления и цвета
Sun::Sun(const glm::vec3 &dir, const glm::vec3 &c) : direction(dir), color(c)
{
}
// Доступ к синглтону
Sun& Sun::get()
{
return instance;
}
// Загрузка данных об источнике на шейдер
void Sun::upload(UBO& sun_data)
{
if (uploadReq)
{
sun_data.loadSub(&instance, sizeof(instance));
uploadReq = false;
}
}
// Константный доступ к направлению лучей источника
const glm::vec3& Sun::c_direction() const
{
return instance.direction;
}
// Неконстантная ссылка для изменений направления лучей источника
glm::vec3& Sun::e_direction()
{
uploadReq = true;
return instance.direction;
}
// Константный доступ к цвету
const glm::vec3& Sun::c_color() const
{
return instance.color;
}
// Неконстантная ссылка для изменений цвета
glm::vec3& Sun::e_color()
{
uploadReq = true;
return instance.color;
}
В фрагментном шейдере, рассчитывающем освещенность фрагмента (shaders/lighting.frag), добавим новый uniform-буфер:
layout(std140, binding = 3) uniform Sun
{
vec3 direction;
vec3 color;
} sun;
После расчета фоновой освещенности добавим вычисления для солнца, если его цвет не черный:
...
// Фоновая освещенность
color = vec4(ka, 1);
// Расчет солнца, если его цвет не черный
if (length(sun.color) > 0)
{
// Данные об источнике относительно фрагмента
L_vertex = normalize(sun.direction);
// Диффузная составляющая
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(sun.color*kd*diffuse, 1)
+ vec4(sun.color*ks*specular, 1);
}
...
Результат работы программы с серым (0.4, 0.4, 0.4) солнцем, направленным на координаты (0, -1, 0), представлен на рисунке 5.
Возможна простая оптимизация: инвертировать вектор направления лучей, тогда в фрагментном шейдере не будет требоваться знак минус, а вектор падения лучей поменяет свой смысл на вектор направления на солнце. Для этого в файле src/Lights.h изменим начальное положение и в преть будем считать вектор direction направлением на источник от поверхности, а не наоборот:
Sun(const glm::vec3 &direction = glm::vec3(0.0f, 1.0f, 0.0f), const glm::vec3 &color = glm::vec3(0.4f, 0.4f, 0.4f));
Тогда в фрагментном шейдере shaders/lighting.frag:
L_vertex = normalize(sun.direction); // Без минуса
Текущая версия доступна на теге v0.2 в репозитории 10.
Заключение
В ходе работы над заметкой были созданы два новых типа источников освещения: прожектор и направленный, для направленного добавлено смягчение краев, прилегающих к ограничивающему конусу.
Проект доступен в публичном репозитории: 10
Библиотеки: dependencies
Ресурсы: resources