Рубрики
3D мир Программирование

OpenGL 12: кубическая текстура и скайбокс

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

Введение

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

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

Рисунок 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.

Рисунок 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 без отладки источников.

Рисунок 3 — Результат рисования скайбокса с отладочным выводом источников света
Рисунок 3 — Результат рисования скайбокса без отладочного вывода источников света

Текущая версия доступна на теге 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.

Рисунок 5 — Отладочный вывод кубической текстуры на плоскость

Заключение

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

Проект доступен в публичном репозитории: 12
Библиотеки: dependencies
Ресурсы: resources

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.