Введение
Скайбоксом называется модель куба с кубической текстурой неба, используемая для создания фонового изображения сцены.
OpenGL позволяет работать с текстурами, описывающими пространство вокруг точки, такие текстуры называются кубическими. Пример такой текстуры представлен на рисунке 1.
Согласно рисунку 1 для работы со сторонами кубической текстуры используются следующие константы в C++:
- GL_TEXTURE_CUBE_MAP_POSITIVE_X — правая сторона с координатами (1, y, z);
- GL_TEXTURE_CUBE_MAP_NEGATIVE_X — левая сторона с координатами (-1, y, z);
- GL_TEXTURE_CUBE_MAP_POSITIVE_Y — верхняя сторона с координатами (x, 1, z);
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Y — нижняя сторона с координатами (x, -1, z);
- GL_TEXTURE_CUBE_MAP_POSITIVE_Z — передняя сторона с координатами (x, y, 1);
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Z — задняя сторона с координатами (x, y, -1).
Вся кубическая текстура с 6 сторонами содержится в одной области памяти с общим дескриптором, который используется в качестве GL_TEXTURE_CUBE_MAP. Для каждой стороны выделяется память и загружается своя 2D текстура (данное действие можно совершать в цикле начиная со стороны GL_TEXTURE_CUBE_MAP_POSITIVE_X).
Пример создания пустой кубической текстуры:
GLuint handler;
glGenTextures(1, &handler);
glBindTexture(GL_TEXTURE_CUBE_MAP, handler);
for (int i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, internalformat, width, height, 0, format, dataType, 0);
Примечание: пустая текстура может понадобиться для рендера теней от точечных источников или создания карты отражений.
Пример создания кубической текстуры и загрузка в неё изображений:
GLuint handler;
unsigned char* image;
int width, height, channels;
glGenTextures(1, &handler);
glBindTexture(GL_TEXTURE_CUBE_MAP, handler);
for (int i = 0; i < 6; ++i)
{
image = stbi_load(filename[i].c_str(), &width, &height, &channels, STBI_default); // Загрузка в оперативную память изображения
if (image)
{
if (channels == 3) // RGB
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, internalformat, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
else if (channels == 4) // RGBA
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
}
}
Содержание заметки:
- создание класса кубической текстуры;
- загрузка и рисование скайбокса;
- отладочный вывод кубической текстуры.
Создание класса кубической текстуры
Для удобства работы с кубическими текстурами создадим класс TextureCube (дочерний от BaseTexture) в файле include/Texture.h:
// Класс кубической текстуры
class TextureCube : public BaseTexture
{
public:
TextureCube(GLuint type = TEX_AVAILABLE_COUNT, const std::string (&filename)[6] = {""}); // Загрузка текстуры с диска или использование "пустой"
TextureCube(GLuint width, GLuint height, GLuint attachment, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Конструктор текстуры заданного размера для использования в буфере
TextureCube(const TextureCube& other); // Конструктор копирования
TextureCube& operator=(const TextureCube& other); // Оператор присваивания
void reallocate(GLuint width, GLuint height, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Пересоздает текстуру для имеющегося дескриптора
virtual void use(); // Привязка текстуры
};
Конструктор для загрузки текстуры из файла в целом повторяет работу аналогичного из класса Texture. Для реализации повторного использования аналогично классу Texture используется словарь filename_handler, который позволяет получить дескриптор по имени файла, но у кубической текстуры их шесть, так что необходимо соединить имена файлов в одной строке (complex_name). Далее, если такая кубическая текстура ещё не загружена, то создаем новую текстуру и в цикле считываем текстуры с диска, загружая их на нужные стороны. Если такая текстура уже была загружена, то можно просто получить её дескриптор на основании соединенных имен (complex_name). В конце просто инкрементируется счетчик количества использований дескриптора, представленный словарем handler_count.
Реализация в файле src/Texture.cpp конструктора с загрузкой текстур из файлов изображений:
// Загрузка текстуры с диска или использование "пустой"
TextureCube::TextureCube(GLuint t, const std::string (&filename)[6])
{
type = t;
std::string complex_name;
for (int i = 0; i < 6; i++)
complex_name += filename[i];
if (!filename_handler.count(complex_name))
{
std::string empty = "";
int width, height, channels; // Ширина, высота и цветовые каналы текстуры
unsigned char* image;
glActiveTexture(type + GL_TEXTURE0);
glGenTextures(1, &handler); // Генерация одной текстуры
glBindTexture(GL_TEXTURE_CUBE_MAP, handler); // Привязка текстуры как активной
filename_handler[complex_name] = handler; // Запомним её дескриптор для этого имени файла
handler_count[handler] = 0; // Создадим счетчик использований дескриптора, который будет изменен в конце
for (int i = 0; i < 6; i++)
{
image = stbi_load(filename[i].c_str(), &width, &height, &channels, STBI_default); // Загрузка в оперативную память изображения
// Если изображение успешно считано
if (image)
{
// Загрузка данных с учетом прозрачности
if (channels == 3) // RGB
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
else if (channels == 4) // RGBA
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
stbi_image_free(image); // Освобождение оперативной памяти
}
// Иначе изображение не считано и надо создать пустую текстуру
else
{
image = new unsigned char[3] {255,255,255}; // RGB по 1 байту на
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, image); // Загрузка данных на видеокарту
delete[] image; // Освобождение оперативной памяти
}
}
}
// Иначе используем уже существующую по имени файла
else
handler = filename_handler[complex_name];
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);
handler_count[handler]++;
}
Второй конструктор генерирует пустую кубическую текстуру, которая может быть использована для вывода содержимого буфера кадра:
// Конструктор текстуры заданного размера для использования в буфере
TextureCube::TextureCube(GLuint width, GLuint height, GLuint attachment, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
type = texType;
// Генерация текстуры заданного размера
glGenTextures(1, &handler);
glBindTexture(GL_TEXTURE_CUBE_MAP, handler);
for (int i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, internalformat, width, height, 0, format, dataType, 0);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Привязка к буферу кадра
glFramebufferTexture(GL_FRAMEBUFFER, attachment, handler, 0);
// Создаем счетчик использований дескриптора
handler_count[handler] = 1;
}
Реализация конструктора копирования, оператора присваивания и методов TextureCube::reallocate и TextureCube::use:
// Конструктор копирования
TextureCube::TextureCube(const TextureCube& other)
{
handler = other.handler;
type = other.type;
// Делаем копию и увеличиваем счетчик
handler_count[handler]++;
}
// Оператор присваивания
TextureCube& TextureCube::operator=(const TextureCube& other)
{
// Если это разные текстуры
if (handler != other.handler)
{
this->~TextureCube(); // Уничтожаем имеющуюся
// Заменяем новой
handler = other.handler;
handler_count[handler]++;
}
type = other.type;
return *this;
}
// Пересоздает текстуру для имеющегося дескриптора
void TextureCube::reallocate(GLuint width, GLuint height, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
use();
for (int i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, internalformat, width, height, 0, format, dataType, 0);
}
// Привязка текстуры
void TextureCube::use()
{
glActiveTexture(type + GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, handler); // Привязка текстуры как активной
}
Текущая версия доступна на теге v0.1 в репозитории 12.
Загрузка и рисование скайбокса
Готовую текстуру скайбокса можно скачать с сайта opengameart.org. Для примера в заметке будет использоваться работа пользователя Spiney «Cloudy Skyboxes». Склейка кубической текстуры скайбокса представлена на рисунке 2.
Переименованную по сторонам текстуру можно найти в репозитории resources/textures/skybox.
Для вывода скайбокса требуется добавить отдельные шейдеры.
Вершинный шейдер использует данные о камере и передает на фрагментный шейдер позицию пространства координат модели в качестве текстурной координаты (vec3). Так как от камеры не требуется трансформация по позиции, а только по вращению необходимо привести её к формату матрицы 3х3, но так как такая матрица не может участвовать в перемножении из-за её размерности, то следует вернуть её к формату 4х4. Содержимое файла shaders/skybox.vert:
#version 420 core
layout (location = 0) in vec3 pos;
out vec3 TexCoords;
layout(std140, binding = 0) uniform Camera
{
mat4 projection;
mat4 view;
vec3 position;
} camera;
void main()
{
TexCoords = pos;
gl_Position = camera.projection * mat4(mat3(camera.view)) * vec4(pos, 1.0);
}
Фрагментный шейдер использует полученную от вершинного шейдера позицию вершины в пространстве модели как текстурные координаты для кубической текстуры. Для получения цвета фрагмента из кубической текстуры используется трехмерный вектор. Дополнительно требуется указать значение буфера глубины в системной переменной gl_FragDepth равным 0.9999f для простоты последующего переноса в отложенном рендере. Содержимое файла shaders/skybox.frag:
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
gl_FragDepth = 0.9999f;
}
В файле src/main.cpp добавим модель куба и загрузим его координаты:
// Вершины для скайбокса
glm::vec3 skybox_verticies[] = {
{-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}
};
// Модель скайбокса
Model skybox;
skybox.load_verteces(skybox_verticies, sizeof(skybox_verticies)/sizeof(glm::vec3));
Далее создадим кубическую текстуру:
TextureCube skybox_texture(TEX_DIFFUSE, { "../resources/textures/skybox/px.jpg"
, "../resources/textures/skybox/nx.jpg"
, "../resources/textures/skybox/py.jpg"
, "../resources/textures/skybox/ny.jpg"
, "../resources/textures/skybox/pz.jpg"
, "../resources/textures/skybox/nz.jpg"
});
А так же загрузим созданные ранее шейдеры:
// Шейдер для скайбокса
ShaderProgram skyboxShader;
// Загрузим шейдеры
skyboxShader.load(GL_VERTEX_SHADER, "shaders/skybox.vert");
skyboxShader.load(GL_FRAGMENT_SHADER, "shaders/skybox.frag");
skyboxShader.link();
// Привязка текстуры скайбокса
const char* skybox_shader_names[] = {"skybox"};
skyboxShader.bindTextures(skybox_shader_names, sizeof(skybox_shader_names)/sizeof(const char*));
Теперь в цикле, где происходит рендер изображения (после копирования значений глубины) необходимо добавить вызов рисования куба скайбокса с его текстурой и шейдером без записи в буфер глубины:
// Перенос буфера глубины
FBO::useDefault(GL_DRAW_FRAMEBUFFER); // Базовый в режиме записи
gbuffer.use(GL_READ_FRAMEBUFFER); // Буфер геометрии в режиме чтения
// Копирование значений глубины
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
FBO::useDefault(); // Использование базового буфера для дальнейших работ
// Отрисовка скайбокса без записи глубины
glDepthMask(GL_FALSE);
// Используем шейдер для скайбокса
skyboxShader.use();
// Подключаем текстуру скайбокса
skybox_texture.use();
// Рендерим куб
skybox.render();
// Возвращаем запись глубины
glDepthMask(GL_TRUE);
// Отрисовка отладочных лампочек со специальным шейдером
bulbShader.use();
for (int i = 0; i < lights_count; i++)
lights[i].render(bulbShader, material_data);
Результат работы представлен на рисунке 3 с отладочным выводом источников света, на рисунке 4 без отладки источников.
Текущая версия доступна на теге v0.2 в репозитории 12.
Отладочный вывод кубической текстуры
Для отладочного вывода кубической карты на плоский прямоугольник можно использовать следующие вычисления на фрагментном шейдере для получения изображения:
float phi=texCoord.s*3.1415*2;
float theta=(-texCoord.t+0.5)*3.1415;
vec3 mapCoord = vec3(cos(phi)*cos(theta),sin(theta),sin(phi)*cos(theta));
Результат такой отладки представлен на рисунке 5.
Заключение
В данной заметке были рассмотрены кубические текстуры, для которых создан класс, упрощающий работу с ними, а так же создан куб и шейдеры для него, выполняющие роль скайбокса.
Проект доступен в публичном репозитории: 12
Библиотеки: dependencies
Ресурсы: resources