Несколько источников освещения
В предыдущих уроках мы выучили довольно много об освещении в OpenGL. Мы познакомились с моделью освещения по Фонгу, разобрались как работать с материалами, текстурными картами и различными типами источника света. В этом уроке мы собираемся объединить все наши знания, чтобы создать полностью освещенную сцену с 6 активными источниками света. Мы собираемся симулировать солнце как направленный источник освещения, добавим 4 точки света, разбросанные по всей сцене, и конечно мы добавим фонарик.
В предыдущих сериях
Часть 1. Начало
- OpenGL
- Создание окна
- Hello Window
- Hello Triangle
- Shaders
- Текстуры
- Трансформации
- Системы координат
- Камера
Часть 2. Базовое освещение
Чтобы использовать более чем 1 источник света в сцене, нам следует разбить расчеты типов освещения на функции. Если мы добавим расчеты всех освещений в одну функцию main, то наш код быстро испортится.
Функции в GLSL такие-же как и функции в языке C. У нас есть имя функции, возвращаемый тип и также мы можем объявить её прототип сверху, а описать её снизу. Мы создадим разные функции для каждого типа освещения: направленного источника освещения, точечного источника и прожектора.
При использовании нескольких источников освещение в сцене, подход обычно такой: у нас есть 1 вектор цвета, который представляет выходной цвет фрагмента. Результат вычисления каждого источника света добавляется к этому вектору. То есть мы будем вычислять каждый цвет фрагмента, относительно каждого источника света в сцене, и комбинировать его с выходным цветом. Сама функция main может выглядеть примерно так:
void main()
{
// устанавливаем значение нашего выходного цвета
vec3 output = vec3(0.0);
// добавляем значение, полученное из направленного источника освещения
output += someFunctionToCalculateDirectionalLight();
// делаем тоже самое и с точечным источником
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// и добавляем остальные значения так же
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
out vec4 FragColor;
Конечный код будет отличатся в зависимости от реализации, но функция main останется такой же. Мы определяем наши функции, вычисляющие их цвет для данного фрагмента и добавляем его в наш результат, который мы присваиваем выходному вектору цвета. Если для примера 2 источника света будут близко находится к фрагменту, их комбинацией будет результат более яркий освещенный фрагмент, чем фрагмент освещенный одним источником освещения.
Направленный источник освещения
Нам нужно написать функцию в фрагментальном шейдере, которая вычисляет результат направленного света на какой-либо фрагмент: функция принимает несколько параметров и возвращает вычисленное значение цвета фрагмента.
Первое, что нам нужно, это решить, какой минимальный набор переменных нам нужен для вычисления направленного источника света. Мы будем хранить переменные в структуре DirLight и объявим её объект как uniform. Эти переменные должны быть вам хорошо знакомы с
предыдущего урока:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
Мы можем передать наш объект dirLight в функцию со следующим прототипом:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
Также как в C и C++, если мы хотим вызвать функцию (в нашем случае внутри функции main) функция должна быть объявлена где-нибудь до того момента, где мы её вызываем. В нашем случае, мы объявим прототип над функцией main, а опишем её где нибудь ниже.Вы можете видеть, что функция требует DirLight структуру и 2 вектора. Если вы успешно завершили предыдущие уроки, тогда код этой функции не должен вызывать для вас вопросов:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// диффузное освещение
float diff = max(dot(normal, lightDir), 0.0);
// освещение зеркальных бликов
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// комбинируем результаты
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
return (ambient + diffuse + specular);
}
Пользуясь примером кода с предыдущего урока и используя вектора, которые принимает функция в качестве аргументов, вычисляем результат каждого компонента (ambient, diffuse и specular). Затем суммируем наши компоненты и получаем конечный цвет фрагмента.
Точечные источники освещения
Так же как и в направленном источнике освещения, мы также определяем функцию, вычисляющее цвет фрагмента от точечного света, включая затухание. Также как и в направленном источнике освещения, мы объявим структуру с минимальным набором
переменных для точечного источника:
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
Как вы можете видеть мы использовали препроцессор GLSL, что-бы объявить число точечных источников NR_POINT_LIGHTS равное 4. Мы используем эту константу NR_POINT_LIGHTS, чтобы создать объект массив структуры PointLight. Массивы в GLSL такие же как и массивы в C и
могут быть созданы с использованием двух квадратных скобок. Прямо сейчас у нас есть 4 объекта pointLights[NR_POINT_LIGHTS] структуры PointLigh.
Мы могли также создать одну большую структуру, которая включала бы все нужные переменные для всех разных типов освещения, и использовали бы её для каждой функции, игнорируя переменные в которых бы мы не нуждались. Хотя, я лично нахожу нынешний подход более лучшим, т.к. не всем типам освещения будут нужны все переменные.Прототип функции точечного источника:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
Функция принимает все данные, в которых она нуждается и возвращает vec3 с вычисленным цветом фрагмента. Снова, несколько манипуляций копировать-вставить из предыдущего урока, приводит к следующему результату:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// диффузное освещение
float diff = max(dot(normal, lightDir), 0.0);
// освещение зеркальных бликов
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// затухание
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// комбинируем результаты
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
Основное преимущество функции, что мы можем рассчитать результирующий цвет, для разных точечных источников, не дублируя код. В функции main мы просто создаем цикл, в котором итерируемся по нашему массиву точечного источника и вызываем функцию CalcPointLight для каждой точки.
Объединяем всё вместе
Теперь, когда мы написали наши функции для направленного и точечного источника освещения, мы можем вызвать их в функции main.
void main()
{
// свойства
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// фаза 1: Направленный источник освещения
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// фаза 2: Точечные источники
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// фаза 3: фонарик
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
Каждый тип освещения добавляет свой вклад в выходной вектор цвета, до тех пор, пока не будут обработаны все источники света. Выходной вектор цвета содержит все вычисления источников света в сцене. Если вы хотите, вы можете также добавить сами функцию прожектора, опираясь на код с предыдущего урока. Мы оставим функцию CalcSpotLight в качестве упражнения для читателей.
Установка значений uniform переменным не должна быть для вас незнакомой, но вы можете быть удивлены как мы можем установить uniform переменные для объекта структуры точечного освещения.
Удачно для нас, что это не слишком сложно. Чтобы задать значение конкретному объекту uniform массива, нужно лишь обратиться к этому объекту, как к обычному массиву (через индекс).
lightingShader.setFloat("pointLights[0].constant", 1.0f);
Здесь мы обращаемся к 1 элементу нашего массива pointLights и устанавливаем значение 1.0f переменной constant. К сожалению это значит, что мы должны таким-же способом установить все переменные, всем элементам массива, что в итоге приведет к 28 строка кода. Вы можете попытаться, написать боле удобный код для этой задачи.
Не забываем, что мы также нуждаемся в position векторе, для каждого точечного источника освещения. Мы определим массив glm::vec3, который будет хранить эти позиции:
glm::vec3 pointLightPositions[] = {
glm::vec3( 0.7f, 0.2f, 2.0f),
glm::vec3( 2.3f, -3.3f, -4.0f),
glm::vec3(-4.0f, 2.0f, -12.0f),
glm::vec3( 0.0f, 0.0f, -3.0f)
};
Далее, мы просто проиндексируемся по этому массиву и установим значения position для каждого объекта массива pointLight. Также, нам нужно нарисовать 4 световых куба, вместо 1. Простой способ сделать это, передавать разные значения матрицы модели, используя наш только что созданный массив pointLightPositions.
Если вы будите использовать фонарик, то наша сцена будет выглядеть примерно так:
Как вы можете видеть, эта сцена включает в себя все наши типы освещения: глобальный свет (как солнце) где-то в небе, так же есть 4 разбросанных световых ящиков по всей сцене и фонарик, светящий из камеры наблюдателя. Выглядит довольно аккуратно, не правда ли?
Вы можете найти полный код здесь.
На изображение представлены все типы освещения с настройками по умолчанию, которые мы использовали в предыдущих уроках, но если вы поиграете с этими значениями, вы можете получить довольно интересные результаты. Художники, как правило настраивают эти переменные освещения в каком нибудь редакторе, чтобы убедится, что освещения соответствуют реальной среде. Используя нашу освещенную среду, вы можете настроить разные, интересные визуальные эффекты, просто настроив их артрибуты:
Мы также изменили значение ClearColor, чтобы лучше отражать свет. Вы можете видеть, что просто регулируя некоторые параметры освещения вы можете создать совершенно разную атмосферу.
Сейчас вы должны иметь довольно хорошее понимание об освещении в OpenGL. С этими знаниями, вы уже можете создать интересную, визуально богатую и атмосферную сцену. Попробуйте поиграться с разными значениями освещения, чтобы создать свою собственную атмосферную сцену.
Задания
Воссоздайте различные типы атмосферных сцен, с последнего изображения, заменив значения артрибутов света ссылка.
→ Оригинал статьи