Практически всегда на наших уроках мы прибегаем к использованию 2D-текстур, но сегодня будет рассмотрен новый тип, представляющий комбинацию нескольких текстур, связанных между собой в одно целое — кубическая карта.
Кубические карты
Кубическая карта (англ. «cube map») — это текстура, содержащая 6 независимых 2D-текстур, каждая из которых представляет отдельно взятую сторону текстурированного куба. Возможно, вы зададитесь вопросом: «А в чем смысл такого куба?». Зачем беспокоиться об объединении 6 отдельных текстур в единую сущность вместо того, чтобы просто использовать 6 отдельных текстур? Дело в том, что кубические карты могут быть проиндексированы/сэмплированы с помощью вектора направления. Представим себе единичный куб размером 1×1×1, в центре которого находится начало вектора направления. Выборка значения текстуры из кубической карты с оранжевым вектором направления выглядит примерно так:
Примечание: Длина вектора направления не имеет значения. Если задано направление, то OpenGL извлекает соответствующие тексели, на которые (в конечном счете) указывает данный вектор направления, и возвращает правильно выбранное значение текстуры.
Если мы представим, что у нас есть куб, к которому мы прикрепляем такую кубическую карту, то вектор направления будет схож с вектором (интерполированных) локальных координат вершины куба. Таким образом, мы можем производить выборку значений кубической карты, используя фактические векторы положения куба, в то время как сам куб должен быть центрирован относительно точки начала координат. Таким образом, мы рассматриваем все координаты вершин в качестве его текстурных координат при выборке кубической карты. В результате, текстурная координата получает доступ к соответствующей индивидуальной текстуре грани кубической карты.
Создание кубической карты
Кубическая карта, по своей сути, является текстурой, поэтому для её создания мы определяем текстуру и, прежде чем выполнять какие-либо дальнейшие операции с ней, привязываем данную текстуру к нужному целевому типу. На этот раз привязка будет осуществляться к GL_TEXTURE_CUBE_MAP
:
1 2 3 |
unsigned int textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); |
Поскольку кубическая карта содержит 6 текстур: по одной на каждую грань, то мы должны вызвать функцию glTexImage2D() с соответствующими параметрами 6 раз. Однако на этот раз необходимо установить target-параметр текстуры так, чтобы он соответствовал определенной грани кубической карты, сообщая OpenGL для какой стороны кубической карты мы создаем текстуру. Это означает, что мы должны вызвать функцию glTexImage2D() один раз для каждой грани кубической карты.
Поскольку у нас есть 6 граней, то OpenGL предоставляет нам 6 специальных текстурных целей для нацеливания на конкретную грань кубической карты:
target-параметр текстуры | Ориентация |
GL_TEXTURE_CUBE_MAP_POSITIVE_X | Справа |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | Слева |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | Сверху |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | Снизу |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | Сзади |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | Спереди |
Данные значения перечислений представлены целочисленным типом int, и изменяются линейно, поэтому, если бы у нас был массив или вектор расположения текстур, мы могли бы перебирать их, начиная с GL_TEXTURE_CUBE_MAP_POSITIVE_X
и увеличивая значение перечисления на единицу каждую итерацию, обходя все target-объекты текстур:
1 2 3 4 5 6 7 8 9 10 |
int width, height, nrChannels; unsigned char *data; for(unsigned int i = 0; i < textures_faces.size(); i++) { data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0); glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data ); } |
В представленном выше фрагменте кода вектор textures_faces
содержит расположение всех текстур, необходимых для кубической карты, в порядке, указанном в таблице. Благодаря этому создается текстура для каждой грани текущей связанной кубической карты.
Поскольку кубическая карта является текстурой, то, как и для любой другой текстуры, необходимо определить методы её наложения и фильтрации:
1 2 3 4 5 |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); |
Не пугайтесь параметра GL_TEXTURE_WRAP_R
, он просто задает метод наложения для r-координаты текстуры, которая соответствует третьему измерению текстуры (аналог z-координаты). Мы установили метод наложения как GL_CLAMP_TO_EDGE
, так как координаты текстуры, которые находятся на границе между двумя гранями, могут не попадать точно в грань (из-за некоторых аппаратных ограничений), поэтому с помощью GL_CLAMP_TO_EDGE
OpenGL всегда возвращает значения их ребер всякий раз, когда мы производим выборку между гранями.
Затем, прежде чем рисовать объекты, которые будут использовать кубическую карту, мы активируем соответствующий текстурный юнит и до этапа рендеринга связываем кубическую карту; не такая уж и большая разница по сравнению с обычными 2D-текстурами.
Во фрагментном шейдере будет использоваться сэмплер типа samplerCube
, который мы задействуем с помощью вызова функции texture(), но на этот раз в связке с вектором направления типа vec3 вместо вектора направления типа vec2. Пример фрагментного шейдера с использованием кубической карты выглядит следующим образом:
1 2 3 4 5 6 7 |
in vec3 textureDir; // вектор направления, представляющий 3D-координату текстуры uniform samplerCube cubemap; // текстурный сэмплер кубической карты void main() { FragColor = texture(cubemap, textureDir); } |
Это всё здорово, но пока выглядит бесполезным, не так ли? Так уж получилось, что есть довольно много интересных методов, которые гораздо проще реализовать с помощью кубической карты. Одним из таких приемов является создание скайбокса.
Скайбокс
Скайбокс (англ. «skybox») — это (большой) куб, который охватывает всю сцену и содержит 6 изображений окружения, создавая у игрока иллюзию, что окружающая среда, в которой он находится, имеет гораздо большие размеры, нежели на самом деле. Некоторые примеры скайбоксов, используемых в видеоиграх — это изображения гор, облаков или звездного ночного неба. Пример скайбокса с изображением звездного ночного неба можно увидеть на следующем скриншоте из игры Elder Scrolls III:
Вы, наверное, уже догадались, что скайбоксы, подобные этому, идеально подходят для кубических карт: у нас есть куб, который имеет 6 граней и каждая его грань должна быть затекстурирована. На предыдущей картинке использовались несколько изображений ночного неба для того, чтобы создать у игрока иллюзию, будто он находится в каком-то большом мире, в то время как на самом деле он находится внутри крошечной коробочки.
Обычно в Интернете достаточно много ресурсов, располагающих подобными скайбоксами. Данные изображения скайбоксов, как правило, имеют следующий шаблон:
Если вы соответствующим образом загнете все 6 сторон, то получите полностью текстурированный куб, имитирующий обширный пейзаж. Некоторые ресурсы предоставляют скайбоксы именно в таком формате, и в данном случае вам придется вручную извлекать все 6 изображений граней, но чаще всего изображения скайбоксов предоставляются в виде 6 отдельных текстур.
Данный (высококачественный) скайбокс мы и будем использовать для нашей сцены, скачать его можно здесь.
Загрузка скайбокса
Поскольку скайбокс сам по себе является обычной кубической картой, то его загрузка не сильно отличается от того, что мы уже проходили в начале этого урока. Для загрузки скайбокса мы будем использовать следующую функцию, которая принимает вектор из 6 текстурных локаций:
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 |
unsigned int loadCubemap(vector<std::string> faces) { unsigned int textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); int width, height, nrChannels; for (unsigned int i = 0; i < faces.size(); i++) { unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data ); stbi_image_free(data); } else { std::cout << "Cubemap tex failed to load at path: " << faces[i] << std::endl; stbi_image_free(data); } } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); return textureID; } |
Сама по себе функция загрузки не должна удивить вас. По сути, это код кубической карты, который мы видели в предыдущем разделе, но объединенный в единую управляемую функцию.
Теперь, прежде чем мы вызовем данную функцию, мы загрузим в вектор соответствующие пути текстуры в порядке, указанном перечислениями кубической карты:
1 2 3 4 5 6 7 8 9 10 |
vector<std::string> faces; { "right.jpg", "left.jpg", "top.jpg", "bottom.jpg", "front.jpg", "back.jpg" }; unsigned int cubemapTexture = loadCubemap(faces); |
После этого, мы загружаем скайбокс в виде кубической карты с идентификатором cubemapTexture
. Теперь можно привязать его к кубу, чтобы заменить тот убогий цвет, который мы использовали всё это время.
Отображение скайбокса
Поскольку скайбокс отрисовывается на кубе, то нам понадобятся: еще один VAO, VBO и новый набор вершин. Вы можете скопировать вершинные данные отсюда:
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 skyboxVertices[] = { -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f }; |
Данные из кубической карты, задействованной для текстурирования 3D-куба, могут быть получены с использованием локальных координат куба в качестве его текстурных координат. Когда куб центрирован в начале координат (0,0,0)
, то каждый из его векторов положения также является вектором направления от начала координат. Данный вектор направления — это именно то, что нам нужно для получения соответствующего значения текстуры в конкретной координате куба. По этой причине мы будем работать только с векторами положения, а не с текстурными координатами.
Для рендеринга скайбокса нам понадобится новый набор шейдеров, но сложного в нем ничего не будет. Поскольку у нас есть только один атрибут вершины, то реализация вершинного шейдера принимает следующий вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#version 330 core layout (location = 0) in vec3 aPos; out vec3 TexCoords; uniform mat4 projection; uniform mat4 view; void main() { TexCoords = aPos; gl_Position = projection * view * vec4(aPos, 1.0); } |
Ключевым моментом данного вершинного шейдера является то, что мы устанавливаем входящий локальный вектор положения в качестве исходящих координат текстуры для использования во фрагментном шейдере. Затем фрагментный шейдер принимает эти координаты в качестве входных данных для сэмплера samplerCube
:
1 2 3 4 5 6 7 8 9 10 11 |
#version 330 core out vec4 FragColor; in vec3 TexCoords; uniform samplerCube skybox; void main() { FragColor = texture(skybox, TexCoords); } |
Фрагментный шейдер также относительно прост. В качестве вектора направления текстуры мы берем интерполированный вектор положения атрибута вершины и используем его для выборки значений текстуры из кубической карты.
Теперь нам не составит труда произвести рендеринг скайбокса, т.к. у нас есть текстуры кубической карты. Нам просто нужно выполнить привязку текстуры кубической карты, и сэмплер skybox
автоматически заполнится кубической картой скайбокса. Для отрисовки скайбокса необходимо отобразить его в качестве первого объекта сцены и отключить запись глубины. Таким образом, скайбокс всегда будет нарисован на фоне всех других объектов, так как единичный куб, скорее всего, имеет меньшие размеры, чем остальная часть сцены:
1 2 3 4 5 6 7 8 |
glDepthMask(GL_FALSE); skyboxShader.use(); // ...установка матриц вида и проекции glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glDepthMask(GL_TRUE); // ...отрисовка остальной части сцены |
Если вы запустите данный код, то столкнетесь с небольшими трудностями. Нам нужно, чтобы скайбокс был сосредоточен вокруг игрока так, что, независимо от того, насколько далеко игрок движется, скайбокс не должен к нему приближаться, создавая впечатление чрезвычайно больших размеров окружающей среды. Однако текущая матрица представления преобразует все позиции скайбокса, вращая, масштабируя и транслируя их так, что при перемещении игрока кубическая карта также будет перемещаться! Нам нужно удалить часть матрицы вида, отвечающую за трансляцию объекта, чтобы только операция вращения влияла на векторы положения скайбокса.
Возможно, вы помните из урока о базовом освещении в OpenGL, что мы можем удалить секцию трансляции матриц преобразования, использовав верхнюю левую матрицу размера 3×3 матрицы 4×4. Мы можем достичь этого, преобразовав матрицу представления в матрицу 3×3 (удалив фрагмент, отвечающий за трансляцию) и затем преобразовав её обратно в матрицу 4×4:
1 |
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); |
Благодаря этому мы убираем любые преобразования перемещения, но сохраняем все преобразования поворота так, что пользователь все еще может смотреть вокруг сцены.
В результате получается сцена, размеры которой, благодаря нашему скайбоксу, выглядят огромными. Если бы мы облетели контейнер, то сразу же получили бы ощущение масштаба, что значительно увеличивает реалистичность сцены. Посмотрите на следующее изображение:
Попробуйте поэкспериментировать с различными скайбоксами и посмотреть, какое большое влияние они могут оказывать на внешний вид вашей сцены.
Оптимизация
Сначала происходит визуализация скайбокса, а затем уже визуализация всех остальных объектов сцены. Описываемый метод прекрасно справляется со своей задачей, но при этом он является не слишком эффективным. Если мы сначала визуализируем скайбокс, то для каждого пикселя на экране происходит запуск фрагментного шейдера, даже если в конечном итоге будет видна только небольшая часть скайбокса; фрагменты, которые можно было бы легко отбросить с помощью предварительного теста глубины, сэкономили бы нам ценную пропускную способность.
Поэтому, чтобы получить небольшое повышение производительности, мы собираемся выполнить отрисовку скайбокса последним. Таким образом, буфер глубины будет полностью заполняться всеми значениями глубины сцены, а нам только и остается, что визуализировать те фрагменты скайбокса, которые прошли предварительный тест глубины, что значительно сокращает количество вызовов фрагментного шейдера. Проблема в том, что скайбокс, скорее всего, не будет визуализироваться, так как это всего лишь куб размером 1×1×1, и он навряд ли пройдет все тесты глубины. Стандартный рендеринг данного куба без учета тестирования глубины — не наше решение, так как в результате этого скайбокс перезапишет все остальные объекты сцены. Нам нужно обмануть буфер глубины, чтобы он поверил, будто скайбокс имеет максимальное значение глубины, равное 1.0
, в результате чего тест глубины выдаст отрицательный результат везде, где перед скайбоксом будет находиться другой объект.
На уроке о системах координат в OpenGL мы говорили, что перспективное деление выполняется после запуска вершинного шейдера, производя деление x-, y-, z-координат переменной gl_Position
на её же w-компоненту. Также из урока о тестировании глубины в OpenGL мы знаем, что z-компонента результата деления равна значению глубины выбранной вершины. Используя эту информацию, мы можем установить z-компоненту выходной координаты равной её w-компоненте, что приведет к z-компонентe, значение которой всегда равно 1.0
, потому что при применении перспективного деления z-компонента преобразуется в w/w = 1.0
:
1 2 3 4 5 6 |
void main() { TexCoords = aPos; vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; } |
В результате, нормализованные координаты устройства всегда будут иметь z-значение, равное 1.0
(т.е. максимальное значение глубины). Таким образом, скайбокс будет визуализироваться только там, где нет видимых объектов (только в этом случае он пройдет тест глубины).
Нужно немного изменить функцию теста глубины, установив её в GL_LEQUAL
вместо заданной по умолчанию GL_LESS
. Буфер глубины будет заполнен значениями 1.0
скайбокса, поэтому нам нужно убедиться, что скайбокс проходит тесты глубины со значениями меньше или равными буферу глубины, а не строго меньше.
GitHub / Урок №24. Кубические карты в OpenGL — Исходный код №1
Отображение окружения
Теперь у нас есть готовое окружение, представленное одним текстурным объектом, и мы можем применить эту информацию не только для скайбокса. Используя кубическую карту с окружающей средой, мы могли бы придать объектам свойства отражения или преломления. Методы, использующие подобную кубическую карту окружения, называются методами отображения окружения, и наиболее популярными из них являются отражение и преломление.
Отражение
Отражение — это свойство объекта (или части объекта) отражать окружающую его среду (например, цвета объекта более или менее равны его окружению, в зависимости от угла зрения зрителя). Зеркало является отражающим объектом: оно отражает свое окружение, в зависимости от угла зрения зрителя.
Основы отражения не так уж и сложны. На следующем рисунке показано, как мы можем определить направление вектора отражения и использовать его для выборки значений из кубической карты:
Мы вычисляем вектор отражения R относительно вектора нормали N и вектора направления взгляда I. Мы можем посчитать данный вектор отражения, применив встроенную GLSL-функцию reflect(). Затем полученный вектор R используется в качестве вектора направления для индексирования/выборки кубической карты, возвращая значение цвета окружающей среды. В результате получается видимость того, что объект отражает скайбокс.
Поскольку у нас уже есть код настройки скайбокса сцены, то создать отражение будет довольно легко. Мы изменим фрагментный шейдер, используемый контейнером, чтобы придать контейнеру свойства отражения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#version 330 core out vec4 FragColor; in vec3 Normal; in vec3 Position; uniform vec3 cameraPos; uniform samplerCube skybox; void main() { vec3 I = normalize(Position - cameraPos); vec3 R = reflect(I, normalize(Normal)); FragColor = vec4(texture(skybox, R).rgb, 1.0); } |
Сначала мы вычисляем вектор I направления вида/камеры и берем его для вычисления вектора отражения R, который затем используем для выборки из кубической карты скайбокса. Обратите внимание, что у нас снова присутствуют переменные Normal
и Position
фрагмента, поэтому нам также нужно будет настроить вершинный шейдер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; out vec3 Normal; out vec3 Position; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { Normal = mat3(transpose(inverse(model))) * aNormal; Position = vec3(model * vec4(aPos, 1.0)); gl_Position = projection * view * vec4(Position, 1.0); } |
Так как используются векторы нормалей, то необходимо снова преобразовать их с помощью нормальной матрицы. Выходной вектор Position
— это вектор положения в мировом пространстве. Данный выходной вектор Position
вершинного шейдера используется для вычисления вектора направления вида во фрагментном шейдере.
Поскольку мы работаем с нормалями, то потребуется обновить вершинные данные:
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.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 }; |
А также необходимо обновить указатели атрибутов. Не забудьте установить uniform-переменную cameraPos
. Затем, перед рендерингом контейнера, нужно выполнить привязку текстуры кубической карты:
1 2 3 |
glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); |
Компиляция и запуск кода даст нам изображение контейнера, который ведет себя как идеальное зеркало. Окружающий скайбокс прекрасно отражается в нем:
GitHub / Урок №24. Кубические карты в OpenGL — Исходный код №2
Когда отражение применяется ко всему объекту (например, к контейнеру), то объект выглядит так, словно он состоит из материала с высокой отражающей способностью, такой как у стали или хрома. Если бы мы загрузили более интересную модель (например, модель рюкзака из урока о загрузке моделей в OpenGL), то получили бы эффект внешнего вида объекта, полностью сделанного из хрома:
Выглядит это очень круто, но на самом деле большинство моделей не являются полностью отражающими. Мы могли бы, например, ввести карты отражений, которые придадут объектам дополнительный уровень детализации. Точно так же, как диффузные и зеркальные карты, карты отражений — это текстурные изображения, которые мы можем использовать для определения отражательной способности фрагмента. Используя эти карты отражений, мы можем определить, какие части модели показывают отражение и какой интенсивности.
Преломление
Другая форма отображения окружающей среды называется преломлением и похожа на отражение. Преломление — это изменение направления света вследствие изменения типа материала, через который проходит свет. Преломление — это то, что мы обычно видим на поверхности, похожей на воду, где свет не идет четко по прямой, а немного отклоняется от направления взгляда. Это все равно, что смотреть на свою руку, когда она наполовину в воде.
Преломление описывается Законом Снеллиуса, который, с использованием карт окружающей среды, выглядит следующим образом:
И снова, у нас есть вектор направления взгляда I, вектор нормали N и, на этот раз, результирующий вектор преломления R. Как видите, направление вектора взгляда (в точке соприкосновения двух разных материалов) слегка отклонено от начального направления. Затем данный результирующий отклоненный вектор R используется для выборки значений кубической карты.
Преломление довольно легко реализовать с помощью встроенной GLSL-функции refract(), которая в качестве параметров принимает вектор нормали, вектор направления взгляда и коэффициенты преломления обоих материалов.
Коэффициент преломления определяет величину искажения/отклонения света в материале. Список наиболее распространенных коэффициентов преломления указан в следующей таблице:
Материал | Коэффициент преломления |
Воздух | 1.00 |
Вода | 1.33 |
Лёд | 1.309 |
Стекло | 1.52 |
Алмаз | 2.42 |
Мы используем коэффициенты преломления для расчета соотношения между обоими материалами, через которые проходит свет. В нашем случае, луч света/зрения идет от воздуха к стеклу (если предположить, что объект сделан из стекла), так что соотношение становится равным 1.00/1.52=0.658
.
Мы уже привязали кубическую карту, снабдили вершинные данные нормалями и задали положение камеры uniform-переменной. Единственное, что осталось сделать — это изменить фрагментный шейдер:
1 2 3 4 5 6 7 |
void main() { float ratio = 1.00 / 1.52; vec3 I = normalize(Position - cameraPos); vec3 R = refract(I, normalize(Normal), ratio); FragColor = vec4(texture(skybox, R).rgb, 1.0); } |
Меняя показатели преломления, можно получить совершенно другие визуальные результаты. Компиляция и запуск приложения с использованием объекта контейнера не так интересна, потому что не показывает ярко выраженный эффект преломления, а лишь действует как увеличительное стекло. Однако, использование тех же шейдеров, но применительно к загруженной 3D-модели, дает нам тот эффект, который мы ожидаем: стеклоподобный объект.
Вы можете себе представить, что при правильном сочетании освещения, отражения, преломления и движения вершин можно создать довольно аккуратную трехмерную графику воды. Обратите внимание, что для получения физически точных результатов мы должны снова преломлять свет, когда он покидает объект; а пока мы просто использовали одностороннее преломление, которое отлично подходит для большинства целей.
Динамические карты окружения
В данный момент в качестве скайбокса мы задействуем статическую комбинацию изображений, которая выглядит великолепно, но не включает в себя фактическую 3D-сцену с движущимися объектами. До сих пор мы этого не замечали, потому что использовали только один предмет. Если бы у нас были зеркальные объекты с множеством окружающих объектов, то в зеркале был бы виден только скайбокс, как если бы он был единственным объектом в сцене.
Используя фреймбуферы, можно создать текстуру сцены для всех 6 различных углов от рассматриваемого объекта и сохранить их в кубической карте каждого кадра. Затем мы можем использовать эту (динамически генерируемую) кубическую карту для создания реалистичных отражающих и преломляющих поверхностей, включающих все остальные объекты. Это называется динамическим отображением окружения, потому что мы динамически создаем кубическую карту окружения объекта и используем её в качестве карты окружения.
Хотя это выглядит великолепно, но есть один огромный недостаток: мы должны визуализировать сцену 6 раз на объект, используя карту окружающей среды, что является огромной нагрузкой на производительность нашего приложения. Современные приложения стараются использовать скайбокс там, где это возможно, и предварительно визуализировать кубические карты везде, где это допускается, для того, чтобы иметь возможность сортировать/создавать динамические карты окружающей среды. Хотя динамическое отображение среды является отличным методом, но он требует много хитроумных трюков и хаков, чтобы заставить его работать в реальном приложении рендеринга без слишком большого падения производительности.
Для наглядности понимания того что по сути зеркальный(прозрачный) куб отражает() только скайбокс добавил в сцену обычный куб. Результат https://youtu.be/s-J2ZrttwSM