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

OpenGL 4: текстуры модели

Данная заметка посвящена использованию текстур для окрашивания моделей.

Введение

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

Содержание заметки:

Теория о текстурах

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

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

Для каждой вершины назначается текстурная координата, которая находится в промежутке [0.0;1.0]. Пример текстурных координат для треугольника представлен на рисунке 1.

Рисунок 1 — Пример текстурных координат треугольника

Для верхней вершины треугольника задается текстурная координата (0.5; 1.0), левой (0.0; 0.0), правой (1.0; 0.0).

Спецификация OpenGL позволяет определить поведение в случае, если текстуры выходят за границы диапазона [0.0; 1.0] с помощью следующих значений:

  • GL_REPEAT — повторение текстуры (отбрасывание целой части, которая больше 1.0), поведение по умолчанию;
  • GL_MIRRORED_REPEAT — аналогично предыдущему, но с зеркальным отражением;
  • GL_CLAMP_TP_EDGE — все координаты выходящие за границы будут приведены обратно к границам;
  • GL_CLAMP_TO_BORDER — координаты за пределами текстуры дают заданный цвет.

Примечание: такое поведение называется «wrapping«.

Пример использования различных поведений изображен на рисунке 2.

Рисунок 2 — Wrapping OpenGL

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

  • target — тип текстуры:
    • GL_TEXTURE_1D,
    • GL_TEXTURE_2D,
    • GL_TEXTURE_3D;
  • pname — в данном случае ось для которой изменяется поведение:
    • GL_TEXTURE_WRAP_S — ось X,
    • GL_TEXTURE_WRAP_T — ось Y,
    • GL_TEXTURE_WRAP_R — ось Z (для 3D текстур);
  • param — значение, описывающее поведение (описано выше).

Пример изменения поведения OpenGL для случаев с выходом за границы текстур:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

Для задания цвета, используемого при GL_CLAMP_TO_BORDER, необходимо вызвать функцию glTexParameterfv со следующими параметрами:

float borderColor[] = { 1.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

Тексель (элемент текстуры — texture element) — наименьший графический элемент в отображении текстуры на трехмерный объект.
Разницу между текселем и пикселем (элемент изображения — picture element) можно выделить из следующего примера:

  • когда объект расположен близко к камере и элементы текстуры большие, на один тексель может приходиться несколько пикселей;
  • когда объект расположен далеко от камеры и элементы текстуры маленькие, на один пиксель может приходиться несколько текселей.

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

  • GL_NEAREST — берется значение одного ближайшего текселя;
  • GL_LINEAR — значения четырех ближайших текселей смешивается (билинейная фильтрация).

Пример фильтрации текстуры представлен на рисунке 3.

Рисунок 3 — Фильтрация текстур

Дополнительный пример фильтрации текстуры при увеличении текселей представлен на рисунке 4.

Рисунок 4 — Фильтрация текстур

Поведение можно задать для увеличения (magnifying) и уменьшения (minifying) текстуры с помощью ранее упомянутой функции glTexParameteri со следующими параметрами:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

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

Рисунок 5 — Пример Mipmap текстуры

Для большей понятности на рисунке 6 представлены два кадра: один со сгенерированной mipmap текстурой, а второй с обычной.

Рисунок 6 — Сравнение разницы Mipmap

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

Для использования mipmap необходимо указать это при определении фильтрации текстур:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

Рассмотрим параметры, используемые для работы с mipmap:

  • GL_NEAREST_MIPMAP_NEAREST — выбирает ближайшее разрешение mipmap текстуры и берет значение одного ближайшего текселя;
  • GL_LINEAR_MIPMAP_NEAREST — выбирает ближайшее разрешение mipmap текстуры и смешивает значения четырех ближайших текселей (билинейная фильтрация);
  • GL_NEAREST_MIPMAP_LINEAR — выбирает два наиболее подходящих разрешения mipmap текстуры, для каждого берет значение одного ближайшего текселя, а результатом является средневзвешенное значение от двух разрешений;
  • GL_LINEAR_MIPMAP_LINEAR — выбирает два наиболее подходящих разрешения mipmap текстуры, для каждого смешивает значения четырех ближайших текселей (билинейная фильтрация), а результатом является средневзвешенное значение от двух разрешений.

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

Важное замечание: текстура может не рисоваться, если указан режим масштабирования с учетом mipmap текстуры, но она не была сгенерирована.

По умолчанию в OpenGL используется одна текстура, которая автоматически привязывается к первому доступному sampler2D, но спецификация позволяет использовать дополнительные и в таком случае необходимо вручную привязывать индексы к нужным sampler’ам. Так как это uniform-переменная, то используется функция glUniform1i в совокупности с glGetUniformLocation для получения расположения uniform-переменной по имени.

Максимальное число одновременно используемых текстур зависит от спецификации версии OGL и драйверов графического адаптера. Получить это значение можно с помощью вызова функции glGetIntegerv с параметром GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:

    int maxActiveTexturesCount = 0;
    glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxActiveTexturesCount);
    std::cout << maxActiveTexturesCount << '\n';

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

Как говорилось выше привязка текстуры производится к активной по умолчанию, обозначенной значением GL_TEXTURE0, а для работы с несколькими текстурами нужно перед каждым вызовом glBindTexture вызвать функцию glActiveTexture, которой следует передать значение требуемой текстуры: GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2, GL_TEXTURE3

Примечание: для использования в цикле можно использовать сумму значения GL_TEXTURE0 с целым числом, например: GL_TEXTURE0 + 5 эквивалентно использованию GL_TEXTURE5.

Логично задавать значения uniform-переменных после переключения шейдеров и вынести это в отдельную функцию, а изменение активной текстуры производить перед привязкой нужной.

Примечание: подход с использованием нескольких текстур будет реализован в классе текстур и использован в последующих заметках.

Использование в шейдерах

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

Содержание вершинного шейдера shaders/shader.vert:

#version 330 core 

layout(location = 0) in vec3 pos; 
layout(location = 1) in vec2 inTexCoord;

uniform mat4 vp;
uniform mat4 model;

out vec2 texCoord;

void main() 
{ 
    gl_Position = vp * model * vec4(pos, 1.0);
    texCoord = inTexCoord;
} 

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

Содержание фрагментного шейдера shaders/shader.frag:

#version 330 core 

in vec2 texCoord;

uniform sampler2D tex_diffuse;

out vec4 color; 

void main() 
{ 
    color = texture(tex_diffuse, texCoord);
} 

Использование в тексте программы

Для загрузки текстур в оперативную память в пригодном для OpenGL виде будет использоваться библиотека stb, так как она не нуждается в компиляции. Скачать библиотеку можно из репозитория.

Если вы используете Makefile необходимо дополнить переменную CFLAGS:

CFLAGS += -I../dependencies/stb

Либо добавить библиотеку к задачам сборки .vscode/tasks.json после 19 строки:

...
            "args": [
                "-I${workspaceFolder}/../dependencies/stb",
...

И в параметры редактора .vscode/c_cpp_properties.json после 9 строки:

                "${workspaceFolder}/../dependencies/stb"

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

#define STB_IMAGE_IMPLEMENTATION 
#include <stb_image.h>

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

В заметке будет использоваться текстура травы, которая представлена на рисунке 7.

Рисунок 7 — Текстура травы

Данную текстуру необходимо расположить в директории с ресурсами: ../resources/textures. Данная директория дублирует содержимое репозитория resources.

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

    GLuint texture; // Дескриптор текстуры
    glGenTextures(1, &texture); // Генерация одной текстуры

Далее необходимо задать текстуру активной с помощью функции glBindTexture, которая принимает тип текстуры (аналогично ранее упомянутой glTexParameter), а так же дескриптор нужной текстуры. С помощью этой же функции можно отключить текстуру.

Пример привязки и отвязки текстуры:


    glBindTexture(GL_TEXTURE_2D, texture); // Привязка текстуры как активной
...
    glBindTexture(GL_TEXTURE_2D, 0); // Отвязка активной текстуры

Теперь можно загрузить саму текстуру в оперативную память. Для этого понадобятся указатель на беззнаковый char и три целочисленных переменные под информацию об изображении: ширина, высота и количество цветовых каналов. В библиотеке stb_image для загрузки с диска используется функция stbi_load, которая принимает в качестве аргументов адрес файла, три адреса на целочисленные переменные (ранее объявленные) и запрашиваемые каналы. Последний аргумент принимает следующие значения:

  • STBI_default — загрузить все доступные;
  • STBI_grey — только оттенки серого;
  • STBI_grey_alpha — оттенки серого с каналом прозрачности;
  • STBI_rgb — цветной RGB;
  • STBI_rgb_alpha — цветной RGB с каналом прозрачности.

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

Пример загрузки изображения в оперативную память:

    int width, height, channels; // Ширина, высота и цветовые каналы текстуры
    unsigned char* image = stbi_load("../resources/textures/grass.png", &width, &height, &channels, STBI_default); // Загрузка в оперативную память изображения

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

  • target — предназначение текстуры (в заметках будет использоваться GL_TEXTURE_2D);
  • level — уровень детализации, который позволяет загрузить разные уровни mipmap текстуры при значениях больше 0;
  • internalformat — определяет количество цветовых каналов текстуры:
    • GL_RED,
    • GL_RG,
    • GL_RGB,
    • GL_RGBA,
    • GL_DEPTH_COMPONENT,
    • GL_DEPTH_STENCIL;
  • width — ширина изображения;
  • height — высота изображения;
  • border — «значение должно равняться нулю» (OpenGL-Refpages);
  • format — формат пикселя загружаемого изображения:
    • GL_RED,
    • GL_RG,
    • GL_RGB,
    • GL_BGR,
    • GL_RGBA,
    • GL_BGRA,
    • GL_RED_INTEGER,
    • GL_RG_INTEGER,
    • GL_RGB_INTEGER,
    • GL_BGR_INTEGER,
    • GL_RGBA_INTEGER,
    • GL_BGRA_INTEGER,
    • GL_STENCIL_INDEX,
    • GL_DEPTH_COMPONENT,
    • GL_DEPTH_STENCIL;
  • type — тип данных, которым представлен конкретный пиксель изображения:
    • тип byte:
      • GL_BYTE,
      • GL_UNSIGNED_BYTE,
      • GL_UNSIGNED_BYTE_3_3_2,
      • GL_UNSIGNED_BYTE_2_3_3_REV,
    • short:
      • GL_SHORT,
      • GL_UNSIGNED_SHORT,
      • GL_UNSIGNED_SHORT_5_6_5,
      • GL_UNSIGNED_SHORT_5_6_5_REV,
      • GL_UNSIGNED_SHORT_4_4_4_4,
      • GL_UNSIGNED_SHORT_4_4_4_4_REV,
      • GL_UNSIGNED_SHORT_5_5_5_1,
      • GL_UNSIGNED_SHORT_1_5_5_5_REV,
    • int:
      • GL_INT,
      • GL_UNSIGNED_INT,
      • GL_UNSIGNED_INT_8_8_8_8,
      • GL_UNSIGNED_INT_8_8_8_8_REV,
      • GL_UNSIGNED_INT_10_10_10_2,
      • GL_UNSIGNED_INT_2_10_10_10_REV
    • float:
      • GL_FLOAT,
      • GL_HALF_FLOAT;
  • data — адрес массива с пикселями загружаемого изображения.

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

    if (channels == 3) // RGB
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    else if (channels == 4) // RGBA
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);

Примечание: internalformat может иметь различное число каналов по сравнению с загружаемой текстурой (format) и все будет обработано корректно.

После загрузки текстуры, можно сгенерировать mipmap:

    glGenerateMipmap(GL_TEXTURE_2D);

Теперь можно отвязать текстуру и освободить оперативную память:

    glBindTexture(GL_TEXTURE_2D, 0); // Отвязка активной текстуры
    stbi_image_free(image); // Освобождение оперативной памяти

Так же необходимо не забыть указать использование mipmap для уменьшенных версий текстуры:

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // Использование уменьшенных версий mipmap  

Для корректной работы приложения необходимо изменить класс модели, чтобы он работал с текстурными координатами. Добавим приватное поле Model::texCoords_vbo и публичный метод Model::load_texCoords:

class Model : public Node
{
    public:

...
        void load_texCoords(glm::vec2* texCoords, GLuint count); // Загрузка текстурных координат в буфер

...
    private:
...
        BO texCoords_vbo; // буфер с текстурными координатами
...
};

Важное замечание: необходимо не забыть проинициализировать значение поля с указателем на вершинный буфер в конструкторе без параметров и в конструкторе копирования:

Model::Model() : ..., texCoords_vbo(VERTEX)
Model::Model(const Model& copy) : ..., texCoords_vbo(copy.texCoords_vbo)

Для метода Model::load_texCoords понадобится вспомогательная функция texCoords_attrib_config для конфигурации атрибута вершинного буфера. Их реализация:

// Функция для конфигурации атрибута вершинного буфера
void texCoords_attrib_config()
{
    // Определим спецификацию атрибута
    glVertexAttribPointer(  1         // индекс атрибута, должен совпадать с Layout шейдера
                          , 2         // количество компонент одного элемента
                          , GL_FLOAT  // тип
                          , GL_FALSE  // необходимость нормировать значения
                          , 0         // шаг
                          , (void *)0 // отступ с начала массива
                         );
    // Включаем необходимый атрибут у выбранного VAO
    glEnableVertexAttribArray(1);
} 

// Загрузка текстурных координат в буфер
void Model::load_texCoords(glm::vec2* texCoords, GLuint count)
{
    // Подключаем VAO
    vao.use();

    texCoords_vbo.use();

    // Загрузка вершин в память буфера
    texCoords_vbo.load(texCoords, sizeof(glm::vec2)*count);
    texCoords_attrib_config();
}

В файле src/main.cpp добавим к модели текстурные координаты:

    // Текстурные координаты
    glm::vec2 texCoords[] = {  {0.0f, 0.0f}
                             , {1.0f, 0.0f}
                             , {1.0f, 1.0f}
                             , {0.0f, 1.0f}
                            };

    // Загрузка текстурных координат модели
    rectangle.load_texCoords(texCoords, sizeof(texCoords)/sizeof(glm::vec2));

А так же изменим трансформацию модели для того, чтобы расположить её горизонтально внизу кадра:

    // Зададим горизонтальное положение перед камерой
    rectangle.e_position().y = -1;
    rectangle.e_position().z = 3;
    rectangle.e_rotation() = {0.707f, 0.707f, 0.0f, 0.0f};
    rectangle.e_scale() = glm::vec3(3);

Осталось только в цикле перед рендером модели подключить её текстуру:

        glBindTexture(GL_TEXTURE_2D, texture); // Привязка текстуры как активной
        rectangle.render(model_uniform);

Перед завершением работы приложения необходимо удалить текстуру:

    // Удаление текстуры
    glDeleteTextures(1, &texture);

Теперь можно запустить приложение и получить результат, представленный на рисунке 8.

Рисунок 8 — Пример работы приложения

Текущая версия доступна на теге v0.1 в репозитории 04.

Класс текстуры

Добавим два файла к проекту: include/Texture.h и src/Texture.cpp.

В файл include/Texture.h необходимо добавить перечисление (enum), которое будет использоваться для определения предназначения текстуры. Пока перечисление будет содержать только значение TEX_DIFFUSE, которое используется для базовой текстуры, и TEX_AVAILABLE_COUNT, но потом добавится текстура нормалей и прочие по ходу развития цикла заметок.

enum TexType {
    TEX_DIFFUSE,
    TEX_AVAILABLE_COUNT
};

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

Сам класс текстуры имеет следующий вид:

class Texture
{
    public:
        Texture(GLuint type = TEX_AVAILABLE_COUNT, const std::string& filename = ""); // Загрузка текстуры с диска или использование "пустой"
        Texture(const Texture& other); // Конструктор копирования
        ~Texture();

        Texture& operator=(const Texture& other); // Оператор присваивания

        static void init_textures(GLuint programID); // Инициализация текстур на шейдере
        void use(); // Привязка текстуры
        static void disable(GLuint type); // Отвязка текстуры по типу
        GLuint getType(); // Возвращает тип текстуры
    private:
        GLuint handler; // Дескриптор текстуры
        GLuint type; // Тип текстуры, соответствует её слоту
        static std::map<std::string, int> filename_handler; // Получение дескриптора текстуры по её имени
        static std::map<int, int> handler_count; // Получение количества использований по дескриптору текстуры (Shared pointer)
};

Конструктор имеет значения параметров по умолчанию, которые позволяют использовать «пустую» текстуру.

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

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

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

Оба словаря, являясь статическими полями должны быть объявлены в файле src/Texture.cpp:

std::map<std::string, int> Texture::filename_handler; // Получение дескриптора текстуры по её имени
std::map<int, int> Texture::handler_count; // Получение количества использований по дескриптору текстуры (Shared pointer)

Для работы с дескрипторами и счетчиком конструкторы, деструктор и оператор присваивания имеют следующий вид в файле src/Texture.cpp:

// Загрузка текстуры с диска или использование "пустой"
Texture::Texture(GLuint t, const char* filename) : type(t)
{
    if (!filename_handler.count(filename))
    {
        std::string empty = "";
        int width, height, channels; // Ширина, высота и цветовые каналы текстуры
        unsigned char* image = stbi_load(filename.c_str(), &width, &height, &channels, STBI_default); // Загрузка в оперативную память изображения
        // Если изображение успешно считано с диска или отсутствует пустая текстура
        if (image || !filename_handler.count(empty))
        {
            glActiveTexture(type + GL_TEXTURE0);
            glGenTextures(1, &handler); // Генерация одной текстуры
            glBindTexture(GL_TEXTURE_2D, handler); // Привязка текстуры как активной

            filename_handler[filename] = handler; // Запомним её дескриптор для этого имени файла
            handler_count[handler] = 0; // Создадим счетчик использований дескриптора, который будет изменен в конце

            // Если изображение успешно считано
            if (image)
            {
                // Загрузка данных с учетом прозрачности
                if (channels == 3) // RGB
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
                else if (channels == 4) // RGBA
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);

                glGenerateMipmap(GL_TEXTURE_2D); // Генерация мипмапа для активной текстуры
                glBindTexture(GL_TEXTURE_2D, 0); // Отвязка активной текстуры
                
                stbi_image_free(image); // Освобождение оперативной памяти
            }
            // Иначе изображение не считано и надо создать пустую текстуру
            else
            {
                image = new unsigned char[3] {255,255,255}; // RGB по 1 байту на 
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, image); // Загрузка данных на видеокарту
                delete[] image; // Освобождение оперативной памяти
                
                filename_handler[empty] = handler; // Запомним дополнительно её дескриптор для NULL-строки
            }
        }
        // Иначе используем существующую пустую текстуру (текстура не загружена, пустую создавать не нужно)
        else
            handler = filename_handler[empty];
    }
    // Иначе используем уже существующую по имени файла
    else
        handler = filename_handler[filename];
        
    handler_count[handler]++;
}

// Конструктор копирования
Texture::Texture(const Texture& other) : handler(other.handler), type(other.type)
{
    // Делаем копию и увеличиваем счетчик
    handler_count[handler]++;
}

// Оператор присваивания
Texture& Texture::operator=(const Texture& other)
{
    // Если это разные текстуры
    if (handler != other.handler)
    {
        this->~Texture(); // Уничтожаем имеющуюся
        // Заменяем новой
        handler = other.handler;
        handler_count[handler]++;
    }
    type = other.type;

    return *this;
}

Texture::~Texture()
{
    if (!--handler_count[handler]) // Если количество ссылок = 0
    {
        glDeleteTextures(1, &handler); // Удаление текстуры
        // Удаление из словаря имен файлов и дескрипторов
        for (auto it = filename_handler.begin(); it != filename_handler.end();)
        {
            if (it->second == handler)
                it = filename_handler.erase(it);
            else
                it++;
        }
    }
}

Теперь за удаление текстуры отвечает деструктор, что отсвобождает от необходимости вызова функции удаления текстуры в файле src/main.cpp.

Важное замечание: определение функций библиотеки stb_image перенесено из файла src/main.cpp в src/Texture.cpp:

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

Методы Texture::use, Texture::disable и Texture::getType понадобятся для организации связи с классом Model:

// Привязка текстуры
void Texture::use()
{
    glActiveTexture(type + GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, handler); // Привязка текстуры как активной
}

// Отвязка текстуры по типу
void Texture::disable(GLuint type)
{
    glActiveTexture(type + GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, 0); // Отвязка текстуры
}

// Возвращает тип текстуры
GLuint Texture::getType()
{
    return type;
}

Метод Texture::init_textures предназначен для инициализации значений sampler2D uniform-переменных:

const char* textures_base_shader_names[] = {"tex_diffuse"};

// Инициализация текстур на шейдере
void Texture::init_textures(GLuint programID)
{
    // Цикл по всем доступным текстурам
    for (int i = 0; i < TEX_AVAILABLE_COUNT; i++)
        glUniform1i(glGetUniformLocation(programID, textures_base_shader_names[i]), i); 
}

Важное замечание: необходимо не забывать следить за содержимым массива textures_base_shader_names при добавлении новых типов текстур.

Теперь можно перейти к файлу src/main.cpp.

Вызовем метод Texture::init_textures после инициализации шейдера.

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

    Texture grass(TEX_DIFFUSE, "../resources/textures/grass.png");

А так же необходимо вызвать метод Texture::use перед рендером модели:

        grass.use();
        rectangle.render(model_uniform);

Текущая версия доступна на теге v0.2 репозитория 04.

Для удобства было бы хорошо привязать текстуру к классу модели, для этого требуется добавить к классу приватное поле Model::diffuse и публичный метод Model::set_texture.

Реализация метода Model::set_texture:

// Привязка текстуры к модели
void Model::set_texture(Texture& texture)
{
    GLuint type = texture.getType();
    switch(type)
    {
        case TEX_DIFFUSE:
            texture_diffuse = texture;
            break;
    };
}

Изменим конструктор копирования и оператор присваивания:

// Конструктор копирования
Model::Model(const Model& copy) : Node(copy),
vao(copy.vao), 
verteces_count(copy.verteces_count), first_index_byteOffset(copy.first_index_byteOffset), indices_count(copy.indices_count), 
vertex_vbo(copy.vertex_vbo), index_vbo(copy.index_vbo), texCoords_vbo(copy.texCoords_vbo),
texture_diffuse(copy.texture_diffuse)
{
...
}

// Оператор присваивания
Model& Model::operator=(const Model& other)
{
...

    texture_diffuse = other.texture_diffuse;
    
    return *this;
}

Осталось изменить метод Model::render, подключив текстуры перед подключением VAO:

void Model::render(const GLuint &mvp_uniform) 
{
...
    // Подключаем текстуры
    texture_diffuse.use();
        
    // Подключаем VAO
...
}

В файле src/main.cpp осталось привязать текстуру после создания объекта и перед вызовом рендера объекта убрать принудительное использование текстуры:


...
    // Текстура травы
    Texture grass(TEX_DIFFUSE, "../resources/textures/grass.png");
    rectangle.set_texture(grass);
...
    while(!glfwWindowShouldClose(window))
    {

...
        // Тут производится рендер
        rectangle.render(model_uniform);
...

Текущая версия доступна на теге v0.3 репозитория 04.

Заключение

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

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

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

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

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