Пару дней назад я наткнулся на первую статью из этой серии уроков. К сожалению, переведены только начальные уроки, а все самое интересное (SSAO, PBR, тени) только впереди. Этот курс переводили три человека (теперь четыре), и я надеюсь, что кто-то из читателей поможет с переводом остальных частей. К сожалению, я не являюсь профессиональным переводчиком, по этом в тексте могут быть ошибки разного характера. Буду рад, если вы сообщите о них. Приятного чтения!
- OpenGL
- Создание окна
- Hello Window
- Hello Triangle
- Shaders
- Текстуры
- Трансформации
- Системы координат
- Камера
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
Часть 4. Продвинутые возможности OpenGL
- Тест глубины
- Тест трафарета
- Смешивание цветов
- Отсечение граней
- Кадровый буфер
- Кубические карты
- Продвинутая работа с данными
- Продвинутый GLSL
- Геометричечкий шейдер
- Инстансинг
- Сглаживание
Часть 5. Продвинутое освещение
- Продвинутое освещение. Модель Блинна-Фонга.
- Гамма-коррекция
- Карты теней
- Всенаправленные карты теней
- Normal Mapping
- Parallax Mapping
- HDR
- Bloom
- Отложенный рендеринг
- SSAO
Часть 6. PBR
Материалы
В реальном мире каждый объект по-разному реагирует на свет. Железные объекты обычно сверкают сильнее, чем, например, глиняная ваза. А деревянный контейнер реагирует на свет не также, как стальной. Каждый объект имеет разную отражающую способность. Некоторые объекты отражают свет без сильного рассеивания, у других радиус отражения довольно большой. Если мы хотим имитировать разные типы объектов в OpenGL, то нам нужно определить свойства материала, специфичные для каждого объекта.
В предыдущем уроке мы устанавливали цвет света, чтобы определить внешний вид объекта, скомбинированный с фоновой и отражающей компонентой. Описывая объекты, мы можем установить цвет материала для всех трех компонентов освещения: окружающего (ambient), рассеянного (diffuse) и бликового (specular). После этого мы получим детальный контроль над результирующим цветом объекта. Теперь добавим силу блеска (shininess) к нашим трем цветам и получим все свойства материала, которые нам нужны:
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
uniform Material material;
Во фрагментном шейдере мы создаем структуру, хранящую свойства материала объекта. Конечно, мы можем хранить из как независимые uniform-переменные, но это менее организованно. Первым делом мы определяем структуру, а затем просто объявляем unifrom-переменную с типом только что созданной структуры.
Как вы можете видеть, мы определяем цветовой вектор для каждого компонента освещения по Фонгу. Вектор ambient
определяет, какой цвет объект отражает под фоновым освещением. Обычно это цвет самого объекта. Вектор diffuse
определяет цвет объекта под рассеянным освещением. Также, как и фоновый, он определяет желаемый цвет объекта. Вектор specular
устанавливает цвет блика на объекте, а переменная shininess
— радиус этого блика.
Используя эти четыре компонента, мы можем имитировать множество реальных материалов. Таблица на сайте devernay.free.fr содержит свойства некоторых материалов, которые мы может видеть в реальной жизни. Следующее изображение показывает наш куб, с разными материалами:
Как вы могли заметить, правильно подобранные свойства материалов полностью преображают наш куб. Конечно, это видно невооруженным глазом, но для большей реалистичности, нам все равно нужна фигура поинтереснее. В следующей секции уроков мы обсудим загрузку 3d-моделей любой сложности.
Подбор правильных материалов для объекта это сложное искусство, которое требует много опыта и экспериментов. По этому не редки случаи, когда полностью терялось визуальное качество объекта, если ему подобрали не подходящий материал.
Давайте попробуем реализовать систему материалов в шейдерах.
Настройка материалов
Мы создали структуру материала во фрагментом шейдере, по этому нам нужно учесть это при дальнейших расчетах освещения. Так как все переменные материала хранятся в структуре мы можем получить доступ к ним через uniform-переменную material
:
void main()
{
// ambient
vec3 ambient = lightColor * material.ambient;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = lightColor * (diff * material.diffuse);
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = lightColor * (spec * material.specular);
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
Как вы могли заметить, сейчас при расчетах освещения мы используем свойства структуры material
, по этому, в этот раз результирующий цвет зависит от определенного нами материала. Каждый компонент материала объекта умножается на соответствующий ему компонент света.
Мы можем настроить материал объекта в приложении, установив значение подходящих uniform-переменных. Однако в GLSL нет особого способа присваивания значений структурам. Структура всего лишь инкапсулирует uniform-переменные. По этому, если мы хотим заполнить ее, мы должны установить значение каждой uniform-переменной точно также, как делали это раньше, но, на этот раз, с приставкой имени структуры:
lightingShader.setVec3("material.ambient", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.diffuse", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 32.0f);
Мы установили фоновую и рассеянную компоненту в цвет самого объекта, а блик сделали умеренно-ярким. Нам не нужно, чтобы он сильно выделялся. Также, мы оставили силу блеска равной 32. Теперь мы можем легко влиять на материал объекта из приложения.
Запуск программы выдаст следующие результаты:
Пока что выглядит не очень реалистично, верно?
Свойства света
Сейчас наш объект слишком яркий. Причина этого кроется в том, что фоновый, рассеянный и бликовый цвета с полной силой отражаются от любого источника света. Однако источники света тоже могут иметь разную интенсивность. В предыдущем уроке мы решили эту проблему, заменив фоновую и рассеянную компоненту константными значениями. Сейчас нам нужно сделать что-то подобное, но теперь для каждой компоненты света. Если мы представим lightColor
как vec3(1.0)
, то код получиться таким:
vec3 ambient = vec3(1.0) * material.ambient;
vec3 diffuse = vec3(1.0) * (diff * material.diffuse);
vec3 specular = vec3(1.0) * (spec * material.specular);
Эти значения (vec3(1.0)
) могут быть изменены индивидуально для каждого источника света, а это именно то, что нам нужно. Сейчас цвет фонового компонента полностью влияет на цвет куба, но фоновый компонент не должен так сильно влиять на итоговый цвет, так что сделаем его немного меньше:
vec3 ambient = vec3(0.1) * material.ambient;
Мы можем влиять на диффузный и бликовый компонент света точно также. Это тесно связано с тем, что мы делали в предыдущем уроке. Сейчас нам нужно создать что-то похожее на структуру материала, но теперь для света:
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform Light light;
Источник света имеет разную интенсивность для фонового, рассеянного и бликового света. Обычно интенсивность фонового света довольно низкая, мы ведь не хотим, чтобы он слишком доминировал? Цвет диффузного компонента — это цвет самого источника света; зачастую это ярко-белый. Зеркальную компоненту обычно оставляют равной vec3(1.0)
. Замечу, что мы также добавили вектор позиции света в структуру.
Теперь обновим расчеты освещения во фрагментном шейдере:
vec3 ambient = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);
Нам также нужно установить интенсивность света в самом приложении:
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f); // darken the light a bit to fit the scene
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
Сейчас мы смоделировали влияние света на материал объекта и получили результат, похожий на результат предыдущего урока. Однако теперь у нас есть полный контроль над освещением и материалом объекта:
Теперь изменять внешний вид объекта относительно несложно.
Различные цвета света
До сих пор цвет нашего источника света варьировался от белого до черного, по этому мы не могли изменить цвет самого объекта (только его интенсивность). Однако, с этого момента у нас есть простой доступ к свойствам света, по этому мы можем изменять его с течением времени, чтобы получать такие прикольные результаты:
Как вы могли заметить, изменение света влияет на то, какие цвета объект может отразить (наверное, вы это помните из урока, посвященного цветам), а это, в свою очередь, существенно влияет на результат.
Мы с легкостью можем время от времени варьировать свет, изменяя фоновый и диффузный цвета через sin
и glfwGetTime
:
glm::vec3 lightColor;
lightColor.x = sin(glfwGetTime() * 2.0f);
lightColor.y = sin(glfwGetTime() * 0.7f);
lightColor.z = sin(glfwGetTime() * 1.3f);
glm::vec3 diffuseColor = lightColor * glm::vec3(0.5f); // decrease the influence
glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); // low influence
lightingShader.setVec3("light.ambient", ambientColor);
lightingShader.setVec3("light.diffuse", diffuseColor);
Пробуйте и экспериментируйте с освещением и материалами и вы увидите, как они влияют на результат. Вы можете найти исходный код приложения здесь.
Упражнения
- Сможете ли вы смоделировать некоторые реальные объекты, наделив их соответствующими материалами, которые мы видели в начале этого урока? Обратите внимание, что табличные значения фонового света не совпадают с диффузными — они не принимают во внимание интенсивность света. Чтобы получить правильные результаты, вам нужно установить интенсивность каждой компоненты света равной
vec3(1.0)
. Решение для сине-зеленого пластмассового контейнера.