На предыдущих уроках мы никогда должным образом не определяли цвет объектов, а только лишь бегло касались взаимодействия с этим атрибутом. Поэтому в данной статье мы обсудим, что такое цвета в OpenGL, и начнем строить сцену для последующих уроков, в которых будет обсуждаться тема освещения.
Цвета в мире графики
В реальном мире существует бесконечное множество различных цветов и их оттенков. Но вычислительная техника не может похвастаться такими безграничными возможностями, поэтому не все реальные цвета могут быть представлены в цифровом виде. Следовательно, возникает задача: «Как описать (или закодировать) бесконечное количество различных вариантов реальных цветов с помощью ограниченного набора цифровых значений?». Ответом на данный вопрос является применение особой системы, в которой реальные цвета представляются в цифровом виде с помощью красного, зеленого и синего компонентов (сокр. «RGB» от англ. «Red, Green, Blue»). Используя различные комбинации только этих 3 значений, определенных в диапазоне [0,1]
, мы можем закодировать практически любой цвет, который только существует. Например, чтобы получить коралловый цвет, необходимо определить вектор цвета следующим образом:
1 |
glm::vec3 coral(1.0f, 0.5f, 0.31f); |
Цвет объекта, который мы видим в реальной жизни — это не тот цвет, который он действительно имеет. На самом деле, мы видим луч конкретного цвета, отраженный от объекта. Лучи различных цветов, которые не поглотились объектом, создадут именно тот цвет объекта, который мы и увидим. Например, свет солнца воспринимается нами как белый свет, но на самом деле он является совокупностью многих лучей разных цветов. Если бы мы посветили этим белым светом на синюю игрушку, то она поглотила бы все лучи, составляющие белый свет, кроме синего. А поскольку луч синего цвета игрушка не поглотила, то он отражается от нее. Отраженный луч попадает в наш глаз, и в результате мы воспринимаем нашу игрушку в виде предмета синего цвета. На следующем рисунке этот процесс показан для игрушки кораллового цвета; она отражает несколько лучей различных цветов с различной интенсивностью:
Мы видим, что белый солнечный свет — это совокупность лучей всех видимых цветов, и при этом объект поглощает большую часть из них. Оставшиеся же лучи отражаются от него. Попадая к нам в глаз, их сочетание и придает объекту тот цвет, каким мы его воспринимаем (в данном случае, коралловым).
Данные правила отражения цвета напрямую применяются в мире графики. Предположим, что мы захотели определить в OpenGL некий источник света и задать для него какой-нибудь цвет. В предыдущем абзаце использовался белый цвет, выберем его и в этот раз. Далее, умножая цвет источника света на значение цвета объекта, мы получим цвет, отраженный от объекта. Этот цвет мы и будем воспринимать, как цвет объекта. Давайте вернемся к нашей игрушке (с коралловым цветом) и посмотрим, как мы будем вычислять её воспринимаемый цвет в мире графики. Чтобы получить результирующий вектор цвета, необходимо выполнить покомпонентное умножение между вектором падающего на объект света и вектором собственного цвета объекта:
1 2 3 |
glm::vec3 lightColor(1.0f, 1.0f, 1.0f); glm::vec3 toyColor(1.0f, 0.5f, 0.31f); glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f); |
Мы видим, что итоговый цвет игрушки поглощает большую часть белого света, но при этом отражает красные, зеленые и синие цвета, и это отражение зависит от собственных значений цвета игрушки. Данный пример дает нам представление о том, как цвета будут работать в реальной жизни. Таким образом, мы можем определить понятие «цвет объекта» как количество каждого цветового компонента источника света, отраженного от объекта. А что будет, если мы поменяем цвет света у источника на зеленый?
1 2 3 |
glm::vec3 lightColor(0.0f, 1.0f, 0.0f); glm::vec3 toyColor(1.0f, 0.5f, 0.31f); glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f); |
Как мы видим, на игрушку не падают красный и синий компоненты света, а это значит, что она не сможет их поглотить и/или отразить. Игрушка поглощает половину зеленого значения света, но при этом также и отражает половину зеленого значения света. В результате итоговый воспринимаемый нами цвет игрушки будет темно-зеленоватым. Не трудно заметить, что если использовать зеленый свет, то только зеленые компоненты цвета могут быть отражены и, таким образом, восприняты; красные и синие компоненты цвета не будут восприниматься. В результате коралловый объект внезапно становится темно-зеленоватым. Ниже представлен еще один пример с темным оливково-зеленым светом источника:
1 2 3 |
glm::vec3 lightColor(0.33f, 0.42f, 0.18f); glm::vec3 toyColor(1.0f, 0.5f, 0.31f); glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f); |
Подводя небольшой итог можно отметить, что мы можем получить интересные цвета от объектов, используя различный цвет падающего света. Нужно лишь проявить немного творческого подхода к выбору цвета.
Но хватит о цветах, давайте начнем строить сцену, где мы сможем поэкспериментировать.
Освещение сцены
На следующих уроках мы будем создавать интересные визуальные эффекты, имитируя реальное освещение с широким использованием цветов. Поскольку теперь мы можем использовать источники света, то в сцене необходимо отобразить их как визуальные объекты и добавить, по крайней мере, один объект для имитации освещения.
Первое, что нам нужно — это объект, который будет отбрасывать свет, и в качестве такого предмета будет задействован уже известный нам куб-контейнер, созданный на предыдущих уроках. Нам также понадобится еще один объект, обозначающий положение источника света на 3D-сцене. Для простоты мы также представим источник света с помощью куба.
Данные вершин куба-контейнера будем использовать следующие:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
float vertices[] = { -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, }; |
Итак, заполнение объекта буфера вершин, установка указателей атрибутов вершин и другие подобные штуки — всё это должно быть вам уже знакомо, поэтому не будем снова на этом останавливаться. Если же эти моменты вызывают у вас затруднения, и вы не знаете, какие действия на этих этапах производятся, то я предлагаю вам пересмотреть предыдущие уроки и по возможности проработать упражнения, прежде чем продолжить.
Итак, приступим к делу. Первое, что нам понадобится — это вершинный шейдер для рисования ящика. Позиции вершин ящика остаются прежними (правда на этот раз текстурные координаты нам не понадобятся), поэтому и код остается прежним. Будет использоваться урезанная версия вершинного шейдера, рассмотренная на последних уроках:
1 2 3 4 5 6 7 8 9 10 11 |
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); } |
Убедитесь, что вы отредактировали данные вершин и указатели атрибутов для того, чтобы они соответствовали новому вершинному шейдеру (если хотите, то можете сохранить текстурные данные и указатели атрибутов; просто прямо сейчас мы не будем их использовать).
Поскольку мы также собираемся визуализировать куб-источник, то необходимо специально для этого источника света создать новый VAO. Можно было бы визуализировать источник света с тем же VAO, а затем выполнить несколько преобразований положения света в матрице модели, но на следующих уроках мы будем довольно часто менять данные вершин и указатели атрибутов объекта ящика, и нам не нужно, чтобы эти изменения распространялись на объект источника света (нас интересует только расположение вершин куба-источника), поэтому мы создадим новый VAO:
1 2 3 4 5 6 7 8 9 10 |
unsigned int lightVAO; glGenVertexArrays(1, &lightVAO); glBindVertexArray(lightVAO); // Нам нужно только привязаться к VBO. VBO контейнера уже содержат нужные данные glBindBuffer(GL_ARRAY_BUFFER, VBO); // Устанавливаем атрибуты вершин (только данные о местоположении нашей лампы) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); |
Вышеприведенный фрагмент кода является относительно простым. Теперь, когда мы создали и контейнер, и куб-источник света, осталось определить только одну вещь — фрагментный шейдер как для контейнера, так и для источника света:
1 2 3 4 5 6 7 8 9 10 |
#version 330 core out vec4 FragColor; uniform vec3 objectColor; uniform vec3 lightColor; void main() { FragColor = vec4(lightColor * objectColor, 1.0); } |
Фрагментный шейдер принимает цвет объекта и цвет падающего света из соответствующих uniform-переменных. Далее мы умножаем цвет падающего света на цвет объекта, как мы и обсуждали в начале данного урока. Опять же, код этого шейдера не должен вызывать у вас вопросы. Давайте установим объекту коралловый цвет из предыдущих примеров:
1 2 3 4 |
// Не забудьте сначала "использовать" соответствующую шейдерную программу (чтобы установить uniform-переменные) lightingShader.use(); lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f); lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f); |
Отметим, что когда на следующих уроках мы начнем обновлять данные шейдеры освещения, также будет затронут и куб-источник, а это нам не нужно. Мы не хотим, чтобы цвет объекта источника света влиял на расчеты освещения, скорее наоборот — нам нужно держать источник света изолированным от всех остальных объектов. Мы хотим, чтобы источник света имел постоянный яркий цвет, не подверженный другим цветовым изменениям (чтобы куб-источник действительно выглядел как источник света).
Для этого нам нужно создать второй набор шейдеров, которые мы будем использовать для отображения куба-источника, тем самым избегая влияния любых изменений в шейдерах освещения. Вершинный шейдер точно такой же, как и вершинный шейдер освещения, поэтому вы можете просто скопировать исходный код. Фрагментный шейдер куба-источника будет задавать для нашей «лампочки» постоянный белый цвет, обеспечивая эффект неизменного ярко белого света.
1 2 3 4 5 6 7 |
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0); // устанавливаем значение 1.0 для всех значений вектора } |
Когда мы захотим визуализировать объект контейнера (или, возможно, многие другие объекты), то будем это делать с помощью шейдера освещения, который мы только что определили, а когда нам понадобится отобразить источник света, то воспользуемся шейдерами источника света. Во время изучения уроков о работе с освещением, мы будем шаг за шагом обновлять соответствующие шейдеры, чтобы постепенно достичь более реалистичных результатов.
Основное назначение куба-источника — это показать, откуда собственно и исходит свет. Обычно, положение источника света определяется где-то в произвольном месте сцены, но при этом подразумевается, что от данной позиции не требуется обеспечивать визуальное восприятие конкретно самого источника света. Чтобы показать, где на самом деле находится источник, мы визуализируем куб в том же месте, где находится источник света. Визуализировать данный куб мы будем с помощью шейдера куба-источника, чтобы убедиться, что куб всегда остается белым, независимо от условий освещения сцены.
Итак, давайте объявим глобальную переменную vec3
, которая представляет местоположение источника света в координатах мирового пространства:
1 |
glm::vec3 lightPos(1.2f, 1.0f, 2.0f); |
Затем мы переводим куб-источник в положение источника света и перед визуализацией уменьшаем его масштаб:
1 2 3 |
model = glm::mat4(1.0f); model = glm::translate(model, lightPos); model = glm::scale(model, glm::vec3(0.2f)); |
Полученный код рендеринга для куба-источника должен выглядеть примерно следующим образом:
1 2 3 4 5 6 |
lightCubeShader.use(); // Задаем uniform-переменные для матриц модели, вида и проекции ... // Отрисовка куба-источника glBindVertexArray(lightCubeVAO); glDrawArrays(GL_TRIANGLES, 0, 36); |
Вставка всех фрагментов кода в соответствующие места нашего проекта приведет к созданию чистого OpenGL-приложения, правильно настроенного для экспериментов с освещением. Если всё удачно скомпилировалось, то результат должен выглядеть примерно следующим образом:
Не густо, но на следующих уроках будет интереснее.
GitHub / Урок №10. Цвета в OpenGL — Исходный код
Теперь, когда у нас есть достаточно знаний о цветах, и создана базовая сцена для экспериментов с освещением, мы можем перейти к следующему уроку, где начинается настоящая магия.
Здравствуйте, в уроке здесь взяты координаты куба вместе с текстурными координатами, но в
страйд указан как 3, а должен быть 5.
В исходном коде куб без текстурных координат, так что там все в порядке.
Спасибо за замечание, поправим 🙂