Ранее мы обсуждали возможность каждого объекта иметь уникальный материал, чтобы по-разному реагировать на свет. Это отлично подходит для того, чтобы придать каждому объекту уникальный вид относительно других объектов на сцене. Но этого все еще не дает нам большой гибкости в настройке внешнего вида объекта.
Часть 2. Базовое освещение
Текстурные карты
В предыдущем уроке мы определили материал для целого объекта, но в реальном мире объекты обычно состоят не из одного, а из нескольких материалов. Представьте себе машину: ее внешний корпус блестящий; окна частично отражают окружающую среду; у машины также есть матовые покрышки, а еще есть сверкающие обода (сверкающие, если вы хорошо моете свою машину). Так вот, каждый объект имеет разные свойства материала для каждой своей части.
Итак, наша система материалов из предыдущего урока не подходит для более или менее сложных объектов, по этому нам нужно расширить ее, введя диффузную и бликовую карты. Это даст нам возможность влиять на диффузный (и, косвенным образом, на фоновый, так как это почти всегда одно и то же) и бликовый компоненты объекта с большей точностью.
Диффузные карты
Все, что нам нужно — это способ установить диффузный цвет для каждого фрагмента объекта. Что может повлиять на значение цвета, основываясь на позиции фрагмента?
Припоминаете? Это — текстуры, которые мы интенсивно обсуждали в одном из предыдущих уроков. Карты освещения – всего лишь другое название того же принципа: используя изображение, нанесенное на поверхность объекта, мы можем делать выборки цвета для каждого фрагмента. В сценах с освещением это обычно называется диффузной картой (как правило ее так называют 3D художники), так как текстурное изображение представляет все диффузные цвета объекта.
Для демонстрации диффузных карт мы будем использовать изображение деревянного контейнера с железной рамкой:
Использование диффузных карт в шейдерах очень похоже на использование текстур в одном из предыдущих уроков. Однако теперь мы заменим ранее определенный вектор vec3
диффузного цвета диффузной картой sampler2D
.
Имейте в виду, что
sampler2D
это, так называемый, непрозрачный тип данных. Это значит, что мы не можем создать экземпляр такого типа, мы можем только определить его как uniform. Если мы попытаемся использовать этот тип не как uniform (например как параметр функции), то GLSL выведет странные ошибки. Это правило также распространяется и на любую структуру, которая содержит непрозрачный тип.
Мы также удалили вектор ambient
, так как он в большинстве случаев совпадает с диффузным цветом, так что нам не нужно хранить его отдельно:
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;
Если вы такой упрямый и все еще хотите установить фоновый цвет отличным от диффузного, то можете оставить вектор
ambient
. Но фоновый цвет будет один для всего объекта. Чтобы получить различные значения фонового цвета для каждого фрагмента объекта, вам нужно использовать отдельную текстуру для значений фонового цвета.
Обратите внимание, что нам снова нужны текстурные координаты во фрагментном шейдере, по этому мы объявляем дополнительную входящую переменную. Затем мы просто делаем выборку из текстуры, чтобы извлечь значение диффузного цвета фрагмента:
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
Также не забудьте установить фоновый цвет материала таким же, как и диффузный:
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
Это все, что нам нужно для использования диффузной карты. Как вы могли заметить, в этом нет ничего нового, но это дает впечатляющий рост визуального качества. Чтобы это заработало, нам нужно добавить текстурные координаты к данным вершин и передать их в качестве вершинного атрибута во фрагментный шейдер, загрузить текстуру и связать ее с соответствующим текстурным блоком.
Обновленные вершинные данные можно найти здесь. Теперь они включают в себя позиции вершин, векторы нормалей и текстурные координаты для каждой вершины куба. Давайте обновим вершинный шейдер, чтобы он мог принимать текстурные координаты, как вершинный атрибут и передавать их во фрагментный шейдер:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;
void main()
{
...
TexCoords = aTexCoords;
}
Удостоверьтесь, что обновили вершинные атрибуты обоих VAO (прим. переводчика: имеются ввиду VAO текстурированного куба и VAO куба-лампы), так чтобы они совпадали с новыми вершинными данными, и загрузили изображение контейнера в текстуру. Перед тем, как нарисовать контейнер нам нужно присвоить переменной material.diffuse
предпочтительный текстурный блок и связать с ним текстуру контейнера:
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
Используя диффузную карту, мы снова получили огромный прирост детализации и теперь, с добавленным освещением, наш контейнер действительно начал блистать (в буквальном смысле). Вероятно, теперь он выглядит вот так:
Вы можете найти полный исходный код приложения здесь.
Бликовые карты
Вероятно, вы заметили, что блик выглядит немного странно, ведь наш объект — это контейнер, который большей частью состоит из дерева. А, как мы знаем, дерево не дает такого зеркального блеска. Мы можем исправить это, установив вектор specular
в структуре Material
равным vec3(0.0)
, но это значит, что железная рамка контейнера тоже перестанет давать блики, а мы знаем, что металл должен, хотя бы немного блестеть. И снова мы хотели бы контролировать, какие части объекта должны блестеть и с какой силой. Эта проблема очень похожа на обсуждение диффузных карт. Совпадение? Не думаю.
Мы снова можем воспользоваться текстурной картой, только теперь для зеркальных бликов. Это значит, что нам нужно создать черно-белую (или цветную, если хотите) текстуру, которая определит силу блеска каждой части объекта. Вот пример бликовой карты:
Интенсивность блеска определяется яркостью каждого пикселя изображения. Каждый пиксель такой карты может быть представлен как цветовой вектор, где черный цвет — это vec3(0.0)
, а серый — vec3(0.5)
, например. Затем, во фрагментном шейдере мы отбираем соответствующее цветовое значение и умножаем его на интенсивность бликового цвета. Соответственно, чем "белее" пиксель, тем больше получается результат умножения, а, следовательно, и яркость блика на фрагменте объекта.
Так как контейнер большей частью состоит из дерева, а дерево — это материал, который не дает бликов, то вся "деревянная" часть текстуры закрашена черным. Черные части вообще не блестят. Поверхность стальной рамки контейнера имеет переменную силу зеркального блеска: сама сталь дает довольно интенсивные блики, в то время как трещины и потертости – нет.
Технически, дерево тоже имеет зеркальные отражения, хотя и с намного более низкой силой блеска (свет сильнее рассеивается), но в образовательных целях, мы сделаем вид, что дерево никак не реагирует на зеркальный свет.
Используя инструменты, такие как Photoshop или Gimp, довольно просто превратить диффузную текстуру в бликовую. Достаточно просто вырезать некоторые части, сделать изображение черно-белым и увеличить яркость/контрастность.
Сэмплинг бликовых карт
Карта зеркальных отражений — это самая обычная текстура, по этому код ее загрузки очень похож на загрузку диффузной карты. Удостоверьтесь, что вы правильно загрузили изображение и сгенерировали текстурный объект. Так как мы используем новую текстуру в том же фрагментом шейдере, нам нужно использовать другой текстурный блок для карты бликов. Давайте свяжем эту текстуру с соответствующим текстурным блоком перед визуализацией:
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
Затем обновим свойства материала во фрагментном шейдере, чтобы отражающий компонент принимался в качестве sampler2D
, а не vec3
:
struct Material {
sampler2D diffuse;
sampler2D specular;
float 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));
FragColor = vec4(ambient + diffuse + specular, 1.0);
Используя карту бликов, мы можем с чрезвычайной точностью определить, какие части объекта дают зеркальные блики, и установить соответствующую их интенсивность. Таким образом, карта бликов дает нам дополнительный уровень контроля поверх диффузной карты.
Вы также можете использовать в бликовой карте цвета, которые определяют не только интенсивность блика, но и его цвет. Однако в реальности, цвет блика в значительной степени (а в большинстве случаев и полностью) зависит от источника света, поэтому использование цветных карт бликов не даст реалистичных результатов (вот почему эти изображения обычно черно-белые — нас интересует только интенсивность блика).
Если вы запустите приложение, то увидите, что материал контейнера очень похож, на деревянный контейнер с железной рамкой:
Вы можете найти полный исходный код приложения здесь.
Используя диффузные и бликовые карты, мы можем добавить огромное количество деталей в относительно простые объекты. Мы можем добавить еще больше деталей, используя другие текстурные карты, такие как карты нормалей/рельефа и/или карты отражений, но их мы прибережем для следующих уроков. Покажите ваш контейнер друзьям и семье и помните, что однажды наш контейнер может стать еще более привлекательным, чем сейчас!
Упражнения
- Попробуйте поиграться с фоновым, диффузным и бликовым векторами источника света и посмотрите, как они влияют на внешний вид объекта.
- Попробуйте инвертировать цвета бликовой карты во фрагментном шейдере, т.е. дерево должно блестеть, а железная рамка — нет (заметьте, что трещины на рамке все еще дают зеркальное отражение, хотя и с меньшей интенсивностью): решение.
- Попробуйте создать карту бликов из диффузной карты, которая использует не черно-белые цвета, и вы увидите, что результат не очень реалистичный. Вы можете использовать эту цветную карту бликов, если не можете сделать ее сами. Результат.
- Вы также можете попробовать добавить к нашему кубу карту эмиссии, которая хранит значение свечения каждого фрагмента объекта. Значения эмиссии — это цвета, которые объект может излучать, как если бы он содержал источник света. Таким образом, объект может светиться независимо от условий освещения. Обычно карты свечения можно увидеть на тех игровых объектах, которые светятся (например глаза робота или полосы света на контейнере). Добавьте эту текстуру (от creativesam) на наш контейнер в качестве карты свечений, так, чтобы буквы излучали свет. Решение, результат.