Введение
В предыдущих заметках было разработано приложение, которое рисует произвольную геометрию красного цвета. Данная заметка посвящена использованию текстур для окрашивания моделей.
Содержание заметки:
Теория о текстурах
Текстурой называется изображение (чаще двумерное, реже одно- и трехмерное) используемое для задания цветов фрагмента, проходящего графический конвейер.
Примечание: текстура может использоваться для передачи на шейдер большого количества данных, которые не учавствуют в окрашивании фрагметнтов.
Для каждой вершины назначается текстурная координата, которая находится в промежутке [0.0;1.0]. Пример текстурных координат для треугольника представлен на рисунке 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.
Для задания необходимого поведения для каждой из осей изображения используется функция 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.
Дополнительный пример фильтрации текстуры при увеличении текселей представлен на рисунке 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.
Для большей понятности на рисунке 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.
Данную текстуру необходимо расположить в директории с ресурсами: ../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.
Текущая версия доступна на теге 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