Введение
В предыдущих заметках было разработано приложение, которое рисует произвольную геометрию красного цвета. Данная заметка посвящена использованию текстур для окрашивания моделей.
Содержание заметки:
Теория о текстурах
Текстурой называется изображение (чаще двумерное, реже одно- и трехмерное) используемое для задания цветов фрагмента, проходящего графический конвейер.
Примечание: текстура может использоваться для передачи на шейдер большого количества данных, которые не учавствуют в окрашивании фрагметнтов.
Для каждой вершины назначается текстурная координата, которая находится в промежутке [0.0;1.0]. Пример текстурных координат для треугольника представлен на рисунке 1.
![](https://rekovalev.site/wp-content/uploads/2022/08/triangle_texture-700x700.png)
Для верхней вершины треугольника задается текстурная координата (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.
![](https://rekovalev.site/wp-content/uploads/2022/08/opengl_wrapping-1-700x195.png)
Для задания необходимого поведения для каждой из осей изображения используется функция 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.
![](https://rekovalev.site/wp-content/uploads/2022/08/opengl-text-filtering.png)
Дополнительный пример фильтрации текстуры при увеличении текселей представлен на рисунке 4.
![](https://rekovalev.site/wp-content/uploads/2022/08/opengl_text_filtering_2-700x201.png)
Поведение можно задать для увеличения (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.
![](https://rekovalev.site/wp-content/uploads/2022/08/ogl_mipmap_example-700x467.png)
Для большей понятности на рисунке 6 представлены два кадра: один со сгенерированной mipmap текстурой, а второй с обычной.
![](https://rekovalev.site/wp-content/uploads/2022/08/ogl_Mipmap_diff-700x250.png)
Для генерации 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.
![](https://rekovalev.site/wp-content/uploads/2022/08/grass.png)
Данную текстуру необходимо расположить в директории с ресурсами: ../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;
- тип byte:
- 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.
![](https://rekovalev.site/wp-content/uploads/2022/08/opengl-texture-grass-example-700x552.png)
Текущая версия доступна на теге 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