В реальном мире каждый объект по-разному реагирует на свет. Стальные предметы часто блестят ярче, чем, например, глиняная ваза, и деревянный ящик по-другому реагирует на свет, в отличие от стального ящика. Некоторые объекты отражают свет практически без рассеивания, что приводит к появлению маленьких зеркальных бликов, другие же рассеивают много света, придавая блику больший радиус. Для моделирования разных типов объектов в OpenGL необходимо указывать соответствующие свойства материала поверхности объекта.
Материалы
На предыдущем уроке мы определили объект и цвет света, чтобы получить визуальное представление данного объекта с учетом фоновой составляющей и интенсивности зеркального компонента освещения. При описании поверхности объекта мы можем определить цвет материала для каждого из 3 компонентов освещения: фонового, рассеянного и отраженного/зеркального. Указывая цвет для каждого из компонентов, мы, тем самым, имеем возможность досконально контролировать настройку итогового цвета поверхности. Давайте добавим к этим 3 компонентам еще один компонент — блеск. В итоге мы получим все необходимые свойства для материала:
1 2 3 4 5 6 7 8 9 |
#version 330 core struct Material { vec3 ambient; vec3 diffuse; vec3 specular; float shininess; }; uniform Material material; |
Во фрагментном шейдере мы создали структуру Material
для хранения свойств материала поверхности. Можно также хранить их в виде отдельных uniform-переменных, но способ хранения их в виде структуры делает это более организованным. Сначала мы определяем общую компоновку структуры, а затем просто объявляем uniform-переменную с только что созданной структурой в качестве её типа.
Как вы можете заметить, мы определили вектор цвета для каждого из компонентов освещения модели Фонга:
вектор ambient
определяет, какой цвет отражает поверхность при фоновом освещении (обычно он совпадает с цветом поверхности);
вектор diffuse
определяет цвет поверхности при рассеянном освещении. Рассеянный цвет (так же, как и фоновое освещение) устанавливается на желаемый цвет поверхности;
вектор specular
задает цвет зеркального блика на поверхности (или, возможно, даже отражает специфический для поверхности цвет);
переменная shininess
влияет на рассеивание/радиус зеркального блика.
С помощью данных 4 компонентов, определяющих материал объекта, мы можем смоделировать различные реальные типы материалов. В данной таблице представлен список свойств объектов, имитирующих реальные материалы. А на следующем рисунке показано влияние некоторых из этих свойств на наш куб:
Как вы можете видеть, правильное определение свойств материала поверхности влияет на наше восприятие объекта. Эффекты явно различимы, но для более реалистичных результатов нам нужно будет заменить куб на что-то посерьезнее. На следующих уроках мы обсудим загрузку моделей объектов более сложных форм.
Определение правильных значений параметров материала объекта — это трудный процесс, в основе которого лежат постоянное экспериментирование с параметрами материалов и наличие большого опыта в данной теме. Очень часто общее визуальное восприятие объекта рушится из-за неправильно подобранного значения материала.
Давайте попробуем с помощью шейдеров реализовать систему материалов объекта.
Настраиваем материалы
Мы создали uniform-структуру материала во фрагментном шейдере, поэтому далее необходимо изменить расчеты освещения, чтобы они соответствовали новым свойствам материала. Поскольку все переменные материала хранятся в структуре, то мы можем получить к ним доступ через uniform-переменную material
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void main() { // Фоновая составляющая vec3 ambient = lightColor * material.ambient; // Рассеянная составляющая vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = lightColor * (diff * material.diffuse); // Отраженная составляющая 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); } |
Как вы можете видеть, теперь мы имеем доступ ко всем свойствам структуры материала везде, где они нам нужны, и на этот раз вычисляем результирующий выходной цвет с помощью цветов материала. Каждый из атрибутов материала объекта умножается на соответствующие ему компоненты освещения.
Мы можем задать материал объекта, изменяя в самой программе соответствующую uniform-переменную. Однако переменная-структура в GLSL по отношению к uniform-переменным не обладает нужными для этого свойствами; она, можно сказать, лишь выполняет для них роль пространства имен. Если мы хотим заполнить структуру, нам придется отдельно устанавливать каждую uniform-переменную, используя в качестве префикса имя структуры:
1 2 3 4 |
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)
, то код выглядел бы так:
1 2 3 |
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)
могут индивидуально изменяться для каждого источника света, а это то, что нам и нужно. В данный момент компонент фонового освещения объекта полностью влияет на цвет куба. Но на самом деле этот компонент не должен оказывать такого большого влияния на конечный цвет, поэтому нам необходимо ограничить его цвет, установив интенсивность на более низкое значение:
1 |
vec3 ambient = vec3(0.1) * material.ambient; |
Точно так же можно влиять на интенсивность рассеянной и зеркальной составляющих источника света. Это очень похоже на то, что мы делали на предыдущем уроке; можно заметить, что мы уже создали некоторые свойства света, чтобы влиять на каждый компонент освещения по отдельности. В данный момент мы хотим создать нечто схожее со структурой материала, только для свойств света:
1 2 3 4 5 6 7 8 9 |
struct Light { vec3 position; vec3 ambient; vec3 diffuse; vec3 specular; }; uniform Light light; |
Источник света имеет различную интенсивность для своих компонентов фонового, рассеянного и отраженного освещений:
фоновый свет обычно устанавливается на низкую интенсивность, потому что мы не хотим, чтобы фоновый цвет был доминирующим;
рассеянная составляющая источника света обычно устанавливается на значение того цвета, который мы хотели бы видеть в нашем свете; часто это ярко-белый цвет;
отраженная составляющая обычно сохраняется на уровне vec3(1.0)
, светя при полной интенсивности.
Обратите внимание, что мы также добавили в структуру вектор положения света.
Так же, как и с uniform-переменной материалов, нам нужно обновить фрагментный шейдер:
1 2 3 |
vec3 ambient = light.ambient * material.ambient; vec3 diffuse = light.diffuse * (diff * material.diffuse); vec3 specular = light.specular * (spec * material.specular); |
Затем в самой программе необходимо установить интенсивность света:
1 2 3 |
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f); lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f); // немного затемним рассеянный свет lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f); |
Теперь, когда мы отрегулировали влияние света на материал объекта, получаем визуальный результат, который очень похож на результат из предыдущего урока. Однако в этот раз в нашем распоряжении имеется полный контроль над освещением и материалами объекта:
Сейчас можно относительно легко изменять визуальные аспекты объектов, поэтому давайте немного оживим наш кубик!
Различные цвета света
До сих пор мы использовали цвета света исключительно для изменения интенсивности отдельных компонентов, выбирая цвета, варьирующиеся от белого до серого и черного, не влияя при этом на фактические цвета объекта (а только на интенсивность цвета). Поскольку теперь у нас есть легкий доступ к свойствам света, мы можем менять цвета объектов с течением времени, чтобы получить действительно интересные эффекты. Поскольку во фрагментном шейдере всё уже настроено, изменение цвета света выполняется элементарно и сразу же создает некоторые забавные эффекты:
Как вы можете видеть, другой цвет света значительно влияет на конечный цвет объекта. Поскольку цвет света непосредственно влияет на то, какие цвета может отражать объект (как мы уже знаем из Урока №10. Цвета в OpenGL), он оказывает значительное влияние на визуальный результат.
Мы можем легко менять цвета света с течением времени путем изменения фонового и рассеянного цветов света с помощью функций sin() и glfwGetTime():
1 2 3 4 5 6 7 8 9 10 |
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); glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); lightingShader.setVec3("light.ambient", ambientColor); lightingShader.setVec3("light.diffuse", diffuseColor); |
Попробуйте поэкспериментировать с различными значениями переменных освещения и материала и посмотрите, как они влияют на визуальный результат.
GitHub / Урок №12. Материалы и освещение в OpenGL — Исходный код
Упражнения
Задание №1
Можете ли вы сделать так, чтобы изменение цвета света изменило и цвет куба?
Задание №2
Можете ли вы смоделировать некоторые объекты реального мира, определив материалы, из которых они созданы, как мы это рассматривали в начале данного урока? Обратите внимание, что в таблице значения фонового света и рассеянного света не совпадают; не была учтена интенсивность света. Чтобы правильно установить данные значения, вам нужно будет задать интенсивность всех составляющих компонентов света, равными vec3(1.0)
.
Ответ №2
Решение для сине-зеленого пластикового контейнера (cyan plastic):
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> #include <learnopengl/shader_m.h> #include <learnopengl/camera.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); void processInput(GLFWwindow *window); // Константы const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; // Камера Camera camera(glm::vec3(0.0f, 0.0f, 3.0f)); float lastX = SCR_WIDTH / 2.0f; float lastY = SCR_HEIGHT / 2.0f; bool firstMouse = true; // Тайминг float deltaTime = 0.0f; float lastFrame = 0.0f; // Освещение glm::vec3 lightPos(1.2f, 1.0f, 2.0f); int main() { // glfw: инициализация и конфигурирование glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Раскомментируйте данную часть кода, если используете macOS /* #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif */ // glfw: создание окна GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); // Сообщаем GLFW захватывать движения нашей мышки glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // glad: загрузка всех указателей на OpenGL-функции if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // Конфигурация глобального состояния OpenGL glEnable(GL_DEPTH_TEST); // Компилирование нашей шейдерной программы Shader lightingShader("3.2.materials.vs", "3.2.materials.fs"); Shader lightCubeShader("3.2.light_cube.vs", "3.2.light_cube.fs"); // Указывание вершин (и буферов) и настройка вершинных атрибутов float vertices[] = { -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f }; // Сначала конфигурируем VAO (и VBO) куба unsigned int VBO, cubeVAO; glGenVertexArrays(1, &cubeVAO); glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindVertexArray(cubeVAO); // Атрибут позиции glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // Атрибут нормали glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // Затем конфигурируем VAO света (VBO остается прежним; вершины такие же для объекта света, который также является 3D-кубом) unsigned int lightCubeVAO; glGenVertexArrays(1, &lightCubeVAO); glBindVertexArray(lightCubeVAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // Обратите внимание, что мы обновляем шаг позиционирования атрибута лампы, чтобы отразить обновленные данные буфера glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // Цикл рендеринга while (!glfwWindowShouldClose(window)) { // Покадровая временная логика float currentFrame = glfwGetTime(); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; // Обработка ввода processInput(window); // Рендеринг glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Не забудьте активировать шейдер при настройке uniform-переменных/рисовании объектов lightingShader.use(); lightingShader.setVec3("light.position", lightPos); lightingShader.setVec3("viewPos", camera.Position); // Свойства света lightingShader.setVec3("light.ambient", 1.0f, 1.0f, 1.0f); // обратите внимание, что все цвета света установлены на полную интенсивность lightingShader.setVec3("light.diffuse", 1.0f, 1.0f, 1.0f); lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f); // Свойства материала lightingShader.setVec3("material.ambient", 0.0f, 0.1f, 0.06f); lightingShader.setVec3("material.diffuse", 0.0f, 0.50980392f, 0.50980392f); lightingShader.setVec3("material.specular", 0.50196078f, 0.50196078f, 0.50196078f); lightingShader.setFloat("material.shininess", 32.0f); // Трансформации вида/проекции glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); glm::mat4 view = camera.GetViewMatrix(); lightingShader.setMat4("projection", projection); lightingShader.setMat4("view", view); // Трансформация мира glm::mat4 model = glm::mat4(1.0f); lightingShader.setMat4("model", model); // Рендеринг куба glBindVertexArray(cubeVAO); glDrawArrays(GL_TRIANGLES, 0, 36); // Также рисуем объект лампы lightCubeShader.use(); lightCubeShader.setMat4("projection", projection); lightCubeShader.setMat4("view", view); model = glm::mat4(1.0f); model = glm::translate(model, lightPos); model = glm::scale(model, glm::vec3(0.2f)); // меньший куб lightCubeShader.setMat4("model", model); glBindVertexArray(lightCubeVAO); glDrawArrays(GL_TRIANGLES, 0, 36); // glfw: обмен содержимым front- и back-буферов. Отслеживание событий ввода/вывода (была ли нажата/отпущена кнопка, перемещен курсор мыши и т.п.) glfwSwapBuffers(window); glfwPollEvents(); } // Опционально: освобождаем все ресурсы, как только они выполнили свое предназначение glDeleteVertexArrays(1, &cubeVAO); glDeleteVertexArrays(1, &lightCubeVAO); glDeleteBuffers(1, &VBO); // glfw: завершение, освобождение всех ранее задействованных GLFW-ресурсов glfwTerminate(); return 0; } // Обработка всех событий ввода: запрос GLFW о нажатии/отпускании кнопки мыши в данном кадре и соответствующая обработка данных событий void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) camera.ProcessKeyboard(FORWARD, deltaTime); if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) camera.ProcessKeyboard(BACKWARD, deltaTime); if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) camera.ProcessKeyboard(LEFT, deltaTime); if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) camera.ProcessKeyboard(RIGHT, deltaTime); } // glfw: всякий раз, когда изменяются размеры окна (пользователем или операционной системой), вызывается данная callback-функция void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // Убеждаемся, что окно просмотра соответствует новым размерам окна. // Обратите внимание, высота окна на Retina-дисплеях будет значительно больше, чем указано в программе glViewport(0, 0, width, height); } // glfw: всякий раз при перемещении курсора вызывается данная callback-функция void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if (firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; // выполняем реверс, поскольку y-координаты идут снизу вверх lastX = xpos; lastY = ypos; camera.ProcessMouseMovement(xoffset, yoffset); } // glfw: всякий раз при прокрутке колёсика мышки вызывается дання callback-функция void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { camera.ProcessMouseScroll(yoffset); } |
Спасибо, очень интересные статьи.
Интересно было бы посмотреть на вариант, в котором имеется несколько источников света для одного освещаемого объекта.