На этом уроке мы рассмотрим использование текстур в OpenGL.
Текстуры в OpenGL
Мы узнали, что для того, чтобы добавить больше деталей к нашим объектам, мы можем использовать указание цветов для каждой вершины, делая итоговое изображение более интересным. Однако, чтобы получить изрядную долю реализма, необходимо было бы иметь гораздо больше вершин, чтобы мы могли указать больше цветов. Данный подход создает значительное количество дополнительных накладных расходов, так как для каждого создаваемого объекта потребуется гораздо большее количество вершин, использующих вдобавок дополнительную информацию в виде атрибутов цвета.
В таких случаях художники и программисты обычно предпочитают использовать текстуру. Текстура — это 2D-изображение (замечу, что также существуют 1D-текстуры и 3D-текстуры), используемое для добавления деталей к объекту; вы можете представить себе текстуру как лист бумаги с красивым узором из кирпичей на нем, аккуратно наложенным поверх вашего 3D-дома, чтобы он выглядел так, как будто ваш дом сделан из кирпича. Данный способ позволяет добавлять множество деталей в одно изображение, а это значит, что у нас появляется возможность создавать иллюзию чрезвычайно детализированного объекта без необходимости указывать дополнительные вершины.
Помимо непосредственно изображений, текстуры также могут использоваться и для хранения самых разнообразных данных, предназначенных для отправки в шейдеры, но рассмотрение этого вопроса мы оставим для другой темы.
Ниже вы увидите текстурное изображение кирпичной стены:
Которое мы наложим на треугольник из предыдущего урока:
Чтобы сопоставить текстуру с треугольником, нужно для каждой вершины треугольника указать, какой части текстуры она соответствует. Таким образом, каждая вершина должна иметь связанную с ней текстурную координату, которая определяет, из какой части текстурного изображения следует взять фрагмент. Затем фрагментная интерполяция сделает всё остальное и для других фрагментов.
Значения текстурных координат лежат в диапазоне от 0
до 1
по соответствующим осям X
и Y
(напоминаю, что мы используем 2D-текстурные изображения). Получение цвета текстуры с использованием текстурных координат называется сэмплированием. Текстурные координаты отсчитываются от точки (0,0)
, соответствующей нижнему левому углу текстурного изображения, и до точки (1,1)
, соответствующей правому верхнему углу текстурного изображения.
На следующем рисунке показано, как текстурные координаты сопоставляются с треугольником:
Мы задаем 3 точки для треугольника. При этом мы хотим, чтобы нижняя левая часть треугольника соответствовала нижней левой части текстуры, поэтому используем текстурную координату (0,0)
для нижней левой вершины треугольника. То же самое относится и к нижней правой части с текстурной координатой (1,0)
. Для верхней вершины треугольника возьмем точку с текстурными координатами (0.5, 1.0)
. Далее остается только передать 3 текстурные координаты в вершинный шейдер, который затем отправит их во фрагментный шейдер, а тот в свою очередь аккуратно интерполирует все текстурные координаты для каждого фрагмента.
Итак, текстурные координаты у нас будут выглядеть следующим образом:
1 2 3 4 5 |
float texCoords[] = { 0.0f, 0.0f, // нижний левый угол 1.0f, 0.0f, // нижний правый угол 0.5f, 1.0f // верхний угол }; |
Текстурное сэмплирование может быть выполнено различными способами. Следовательно, наша задача — это рассказать OpenGL, как он должен выполнить сэмплирование.
Наложение текстур
Как уже говорилось выше, значения текстурных координат обычно лежат в диапазоне [0,0; 1,1]
, но что произойдет, если мы зададим координаты вне этого диапазона? По умолчанию, поведение OpenGL заключается в повторении текстурных изображений, но есть и другие варианты:
GL_REPEAT
— поведение, заданное по умолчанию для текстур. Повторяет текстурное изображение.
GL_MIRRORED_REPEAT
— то же самое, что и GL_REPEAT
, но отзеркаливает изображение с каждым повторением.
GL_CLAMP_TO_EDGE
— фиксирует координаты между 0
и 1
. Результатом является то, что текстурные координаты, значения которых лежат вне данного диапазона, приводятся к интервалу [0, 1]
, а у текстурного изображения растягиваются края.
GL_CLAMP_TO_BORDER
— координаты вне диапазона закрашиваются заданным пользователем цветом границы.
Каждый из вышеперечисленных параметров будет иметь свой визуальный эффект, если использовать текстурные координаты, выходящие за границы заданного по умолчанию диапазона значений. Давайте посмотрим, как это всё будет выглядеть на образце текстурного изображения:
Вышеупомянутые параметры могут быть заданы для каждой координатной оси (оси S
, T
и R
, эквивалентные осям X
, Y
, Z
, если вы используете 3D-текстуры) с помощью функции glTexParameteri():
1 2 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); |
Рассмотрим детально вызов функции glTexParameteri():
Аргумент №1: С каким типом изображения (1D- или 2D-) мы собираемся работать; мы пользуемся 2D-текстурами, поэтому выбираем GL_TEXTURE_2D
.
Аргумент №2: Для какой оси текстуры будет задаваться выбранный нами режим наложения; мы хотим настроить его как для оси S
, так и для оси T
.
Аргумент №3: Указываем режим наложения текстуры; в нашем случае для текущей активной текстуры задан режим GL_MIRRORED_REPEAT
.
Если бы мы выбрали режим GL_CLAMP_TO_BORDER
, то еще должны были бы указать цвет границы. Это делается с использованием функции glTexParameterfv() с аргументом GL_TEXTURE_BORDER_COLOR
, в котором, используя массив значений типа float, мы передаем значение цвета границы:
1 2 |
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); |
Фильтрация текстур
Текстурные координаты не зависят от разрешения, но при этом могут быть любым значением типа с плавающей точкой, а это означает, что OpenGL должен вычислить, какому текстурному пикселю (также известному как тексель (англ. «texel»)) сопоставить нужную текстурную координату. Данный момент становится особенно важным, если у вас есть очень большой объект и текстура с низким разрешением. Вы, вероятно, уже догадались, что мы подошли к проблеме фильтрации текстур, и в OpenGL для этого есть соответствующий набор опций. Вообще, таких опций довольно много, но сейчас мы обсудим наиболее важные из них, а именно: GL_NEAREST
и GL_LINEAR
.
GL_NEAREST
(также известен как «фильтрация с использованием «метода ближайших соседей») — это метод фильтрации текстур, заданный по умолчанию в OpenGL. При использовании режима GL_NEAREST
, OpenGL выбирает тексель, центр которого находится ближе всего к текстурной координате. Ниже вы можете увидеть 4 пикселя, где положение крестика представляет собой конкретную текстурную координату. Центр верхнего левого текселя ближе всего расположен к текстурной координате и поэтому выбирается в качестве сэмплированного цвета:
GL_LINEAR
(также известен как (би)линейная фильтрация) принимает интерполированное значение текстурных координат соседних текселей и аппроксимирует цвет между текселями. Чем меньше расстояние от текстурной координаты до центра текселя, тем больший вклад тексель вносит своим цветом в сэмплированный цвет. Ниже мы видим, что в качестве результата возвращается смешанный цвет соседних текселей:
Но какой будет визуальный эффект у данных методов фильтрации текстур? Давайте посмотрим, как они работают при использовании текстуры с низким разрешением на большом объекте (из-за этого текстура масштабируется с ухудшением качества картинки, и становятся заметны отдельные тексели):
GL_NEAREST
приводит к появлению угловатых узоров (мы можем ясно видеть пиксели, образующие текстуру), в то время как GL_LINEAR
создает более гладкий узор, на котором отдельные пиксели менее заметны. GL_LINEAR
производит более реалистичный вывод изображения, но некоторые разработчики больше предпочитают использовать 8-битный внешний вид (пиксельную графику) и в результате выбирают самый простой вариант — GL_NEAREST
.
Также фильтрация текстур может устанавливаться и для операций увеличения или уменьшения (когда происходит увеличение или уменьшения масштаба), поэтому мы можем, например, использовать GL_NEAREST
-фильтрацию, когда текстуры масштабируются с уменьшением масштаба, и GL_LINEAR
-фильтрацию для текстур, масштабируемых с увеличением масштаба. Таким образом, чтобы указать метод фильтрации для обоих вариантов, можно воспользоваться функцией glTexParameteri(). Код должен выглядеть так же, как и для указания метода наложения:
1 2 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
Мипмапы
Представьте себе, что у нас есть большая комната с тысячами предметов, каждый из которых имеет заданную текстуру. В этой комнате будут находиться далекие объекты, которые при этом имеют такую же текстуру высокого разрешения, как и объекты, расположенные близко к зрителю. Поскольку объекты находятся далеко и, вероятно, создают лишь несколько фрагментов, OpenGL испытывает трудности с получением правильного значения цвета для фрагмента из текстуры высокого разрешения, поскольку он должен выбрать цвет текстуры для фрагмента, который охватывает большую часть текстуры. Это приводит к появлению видимых артефактов на небольших объектах, не говоря уже о потере пропускной способности памяти при использовании текстур высокого разрешения на небольших объектах.
Для решения этой проблемы OpenGL использует концепцию мипмап-текстур (англ. «mipmap», от лат. «multum in parvo» = «много в малом»), которая представляет собой набор текстурных изображений, где каждая последующая текстура в два раза меньше предыдущей. Идея, лежащая в основе мипмап-текстур, очень проста для понимания: после определенного порога расстояния от зрителя, OpenGL будет использовать другую мипмап-текстуру, которая лучше всего подходит для изображения объекта на заданном расстоянии. Поскольку объект находится далеко, меньшее разрешение текстуры не будет заметно пользователю. Благодаря этому, OpenGL сможет сэмплировать правильные тексели, используя меньше кэш-памяти. Давайте поближе рассмотрим то, как выглядят мипмап-текстуры:
Каждая из этих текстур называется мипмап-уровнем (или «уровнем детализации»).
Создание вручную коллекции мипмап-текстур для каждого текстурного изображения является довольно громоздким делом, но, к счастью, OpenGL может выполнить всю эту работу за нас с помощью одного вызова функции glGenerateMipmaps() после того, как мы создали текстуру.
При переключении между мипмап-уровнями во время рендеринга, OpenGL может отобразить некоторые артефакты, такие как: острые края, видимые между двумя мипмап-уровнями. Так же, как и в предыдущем примере с обычной текстурой, мы можем задействовать фильтрацию текстур и между мипмап-уровнями, используя режимы NEAREST
и LINEAR
при переключении между мипмап-уровнями. Чтобы указать метод фильтрации между мипмап-уровнями, мы можем заменить исходные методы фильтрации одним из следующих четырех вариантов:
GL_NEAREST_MIPMAP_NEAREST
— принимает ближайшую мипмап-текстуру, которая соответствует размеру пикселя, и использует интерполяцию по методу ближайших соседей для текстурного сэмплирования.
GL_LINEAR_MIPMAP_NEAREST
— использует ближайший мипмап-уровень и сэмплирует его с помощью метода линейной интерполяции.
GL_NEAREST_MIPMAP_LINEAR
— линейная интерполяция между двумя мипмап-текстурами, которые наиболее точно соответствуют размеру пикселя, и затем сэмплирование интерполированного уровня при помощи «метода ближайших соседей».
GL_LINEAR_MIPMAP_LINEAR
— линейная интерполяция между двумя ближайшими мипмап-текстурами и сэмплирование интерполированного уровня с помощью метода линейной интерполяции.
Так же, как и в примере с фильтрацией текстур, мы можем задать способ фильтрации к одному из четырех вышеупомянутых методов с помощью функции glTexParameteri():
1 2 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
Распространенной ошибкой является установка одного из параметров мипмап-фильтрации в качестве фильтра увеличения. Данное действие не будет иметь никакого эффекта, так как мипмап-текстуры используются в основном для уменьшения масштаба текстур. Увеличение текстуры не использует мипмап-текстуры, и задание для него опции фильтрации мипмап-текстур приведет к возникновению ошибки с кодом GL_INVALID_ENUM
в OpenGL.
Загрузка и создание текстур
Первое, что нам нужно сделать, чтобы непосредственно воспользоваться текстурами — это загрузить их в наше приложение. Изображения текстур могут храниться в десятках различных форматов файлов, каждый из которых имеет свою собственную структуру и порядок следования данных. Так как же мы собираемся получить эти изображения в нашем приложении? Одним из решений мог быть выбор формата файла, который мы хотели бы использовать, допустим, формат .png, и написание собственного загрузчика изображений для преобразования формата изображения в большой массив байтов. Хотя написать свой собственный загрузчик изображений не очень сложно, но это все равно является обременительной задачей, и что делать, если мы захотим добавить поддержку большего количества форматов файлов? Нужно будет писать загрузчик изображений для каждого формата, поддержку которого мы хотим обеспечить.
Другим решением, и, вероятно, наиболее рациональным, является использование библиотеки загрузки изображений, которая поддерживает несколько популярных форматов и делает всю эту тяжелую работу за нас. Примером такой библиотеки является библиотека stb_image.h.
Библиотека stb_image.h
Библиотека stb_image.h — это очень популярная библиотека загрузки изображений, написанная Шоном Барреттом, и при этом состоящая всего из одного заголовочного файла, которая способна загружать самые популярные форматы файлов (и её легко интегрировать в наш проект(ы)). Скачать stb_image.h можно здесь. Загружаем один единственный заголовочный файл, добавляем его в свой проект как stb_image.h и создаем еще один дополнительный .cpp-файл, например, stb_image.cpp со следующими строками:
1 2 |
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" |
А дальше в основном файле нашей программы (у меня это main.cpp) добавляем следующую директиву:
1 |
#include "stb_image.h" |
Далее в качестве текстуры мы будем использовать изображение деревянного ящика:
Чтобы загрузить изображение с помощью stb_image.h, мы воспользуемся содержащейся в нем функцией stbi_load():
1 2 |
int width, height, nrChannels; unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); |
Первым аргументом функция принимает расположение файла изображения. Затем, в качестве второго, третьего и четвертого аргументов — три переменные типа int, которые stb_image.h будет использовать в качестве данных о ширине, высоте и количестве цветовых каналов получившегося изображения. Забегая вперед, скажу, что нам потребуются ширина и высота изображения для последующего создания текстур.
Генерирование текстур
Как и у любых других из ранее рассмотренных OpenGL-объектов, у текстуры должен быть ссылающийся на нее идентификатор; давайте создадим его:
1 2 |
unsigned int texture; glGenTextures(1, &texture); |
Функция glGenTextures() в качестве входных данных принимает количество текстур, которые мы хотим создать, и сохраняет их в массиве типа unsigned int, заданном в качестве второго аргумента (в нашем случае этим аргументом является только одна переменная типа unsigned int). Так же, как и другие объекты, нам нужно связать объект текстуры для того, чтобы любые последующие текстурные команды взаимодействовали с текущей связанной текстурой:
1 |
glBindTexture(GL_TEXTURE_2D, texture); |
Теперь, когда текстура связана, мы можем сгенерировать текстуру, используя ранее загруженные графические данные. Текстуры генерируются с помощью функции glTexImage2D():
1 2 |
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); |
Функция имеет довольно большое количество параметров, поэтому давайте шаг за шагом рассмотрим каждый из них:
Аргумент №1: Указываем целевой тип текстуры; в нашем случае значение GL_TEXTURE_2D
означает, что данная операция будет генерировать текстуру для текущего связанного текстурного объекта в том же целевом типе (так что любые текстуры, привязанные к целевым типам GL_TEXTURE_1D
или GL_TEXTURE_3D
, не будут затронуты).
Аргумент №2: Указываем мипмап-уровень, для которого мы хотим создать текстуру, если у вас есть желание вручную задавать каждый мипмап-уровень (но мы оставим его на базовом уровне, который равен 0
).
Аргумент №3: Сообщаем OpenGL, в каком формате мы хотим хранить текстуру. Наше изображение имеет только RGB-значения, поэтому и хранить текстуру мы будем с RGB-значениями.
Аргументы №4-5: Указываем ширину и высоту результирующей текстуры. Мы сохранили их ранее при загрузке изображения, поэтому будем использовать соответствующие переменные.
Аргумент №6: Должен всегда быть равен 0
(наследие старого кода).
Аргументы №7-8: Указываем формат и тип данных исходного изображения. Мы загрузили изображение с RGB-значениями и сохранили их в виде набора типа char (байтов), поэтому передадим соответствующие значения.
Аргумент №9: Фактические данные изображения.
Как только произойдет вызов функции glTexImage2D(), связанный в данный момент текстурный объект теперь будет иметь текстуру, прикрепленную к нему. Однако сейчас он имеет только базовый уровень загруженного изображения текстуры, и если мы хотим использовать мипмап-текстуры, то мы должны указать все различные изображения вручную (постоянно увеличивая второй аргумент) или воспользоваться вызовом функции glGenerateMipmap() после создания текстуры. Вызов данной функции автоматически создаст все необходимые мипмап-уровни для текущей связанной текстуры.
После того, как мы закончим генерировать текстуру и соответствующие ей мипмап-уровни, рекомендуется освободить память, занимаемую изображением:
1 |
stbi_image_free(data); |
Весь процесс создания текстуры выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); // Устанавливаем параметры наложения и фильтрации текстур (для текущего связанного объекта текстуры) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Загрузка и генерация текстуры int width, height, nrChannels; unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); |
Применение текстур
Для следующих примеров мы будем использовать форму прямоугольника, нарисованную с помощью функции glDrawElements() из заключительной части урока №4. Нам нужно сообщить OpenGL, как проводить сэмплирование, поэтому придется обновить определение массива данных вершин, добавив в него координаты текстуры:
1 2 3 4 5 6 7 |
float vertices[] = { // координаты // цвета // текстурные координаты 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // верхняя правая 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // нижняя правая -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // нижняя левая -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // верхняя левая }; |
Так как мы добавили дополнительный атрибут вершины, то необходимо снова уведомить OpenGL о новом формате вершинных данных:
В коде:
1 2 |
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); |
Обратите внимание, что нам также стоит изменить параметр шага предыдущих двух атрибутов вершин на значение, равное 8 * sizeof(float)
.
Далее нам нужно изменить вершинный шейдер, чтобы иметь возможность принимать текстурные координаты в качестве атрибута вершины, а затем пересылать их во фрагментный шейдер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; layout (location = 2) in vec2 aTexCoord; out vec3 ourColor; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; TexCoord = aTexCoord; } |
После этого фрагментный шейдер должен принять выходную переменную TexCoord
в качестве входной переменной.
Фрагментный шейдер также должен иметь доступ к объекту текстуры, но как мы передадим объект текстуры во фрагментный шейдер? В языке GLSL есть встроенный тип данных для объектов текстуры под названием сэмплер, который принимает в качестве постфикса тип текстуры, который нам необходим, например, sampler1D
, sampler3D
или, в нашем случае, sampler2D
. Затем мы можем добавить текстуру во фрагментный шейдер, просто объявив uniform-переменную sampler2D
, которой мы позже присвоим нашу текстуру:
1 2 3 4 5 6 7 8 9 10 11 12 |
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture; void main() { FragColor = texture(ourTexture, TexCoord); } |
Для сэмплирования цвета текстуры мы воспользуемся встроенной в GLSL функцией texture(), которая в качестве первого аргумента принимает сэмплер текстуры, а в качестве второго — соответствующие координаты текстуры. Затем функция texture() производит сэмплирование соответствующего значения цвета, используя параметры текстуры, заданные ранее. Выходные данные этого фрагментного шейдера являются (фильтрованным) цветом текстуры соответствующей (интерполированной) текстурной координаты.
Всё, что нам осталось сейчас сделать, перед вызовом функции glDrawElements(), — это связать текстуру, и эта функция автоматически присвоит текстуру сэмплеру фрагментного шейдера:
1 2 3 |
glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); |
Если вы всё сделали правильно, то результат должен быть примерно следующим:
Если ваш прямоугольник полностью белый или черный, то вы, вероятно, допустили ошибку. Проверьте логи своего шейдера и попробуйте сравнить ваш код с исходным кодом приложения.
GitHub / Урок №6. Текстуры в OpenGL — Исходный код №1
Примечание: Если ваш текстурный код не работает или отображает полностью черную текстуру, продолжайте читать далее и переходите к последнему примеру, который должен работать. При работе с некоторыми драйверами требуется присвоить текстурный юнит для каждой uniform-переменной сэмплера, это мы и обсудим далее на текущем уроке.
Чтобы немного развеяться, давайте смешаем результирующий цвет текстуры с цветами вершин. Мы просто умножаем полученный цвет текстуры на цвет вершины во фрагментном шейдере, чтобы смешать оба цвета:
1 |
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0); |
Результат должен быть смесью цвета вершины и цвета текстуры:
Кажется, нашему ящику нравится стиль диско.
Текстурные юниты
Вы, вероятно, задавались вопросом, почему переменная sampler2D
является uniform-переменной, если мы даже не присвоили никакого значения ей при помощи glUniform()? Используя glUniform1i(), мы фактически можем присвоить location
-значение текстурному сэмплеру, чтобы установить сразу несколько текстур во фрагментном шейдере. Такое расположение текстуры называется текстурным юнитом (или «текстурной единицей»). Текстурный юнит, заданный по умолчанию, имеет значение 0
, при этом являясь активным по умолчанию текстурным юнитом, поэтому нам не нужно было присваивать location
в предыдущем разделе; обратите внимание, что не все графические драйверы присваивают текстурный юнит, заданный по умолчанию, поэтому результат предыдущего примера мог не отображаться у вас.
Основная цель текстурных юнитов — это позволить нам использовать более одной текстуры в наших шейдерах. Назначая текстурные юниты сэмплерам, мы можем связываться сразу с несколькими текстурами, если сначала активируем соответствующий текстурный юнит. Так же, как и при использовании функции glBindTexture(), мы можем активировать текстурные юниты с помощью функции glActiveTexture(), передавая в качестве параметра текстурный юнит, который мы хотели бы использовать:
1 2 |
glActiveTexture(GL_TEXTURE0); // сначала активируем текстурный юнит, прежде чем связывать текстуру glBindTexture(GL_TEXTURE_2D, texture); |
После активации текстурного юнита, последующий вызов glBindTexture() свяжет указанную в функции текстуру с текущим активным текстурным юнитом. Текстурный юнит GL_TEXTURE0
всегда по умолчанию активирован, поэтому в предыдущем примере при использовании glBindTexture() нам не нужно было активировать какие-либо текстурные юниты.
Примечание: OpenGL имеет как минимум 16 текстурных юнитов, которые вы можете использовать в своих целях. Их можно активировать с помощью параметров, начиная с GL_TEXTURE0
и заканчивая GL_TEXTURE15
. Они определены в последовательном порядке, поэтому вы также можете работать с GL_TEXTURE8
через выражение GL_TEXTURE0 + 8
. Это будет полезно, когда нам придется проходиться циклом по нескольким текстурным юнитам.
Однако нам все еще нужно отредактировать фрагментный шейдер, чтобы иметь возможность принять другой сэмплер:
1 2 3 4 5 6 7 8 9 10 |
#version 330 core ... uniform sampler2D texture1; uniform sampler2D texture2; void main() { FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2); } |
Конечный выходной цвет теперь представляет собой комбинацию двух текстур. Встроенная GLSL-функция mix() принимает два значения в качестве входных данных и линейно интерполирует их на основе своего третьего аргумента. Если третий аргумент равен 0.0
, то возвращается только первый входной аргумент без изменения цвета; если третий аргумент равен 1.0
, то возвращается только второй входной аргумент также полностью не прозрачный. Значение 0.2
возвращает 80% от первого входного цвета и 20% от второго входного цвета, что приводит к смешиванию обеих наших текстур.
Сейчас попробуем загрузить и создать другую текстуру; теперь вы должны быть знакомы с этими шагами. Убедитесь, что у вас создан дополнительный текстурный юнит, загрузите изображение и сгенерируйте окончательную текстуру с помощью функции glTexImage2D(). Для второй текстуры мы будем использовать изображение вашего выражения лица во время изучения OpenGL.
Код:
1 2 3 4 5 6 |
unsigned char *data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } |
Обратите внимание, что теперь мы загружаем .png-изображение, включающее альфа-канал (прозрачность). Это означает, что теперь с помощью GL_RGBA
нам нужно указать, что данные изображения также содержат и альфа-канал; в противном случае OpenGL будет неправильно их интерпретировать.
Чтобы использовать две текстуры одновременно, нам придется немного изменить процедуру рендеринга, привязав обе текстуры к соответствующему текстурному юниту:
1 2 3 4 5 6 7 |
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); |
Мы также должны сообщить OpenGL, к какому текстурному юниту относится каждый сэмплер шейдера, используя вызов функции glUniform1i(). Это необходимо указать один раз, поэтому сделать это нужно до того, как мы войдем в цикл рендеринга:
1 2 3 4 5 6 7 8 |
ourShader.use(); // не забудьте активировать шейдер перед настройкой uniform-переменных! glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // устанавливаем вручную… ourShader.setInt("texture2", 1); // …или с помощью шейдерного класса while(...) { [...] } |
Устанавливая сэмплеры через glUniform1i(), мы следим за тем, чтобы каждый uniform-сэмплер соответствовал правильному текстурному юниту. У вас должен получиться следующий результат:
GitHub / Урок №6. Текстуры в OpenGL — Исходный код №2
Вы, наверное, заметили, что текстура перевернута вверх ногами! Это происходит потому, что OpenGL ожидает, что точка с координатами 0.0
на оси Y
будет находиться в нижней части изображения, но изображения обычно имеют точку с координатами 0.0
в верхней части оси Y
. К счастью для нас, stb_image.h может перевернуть ось Y
во время загрузки изображения, для этого нужно добавить следующее выражение перед загрузкой любого изображения:
1 |
stbi_set_flip_vertically_on_load(true); |
После того, как вы дали указание для stb_image.h, чтобы она перевернула ось Y
при загрузке изображения, вы должны получить следующий результат:
GitHub / Урок №6. Текстуры в OpenGL — Исходный код №3
Упражнения
Чтобы лучше освоиться с текстурами, рекомендуется проработать эти упражнения, прежде чем продолжать обучение.
Задание №1
Сделайте так, чтобы текстура смайлика смотрела в другом/обратном направлении, изменив фрагментный шейдер.
Ответ №1
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture1; uniform sampler2D ourTexture2; void main() { FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2); } |
Задание №2
Поэкспериментируйте с различными методами наложения текстур, указав текстурные координаты в диапазоне от 0.0f
до 2.0f
, вместо 0.0f
до 1.0f
. Посмотрите, можно ли отобразить 4 смайлика, прижатых к краю, на одном изображении контейнера:
Ответ №2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <stb_image.h> #include <learnopengl/shader_s.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow *window); // Константы const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; int main() { // glfw: инициализация и конфигурирование glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Раскомментируйте данную часть кода, если используете macOS /* #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif */ // glfw: создание окна GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // glad: загрузка всех указателей на OpenGL-функции if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // Компилирование нашей шейдерной программы Shader ourShader("4.3.texture.vs", "4.3.texture.fs"); // Указывание вершин (и буферов) и настройка вершинных атрибутов float vertices[] = { // координаты // цвета // текстурные координаты (обратите внимание, что мы изменили их на 2.0f!) 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 2.0f, // верхняя правая 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 2.0f, 0.0f, // нижняя правая -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // нижняя левая -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 2.0f // верхняя левая }; unsigned int indices[] = { 0, 1, 3, // первый треугольник 1, 2, 3 // второй треугольник }; unsigned int VBO, VAO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // Атрибут позиции glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // Атрибут цвета glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // Атрибут координат текстуры glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); // Загружаем и создаем текстуру unsigned int texture1, texture2; // Текстура №1 glGenTextures(1, &texture1); glBindTexture(GL_TEXTURE_2D, texture1); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // обратите внимание, что мы установили метод обтекания контейнера GL_CLAMP_TO_EDGE glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Устанавливаем параметры фильтрации текстур glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Загружаем изображение, создаем текстуру и генерируем мипмап int width, height, nrChannels; stbi_set_flip_vertically_on_load(true); // просим stb_image.h перевернуть загруженную текстуру относительно оси Y // FileSystem::getPath(...) - это часть GitHub-репозитория, чтобы мы могли найти файлы на любую IDE/платформу; замените этот путь на свой собственный путь к изображению unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Текстура №2 glGenTextures(1, &texture2); glBindTexture(GL_TEXTURE_2D, texture2); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // мы хотим повторить шаблон, поэтому сохраняем его в GL_REPEAT glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // Устанавливаем параметры фильтрации текстур glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Загружаем изображение, создаем текстуру и генерируем мипмап data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0); if (data) { // Обратите внимание, что файл awesomeface.png имеет прозрачность и, следовательно, альфа-канал, поэтому обязательно сообщите OpenGL тип данных GL_RGBA glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Сообщаем OpenGL к какому текстурному юниту принадлежит каждый сэмплер (нужно указать один раз) ourShader.use(); // не забудьте активировать/использовать шейдер перед настройкой uniform-переменных! // Либо устанавливаем это вручную следующим образом glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // Либо устанавливаем через текстурный класс ourShader.setInt("texture2", 1); // Цикл рендеринга while (!glfwWindowShouldClose(window)) { // Обработка ввода processInput(window); // Рендеринг glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Выполняем привязку текстур на соответствующих текстурных юнитах glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); // Контейнер рендеринга ourShader.use(); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // glfw: обмен содержимым front- и back-буферов. Отслеживание событий ввода/вывода (была ли нажата/отпущена кнопка, перемещен курсор мыши и т.п.) glfwSwapBuffers(window); glfwPollEvents(); } // Опционально: освобождаем все ресурсы, как только они выполнили свое предназначение glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); // glfw: завершение, освобождение всех ранее задействованных GLFW-ресурсов glfwTerminate(); return 0; } // Обработка всех событий ввода: запрос GLFW о нажатии/отпускании кнопки мыши в данном кадре и соответствующая обработка данных событий void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // glfw: всякий раз, когда изменяются размеры окна (пользователем или операционной системой), вызывается данная callback-функция void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // Убеждаемся, что окно просмотра соответствует новым размерам окна. // Обратите внимание, высота окна на Retina-дисплеях будет значительно больше, чем указано в программе glViewport(0, 0, width, height); } |
Задание №3
Попробуйте отобразить только центральные пиксели изображения текстуры на прямоугольнике таким образом, чтобы отдельные пиксели становились видимыми в зависимости от текстурных координат. Попробуйте установить метод фильтрации текстур в GL_NEAREST
, чтобы лучше видеть пиксели.
Ответ №3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <stb_image.h> #include <learnopengl/shader_s.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow *window); // Константы const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; int main() { // glfw: инициализация и конфигурирование glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Раскомментируйте данную часть кода, если используете macOS /* #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif */ // glfw: создание окна GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // glad: загрузка всех указателей на OpenGL-функции if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // Компилирование нашей шейдерной программы Shader ourShader("4.4.texture.vs", "4.4.texture.fs"); // Указывание вершин (и буферов) и настройка вершинных атрибутов float vertices[] = { // координаты // цвета // текстурные координаты (обратите внимание, что мы изменили их, чтобы увеличить масштаб нашего изображения текстуры) 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.55f, 0.55f, // верхняя правая 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.55f, 0.45f, // нижняя правая -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.45f, 0.45f, // нижняя левая -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.45f, 0.55f // верхняя левая }; unsigned int indices[] = { 0, 1, 3, // первый треугольник 1, 2, 3 // второй треугольник }; unsigned int VBO, VAO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // Атрибут позиции glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // Атрибут цвета glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // Атрибут координат текстуры glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); // Загружаем и создаем текстуру unsigned int texture1, texture2; // Текстура №1 glGenTextures(1, &texture1); glBindTexture(GL_TEXTURE_2D, texture1); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // обратите внимание, что мы установили метод обтекания контейнера GL_CLAMP_TO_EDGE glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Устанавливаем параметры фильтрации текстур glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // устанавливаем фильтрацию текстуры "ближайшего соседа", чтобы четко видеть тексели/пиксели glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // Загружаем изображение, создаем текстуру и генерируем мипмапы int width, height, nrChannels; stbi_set_flip_vertically_on_load(true); // просим stb_image.h перевернуть загруженную текстуру относительно оси Y // FileSystem::getPath(...) - это часть GitHub-репозитория, чтобы мы могли найти файлы на любую IDE/платформу; замените этот путь на свой собственный путь к изображению unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Текстура №2 glGenTextures(1, &texture2); glBindTexture(GL_TEXTURE_2D, texture2); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // Устанавливаем параметры фильтрации текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // устанавливаем фильтрацию текстуры "ближайшего соседа", чтобы четко видеть тексели/пиксели glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // Загружаем изображение, создаем текстуру и генерируем мипмапы data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0); if (data) { // Обратите внимание, что файл awesomeface.png имеет прозрачность и, следовательно, альфа-канал, поэтому обязательно сообщите OpenGL тип данных GL_RGBA glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Сообщаем OpenGL к какому текстурному юниту принадлежит каждый сэмплер (нужно указать один раз) ourShader.use(); // не забудьте активировать/использовать шейдер перед настройкой uniform-переменных! // Либо устанавливаем это вручную следующим образом glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // Либо устанавливаем через текстурный класс ourShader.setInt("texture2", 1); // Цикл рендеринга while (!glfwWindowShouldClose(window)) { // Обработка ввода processInput(window); // Рендеринг glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Выполняем привязку текстур на соответствующих текстурных юнитах glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); // Контейнер рендеринга ourShader.use(); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // glfw: обмен содержимым front- и back-буферов. Отслеживание событий ввода/вывода (была ли нажата/отпущена кнопка, перемещен курсор мыши и т.п.) glfwSwapBuffers(window); glfwPollEvents(); } // Опционально: освобождаем все ресурсы, как только они выполнили свое предназначение glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); // glfw: завершение, освобождение всех ранее задействованных GLFW-ресурсов glfwTerminate(); return 0; } // Обработка всех событий ввода: запрос GLFW о нажатии/отпускании кнопки мыши в данном кадре и соответствующая обработка данных событий void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // glfw: всякий раз, когда изменяются размеры окна (пользователем или операционной системой), вызывается данная callback-функция void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // Убеждаемся, что окно просмотра соответствует новым размерам окна. // Обратите внимание, высота окна на Retina-дисплеях будет значительно больше, чем указано в программе glViewport(0, 0, width, height); } |
Задание №4
Используйте uniform-переменную в качестве третьего параметра функции mix(), чтобы изменить количество видимых текстур. Используйте клавиши со стрелками вверх и вниз, чтобы изменить видимость контейнера или смайлика.
Ответ №4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <stb_image.h> #include <learnopengl/shader_s.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow *window); // Константы const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; float mixValue = 0.2f; int main() { // glfw: инициализация и конфигурирование glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Раскомментируйте данную часть кода, если используете macOS /* #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif */ // glfw: создание окна GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // glad: загрузка всех указателей на OpenGL-функции if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // Компилирование нашей шейдерной программы Shader ourShader("4.5.texture.vs", "4.5.texture.fs"); // Указывание вершин (и буферов) и настройка вершинных атрибутов float vertices[] = { // координаты // цвета // текстурные координаты 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // верхняя правая 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // нижняя правая -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // нижняя левая -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // верхняя левая }; unsigned int indices[] = { 0, 1, 3, // первый треугольник 1, 2, 3 // второй треугольник }; unsigned int VBO, VAO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // Атрибут позиции glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // Атрибут цвета glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // Атрибут координат текстуры glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); // Загружаем и создаем текстуру unsigned int texture1, texture2; // Текстура №1 glGenTextures(1, &texture1); glBindTexture(GL_TEXTURE_2D, texture1); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // устанавливаем обтекание текстуры GL_REPEAT (метод обтекания по умолчанию) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // Устанавливаем параметры фильтрации текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Загружаем изображение, создаем текстуру и генерируем мипмапы int width, height, nrChannels; stbi_set_flip_vertically_on_load(true); // просим stb_image.h перевернуть загруженную текстуру относительно оси Y // FileSystem::getPath(...) - это часть GitHub-репозитория, чтобы мы могли найти файлы на любую IDE/платформу; замените этот путь на свой собственный путь к изображению unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Текстура №2 glGenTextures(1, &texture2); glBindTexture(GL_TEXTURE_2D, texture2); // Устанавливаем параметры обтекания текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // устанавливаем обтекание текстуры GL_REPEAT (метод обтекания по умолчанию) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // Устанавливаем параметры фильтрации текстуры glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Загружаем изображение, создаем текстуру и генерируем мипмапы data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0); if (data) { // Обратите внимание, что файл awesomeface.png имеет прозрачность и, следовательно, альфа-канал, поэтому обязательно сообщите OpenGL тип данных GL_RGBA glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); // Сообщаем OpenGL к какому текстурному юниту принадлежит каждый сэмплер (нужно указать один раз) ourShader.use(); // не забудьте активировать/использовать шейдер перед настройкой uniform-переменных! // Либо устанавливаем это вручную следующим образом glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // Либо устанавливаем через текстурный класс ourShader.setInt("texture2", 1); // Цикл рендеринга while (!glfwWindowShouldClose(window)) { // Обработка ввода processInput(window); // Рендеринг glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Выполняем привязку текстур на соответствующих текстурных юнитах glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); // Устанавливаем значение текстуры смешивания в шейдере ourShader.setFloat("mixValue", mixValue); // Контейнер рендеринга ourShader.use(); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // glfw: обмен содержимым front- и back-буферов. Отслеживание событий ввода/вывода (была ли нажата/отпущена кнопка, перемещен курсор мыши и т.п.) glfwSwapBuffers(window); glfwPollEvents(); } // Опционально: освобождаем все ресурсы, как только они выполнили свое предназначение glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); // glfw: завершение, освобождение всех ранее задействованных GLFW-ресурсов glfwTerminate(); return 0; } // Обработка всех событий ввода: запрос GLFW о нажатии/отпускании кнопки мыши в данном кадре и соответствующая обработка данных событий void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) { mixValue += 0.001f; // измените это значение соответствующим образом (может быть слишком медленным или слишком быстрым, в зависимости от вашего системного оборудования) if(mixValue >= 1.0f) mixValue = 1.0f; } if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) { mixValue -= 0.001f; // измените это значение соответствующим образом (может быть слишком медленным или слишком быстрым, в зависимости от вашего системного оборудования) if (mixValue <= 0.0f) mixValue = 0.0f; } } // glfw: всякий раз, когда изменяются размеры окна (пользователем или операционной системой), вызывается данная callback-функция void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // Убеждаемся, что окно просмотра соответствует новым размерам окна. // Обратите внимание, высота окна на Retina-дисплеях будет значительно больше, чем указано в программе glViewport(0, 0, width, height); } |
При смешивании второе изображение отображается как полностью черное. Почему это может произойти? Шейдеры не ломались вроде как.
Как оказалось проблема была в том, что программа не находила текстуру.
Не работает подключение библиотеки stb_image.h так, как описано в главе. Visual Studio просто отказывается видеть заголовочный файл stb_image.h. Пробовал и через "", и через <>
Еще есть вот такой вариант, для размещения 4-х смайликов на одном контейнере, не оптимизированный правда…
vertexShader.txt
fragmentShdaer.txt
У Вас тут текстура лица без альфа канала. Причем, кажется, на github тоже. Пришлось лезть в оригинальную статью
Спасибо, исправили.
Интересно, что 3 аргумент в в функции mix, если он отрицателен, создает эффект негатива при смешивании.
По 3му упражнению немного непонятен ответ, я имею ввиду что поменялось в коде крому увеличения масштаба текстуры?
PS: Решил задачу через шейдер.
Можно ли было во 2 задании в коде фрагментного шейдера изменить тело main на:
?
Или такой подход нежелателен?
Как вариант.