OGL3

Кубические карты


До сих пор нам приходилось пользоваться лишь двухмерными текстурами, однако, OpenGL поддерживает гораздо больше типов текстур. И в этом уроке мы рассмотрим тип текстурной карты, на самом деле, представляющий собой комбинацию нескольких отдельных текстур – это кубическая карта (cubemap).

Кубическая карта, по сути, является одним текстурным объектом, содержащим 6 отдельных двухмерных текстур, каждая из которых соотносится со стороной оттекстурированного куба. Зачем может пригодиться такой куб? Зачем сшивать шесть отдельных текстур в одну карту вместо использования отдельных текстурных объектов? Суть в том, что выборки из кубической карты можно совершать используя вектор направления.


Представьте себе единичный куб (куб со сторонами 1х1х1 единиц) из центра которого исходит вектор направления. Текстурная выборка из кубической карты с вот таким вот оранжевым вектором направления внутри выглядела бы так:

Длина вектора направления не важна. OpenGL достаточно знать направление для проведения корректной итоговой выборки из текстуры.

Если представить себе фигуру куба на которую натянута кубическая карта, то выходит, что вектор направления, указывающий область выборки, выглядит схожим образом с интерполированными координатами вершин куба. С учетом этого мы можем делать выборки из карты используя данные координат куба, необходимо лишь чтобы сам куб оставался расположенным симметрично относительно начала координат. В таком случае текстурные координаты вершин куба можно взять равными векторам положений вершин куба. В результате текстурные координаты корректно ссылаются на текстуры отдельных граней куба.

Создание кубической карты


Кубическая карта, как и любой другой текстурный объект, создается по уже знакомым правилам: создаем непосредственно объект текстуры и привязываем к подходящей текстурной цели (texture target) перед тем как выполнять какие-либо действия над текстурой. В нашем случае точкой привязки будет GL_TEXTURE_CUBE_MAP:

unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

Поскольку кубическая картой является комбинация из шести отдельных текстурных образов, то нам придется сделать шесть вызовов glTexImage2D с набором параметров, схожим с теми, что применялись в предыдущих уроках. В этот раз, однако, в каждом вызове значение параметра текстурной цели будет принимать одно из специальных значений, связанных с каждой гранью кубической карты. По сути, мы сообщаем OpenGL для какой из граней создаем текстурный образ. И так шесть раз: по одному glTexImage2D на каждую грань.

Поскольку граней всего шесть, то в OpenGL задано шесть специальных текстурных целей, конкретно отвечающих за создание текстуры для всех граней кубической карты:


Нередко элементы перечислений (enum) OpenGL имеют числовые значения, изменяющиеся линейно. В случае с перечисленными элементами это также верно. Это позволит нам легко проинициализировать текстуры для всех граней, используя простой цикл, начинающийся с текстурной цели GL_TEXTURE_CUBE_MAP_POSITIVE_X и с каждым проходом просто увеличивающий значение цели на 1:

int width, height, nrChannels;
unsigned char *data;  
for(GLuint 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
    );
}

Здесь мы используем вектор с именем textured_faces, содержащий пути ко всем необходимым для задания кубической карты файлам текстур в порядке, соответствующем порядку идентификаторов граней из таблицы. Таким образом будут созданы текстуры для каждой из граней текущей привязанной кубической карты.

Текстурный объект кубической карты обладает все теми же свойствами, что и любая текстура, так что не помешает настроить режимы текстурной фильтрации и повторения:

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 – он всего лишь настраивает режим повторения по третьей координате текстуры (а-ля координата z для положений). Применяем режим повторения GL_CLAMP_TO_EDGE, поскольку текстурные координаты, оказавшиеся точно между двумя гранями могут привести к отсутствию корректной выборки (из-за некоторых аппаратных ограничений). Такой выбранный режим повторения позволяет возвращать значение с границы текстуры, даже в случаях выборок между гранями карты.

Перед рендером объектов, использующих кубическую карту мы должны указать используемый текстурный блок и привязать кубическую карту. Все ровно также, как и в случаях с 2D текстурами.

В коде фрагментного шейдера придется сменить тип семплера на samplerCube, поскольку в функцию texture мы будем передавать трехмерный вектор текстурных координат вместо двухмерного. Пример кода шейдера, использующего кубическую карту ниже:

in vec3 textureDir; // вектор направления, таже представляющий трехмерную текстурную координату
uniform samplerCube cubemap; // сэмплер для кубической карты

void main()
{             
    FragColor = texture(cubemap, textureDir);
}  

Что ж, все это занимательно, но пока кажется бесполезным. Не спешите с выводами, некоторые замечательные эффекты очень просто реализуются с использованием кубических карт. Один из них – создание скайбокса.

Скайбокс


Скайбокс – это огромный куб, заключающий в себе всю текущую сцену, оттекстурированный шестью изображениями окружающей сцену обстановки и создающий для игрока иллюзию того, что сцена, в которой он находится гораздо больше, чем есть на самом деле. Обычно скайбоксы в играх реализуются используя изображения гор, облачного или ночного неба – все это, думается, вам знакомо. Вот, например, скайбокс с текстурой ночного звездного неба из третей части TES:


Думаю, вы уже догадались, что кубические карты просто сами просятся быть использованными здесь: у нас есть куб с шестью гранями и необходимостью корректно его оттекстурировать в соответствии с гранью. На предыдущем скриншоте всего несколько изображений ночного неба дали игроку эффект нахождения в огромной вселенной, хотя на самом деле – он узник крошечного кубика.

В сети хватает ресурсов, где можно найти подготовленные для скайбоксов наборы текстур. Например, здесь и здесь их хватит на многие эксперименты. Нередко, выложенные в сети текстуры скайбоксов следуют следующему предсталению:


Если сложить изображение по границам (загибая края «за экран»), то выйдет полностью оттукстурированный кубик, внутри которого создается ощущение большого пространства. Некоторые ресурсы следуют такой раскладке текстуры, так что вам потребуется самим вырезать шесть текстур для отдельных граней. Но, в большинстве случаев их выкладывают набором из шести отдельных изображений.

Здесь в хорошем разрешении можно скачать текстуру для скайбокса, которую будем использовать в уроке.

Загрузка скайбокса


Поскольку скайбокс представляется кубической картой, то процесс загрузки не сильно отличается от того, что было уже упомянуто в уроке. Для загрузки используется следующая функция, принимающая вектор с шестью путями к файлам текстур:

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 texture 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;
}

Код функции не должен стать сюрпризом. Это все тот же код работы с кубической картой, что был показан в предыдущем разделе, но собранный воедино в одной функции.

vector<std::string> faces;
{
    "right.jpg",
    "left.jpg",
    "top.jpg",
    "bottom.jpg",
    "front.jpg",
    "back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);

Перед вызовом самой функции необходимо заполнить вектор с путями к файлам текстур, причем в порядке, соответствующем порядку описаний элементов перечисления OpenGL, описывающем порядок граней кубической карты.

Отображение скайбокса


Поскольку без объекта куба нам не обойтись, то понадобятся новые VAO, VBO и массив вершин, который можно скачать здесь.

Выборки из кубической карты, используемой для покрытия трехмерного кубика, можно делать с помощью координат на самом кубе. Когда куб симметричен относительно начала координат, то каждая из координат его вершин также задает и вектор направления из начала координат. И этот вектор направления как раз и используется для выборки из текстуры в точке, соответствующей вершине куба. В итоге нам даже не придется передавать в шейдер текстурных координат!
Для рендера понадобится новый набор шейдеров, довольно простых. Вершинный шейдер тривиален, используется всего один вершинный атрибут, без текстурных координат:

#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:

#version 330 core
out vec4 FragColor;

in vec3 TexCoords;

uniform samplerCube skybox;

void main()
{    
    FragColor = texture(skybox, TexCoords);
}

И этот шейдер довольно прост. Берем переданные координаты вершин куба как текстурные координаты и делаем выборку из кубической карты.

Рендер также не содержит ничего хитрого: привязываем кубическую карту как текущую текстуру и семплер skybox немедля получает необходимые данные. Вывод скайбокса мы будем производить первым из всех объектов сцены, также отключим тест глубины. Это гарантирует то, что скайбокс всегда окажется на фоне остальных объектов.

glDepthMask(GL_FALSE);
skyboxShader.use();
// ... задание видовой и проекционной матриц
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... вывод остальной сцены

Если вы попытаетесь запустить программу прямо сейчас, то столкнетесь со следующей проблемой. По идее, нам бы хотелось, чтобы объект скайбокса оставался расположен симметрично относительно позиции игрока, как бы далеко тот не передвигался – только там можно создать иллюзию того, что окружение, изображенное на скайбоксе действительно огромное. Но видовая матрица, которую мы используем применяет полный набор преобразований: поворот, масштабирование и перемещение. Так что если игрок двигается, скайбокс также перемещается! Чтобы действия игрока не влияли на скайбокс, нам следовало бы удалить информацию о перемещении из видовой матрицы.

Возможно вы вспомните вводный урок по освещению и тот факт, что мы смогли избавиться от информации о перемещении в матрицах размером 4х4 просто извлечением подматрицы 3х3. Можно повторить этот трюк простым приведением размерности видовой матрицы сначала к 3х3, затем обратно к 4х4:

glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));  

Данные о смещении будут убраны, но данные остальных трансформаций сохранятся, что позволит игроку нормально смотреть по сторонам.

В итоге получаем картину, передающую ощущение большого пространства за счет скайбокса. Полетав рядом с контейнером вы почувствуете это ощущение масштаба, что весьма добавляет реализма всей сцене:


Попробуйте применить другие подборки текстур и оцените тот эффект, что они оказывают на ощущения от сцены.

Оптимизация рендера скайбокса


Как было отмечено, скайбокс рендерится перед всеми остальными объектами сцены. Работает на ура, но не слишком экономно по ресурсам. Посудите сами: при выводе наперед нам придется выполнить фрагментный шейдер для всех пикселей экрана, хотя в итоговой сцене видимыми останутся лишь некоторые из них, а большая часть могла бы быть отброшена с помощью раннего теста глубины, сэкономив нам ценные ресурсы.

Так что из соображений эффективности стоит выводить скайбокс последним. В этом случае буфер глубины будет заполнен значениями глубин всех объектов сцены, а скайбокс будет выводится только там, где ранний тест глубины прошел успешно, экономя нам вызовы фрагментного шейдера. Но есть загвоздка: скорее всего в кадр вообще ничего не попадет, поскольку скайбокс – это всего лишь кубик 1х1х1 и большую часть тестов глубины он провалит. Отключить перед рендером тест глубины также нельзя, ведь скайбокс просто будет выведен поверх сцены. Нужно заставить буфер глубины поверить в то, что глубина всего скайбокса равна 1, чтобы тест глубины проваливался в случаях, если перед скайбоксом есть другой объект.

В уроке о системах координат мы упоминали, что перспективное деление производится после выполнения вершинного шейдера и производит деление компонент xyz вектора gl_Positions на величину компоненты w. Также, из урока по тесту глубины мы знаем, что компонента z, полученная после деления равна глубине данной вершины. Используя эту информацию мы можем установить компоненту z вектора gl_Positions равной компоненте w, что после перспективного деления даст нам значение глубины всюду равное 1:

void main()
{
    TexCoords = aPos;
    vec4 pos = projection * view * vec4(aPos, 1.0);
    gl_Position = pos.xyww;
} 

В итоге в нормализованных координатах устройства вершина всегда будет иметь компоненту z = 1, что есть максимальное значение в буфере глубины. А скайбокс будет выводится только в областях экрана, где нет других объектов (только здесь тест глубины будет пройден, поскольку в других точках скайбокс что-то перекрывает).

Однако, нам понадобится изменить тип функции глубины на GL_LEQUAL вместо стандартного GL_LESS. Очищенный буфер глубины по умолчанию заполнен значением 1, в итоге, нужно обеспечить условия, чтобы скайбокс успешно прошел тест для значений глубины не строго меньших, чем хранящие в буфере, а меньше либо равных.

Код с применением этой оптимизации можно найти здесь.

Отображение окружения


Итак, кубическая карта содержит информацию об окружении сцены, спроецированную в один текстурный объект. Такой объект может пригодиться не только для изготовления скайбоксов. Используя кубическую карту с текстурой окружения можно сымитировать отражающие или преломляющие свойства у объектов. Такие техники, использующие кубические карты, называются техниками отображения окружения и самые широко известные из них: имитация отражения и преломления.

Отражение


Отражение – свойство объекта отображать окружающее пространство, т.е. в зависимости от угла наблюдения объект приобретает цвета в большей или меньшей степени идентичные таковым окружающей обстановки. Зеркало – идеальный пример: отражает окружение в зависимости от направления взгляда наблюдателя.

Основы физики отражений не слишком сложны. На следующем изображении показано как найти вектор отражения и использовать его для выборки из кубической карты:


Вектор отражения $\color{green}{\bar{R}}$ находится на основе направления взгляда $\color{gray}{\bar{I}}$ относительно вектора нормали $\color{red}{\bar{N}}$. Непосредственный расчет можно провести с помощью встроенной функции GLSL reflect(). Получившийся вектор $\color{green}{\bar{R}}$ затем можно использовать как вектор направления для выборки из кубической карты значения отраженного цвета. В итоге объект будет выглядеть так, будто отражает окружающий его скайбокс.

Поскольку в нашей сцене скайбокс уже подготовлен, то создание отражений не сильно сложное. Немного изменим фрагментный шейдер, используемый контейнером, для придания ему отражающих свойств:

#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, так что придется уточнить и вершинный шейдер:

#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 * model * vec4(aPos, 1.0);
} 

Т.к. нормали снова используются, то их трансформация требует возврата использования матрицы нормалей. Вектор Position содержит мировые координаты вершины. Он будет использован во фрагментном шейдере для вычисления вектора направления взгляда.

Из-за наличия нормалей придется обновить и используемый массив данных вершин, а также обновить код, связанный с настройкой указателей атрибутов вершин. Также не забудьте устанавливать значение юниформа cameraPos в каждой итерации рендера.

Перед самым выводом контейнера следует привязать объект кубической карты:

glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);  		
glDrawArrays(GL_TRIANGLES, 0, 36);	

В итоге выполнения программы получим куб, ведущий как идеальное зеркало, отражающее все окружение.


Исходный код находится здесь.

Когда всему объекту назначен подобный спецэффект он выглядит как выполненный из сильно отражающего материала как полированная сталь или хром. Если загрузить модель нанокостюма из урока по загрузке моделей, то весь костюм будет выглядеть цельнометаллическим:


Выглядит потрясно, но в реальности редко какой объект целиком зеркален. Но можно ввести в использование карты отражения, которые добавят моделям еще немного детальности. Как и диффузные карты и карты зеркального блеска, этот вид карт представляет собой обычную текстуру, выборка из которой определяет степень зеркального отражения для фрагмента. С их помощью можно задать области модели, обладающие зеркальным отражением и его интенсивность. В упражнениях к этому уроку вам будет предложена задача добавления карты отражения в загрузчик моделей, разработанный ранее.

Преломление


Другая форма отображения окружения – преломление, которое некоторым образом схоже с отражением. Преломление представляет собой изменение направления движения луча света, вызванное его переходом через границу раздела двух сред. Именно преломление дает эффект изменения направления света в жидких средах, которое легко заметно если погрузить в воду половину руки.

Преломление описывается законом Снелля, в сочетании с кубической картой что выглядит следующим обазом:


Снова у нас задан вектор наблюдения $\color{gray}{\bar{I}}$, вектор нормали $\color{red}{\bar{N}}$, и итоговый вектор преломления $\color{green}{\bar{R}}$. Как видно, направление вектора взгляда искажается вследствие преломления. И именно измененный вектор $\color{green}{\bar{R}}$ будет использоваться для выборки из кубической карты.

Вектор преломления легко рассчитать с использованием еще одной встроенной функции GLSL – refract(), которая принимает вектор нормали, вектор направления взгляда и отношение коэффициентов преломления граничащих материалов.

Коэффициент преломления определяет степень отклонения направления луча света, следующего сквозь материал. Каждому веществу соответствует свой коэффициент, здесь показаны несколько примеров:


Данные из таблицы используются для расчета отношения между коэффициентами преломления материалов, через которые проходит свет. В нашем случае луч проходит из воздуха в стекло (условимся, что наш контейнер – стеклянный), значит отношение равно 1/1.52 = 0.658.

Кубическая карта у нас уже привязана, данные вершин, включая нормали, загружены, а положение камеры передано в соответствующий юниформ. Остается только изменить фрагментный шейдер:

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);
}  

Изменяя значения коэффициентов преломления можно создавать совершенно разные визуальные эффекты. При запуске приложения нас, увы, не ждет особо впечатляющая картинка, поскольку стеклянный куб не слишком сильно подчеркивает эффект преломления и просто выглядит как кубическое увеличительно стекло. Другое дело, если использовать модель нанокостюма – сразу становится понятно, что объект сделан из вещества подобного стеклу:


Можно представить, что с помощью виртуозного применения освещения, отражения, преломления и трансформации вершин можно добиться весьма впечатляющей симуляции водной поверхности.

Стоит, однако, отметить, что для физически достоверных результатов нам потребуется сымитировать второе преломление при выходе луча с обратной стороны объекта. Но для простых сцен даже упрощенное преломление с учетом всего одной стороны объекта вполне достаточно.

Изменяющиеся карты окружения


В этом уроке мы научились использовать комбинацию нескольких изображений для создания скайбокса, что хоть и выглядит замечательно, но не учитывает возможного наличия в сцене перемещающихся объектов. Сейчас это незаметно, поскольку в сцене находится единственный объект. Но при наличии в сцене отражающего объекта, окруженного другими объектами, мы бы увидели только отражение скайбокса, будто остальных объектов и нет в помине.

Используя внеэкранные буферы кадра возможно создать новую кубическую карту сцены из точки стояния отражающего объекта, проведя рендер сцены в шести направлениях. Такая динамически создаваемая кубическая карта могла бы использоваться для создания реалистичных отражающих или преломляющих поверхностей, в которых видны и другие объекты. Эта техника называется динамическим отображением окружения, поскольку мы подготавливаем новые кубические карты, включающие все окружение, прямо на лету, в процессе рендера.

Подход замечательный, но с существенным недостатком – необходимостью шестикратного рендера всей сцены для каждого объекта, использующего кубическую карту окружения. Это весьма ощутимый удар по производительности любого приложения. Современные приложения стараются максимально использовать статичные скайбоксы и просчитанные заранее кубические карты там, где это возможно, для приближенной реализации динамического отображения окружения.

Несмотря на то, что динамическое отображение – замечательная техника, приходится использовать множество ухищрений и находчивых хаков, чтобы применить её в приложении без значительной просадки производительности.

Упражнения


Попробуйте добавить карту отражения в загрузчик моделей, созданный в соответствующем уроке. Данные модели нанокостюма, включая саму текстуру карты отражений, можно скачать здесь. Отмечу несколько моментов:

  • Assimp явно недолюбливает карты отражений в большинстве форматов моделей, потому мы немного схитрим – будем хранить карту отражений как карту фоновой засветки. Загрузку карты, соответственно, придется выполнять с передачей типа текстуры aiTextureType_AMBIENT в процедуре загрузки материала.
  • Карта отражений наскоро создана из карты зеркального блеска. Поэтому в некоторых местах она не очень хорошо накладывается на модель.
  • Поскольку загрузчик модели уже использует три текстурных юнита в шейдере, то вам придется использовать четвертый юнит для привязки кубической карты, поскольку выборка из нее ведется в том же самом шейдере.

Если все сделано верно, то итог будет таким:


От переводчика:

Стоит отметить, что при работе с развертками для скайбоксов можно натолкнуться на значительную путаницу с ориентацией, особенно при именовании текстур по названиям граней («левая», «правая», ...), а не по направлениям осей (+X — positiveX, posX; -X — negativeX, negX, ...).
Это следствие того, что спецификация расширения кубических карт довольно древняя и своими корнями идет в спецификацию пиксаровского RenderMan, где кубическая карта использует леворукую систему координат и сам кубик сворачивается как если бы наблюдатель был в центре. Сама же OpenGL, как мы помним, использует праворукую систему координат.

Вспомнить об этом придется либо при несовпадении развертки интересующей текстуры с описанной здесь, либо при наличии необходимости выровнять «стороны» кубической карты относительно мировых координат так, будто свернут он с наблюдателем внутри.

На приведенном в разделе «Скайбокс» «кресте» развертки передняя грань считается таковой с позиции наблюдателя, находящегося снаружи кубика, смотрящего прямо на него. В коде она загружается в цель, соответствующую положительной полуоси Z (в мировых координатах). В приложении же выходит так, что изнутри куба мы наблюдаем изображение отзеркаленное по оси Х относительно того, что изображено на текстуре.

Автор сам дает ссылку на этот сайт, где использованная развертка не соответствует той, что показана в уроке. Чтобы привести их в соответствие придется поменять местами правую и левую панели, верхнюю панель повернуть по часовой на 90°, нижнюю – против часовой.


Развертки с этого сайта подписаны в соотвествии с направлениями осей, а не названиями граней. Все что потребуется – переименовать файлы в соответствии с здешней таблицей.

Понять суть проще в действии – в собранном приложении. Подписать изображения нарезки скайбокса предполагаемыми сторонами (из любопытства можно еще добавить оси текстурных координат) и увидеть в приложении, как на самом деле это обернется.

P.S.: У нас есть телеграм-конфа для координации переводов. Если есть желание вписаться в цикл, то милости просим!

Комментарии (2)


  1. GeorgeIV
    04.02.2018 08:59

    В примерах с cubemaps меня всегда волнует один вопрос — а если камера не в центре куба?
    Никогда не видел подобных примеров, но ведь человек необязательно находится в центре, допустим, комнаты.


    1. UberSchlag Автор
      04.02.2018 09:04

      Если это становится важно, а в больших проектах и а динамике это вполне ожидаемо, то при подготовке данных уровня ставят точки формирования выборок карт окружения и потом запекают это дело в ресурсы. Когда-нибудь дойдем до уроков по imaged lighting, там и видно будет, зачем нужны подробные карты окружения, в не только одна единственная.