Урок 5: Текстурированный куб
Добро пожаловать на наш пятый урок. В этом уроке вы узнаете:
- Что такое UV-координаты
- Как самостоятельно загружать текстуры
- Как использовать их в OpenGL
- Что такое фильтрация и мип-маппинг и как их использовать
- Как загружать текстуры с помощью GLFW
- Что такое Alpha-канал
UV-координаты
Когда вы текстурируете какой-то объект, то вам необходимо как-то сообщить OpenGL, какая часть изображения прикрепляется к каждому треугольнику. Именно для этого и используются UV-координаты
Каждая вершина помимо позиции имеет несколько дополнительных полей, а также U и V. Эти координаты используются применительно к текстуре, как показано на рисунке:

Обратите внимание, как текстура искажается на треугольнике.
Загрузка Bitmap-изображений
Знание формата файлов BMP не является критичным, так как многие библиотеки могут сделать загрузку за вас. Однако, чтобы лучше понимать то, что происходит в таких библиотеках мы разберем ручную загрузку.
Объявляем функцию для загрузки изображений:
Вызываться она будет так:
Теперь перейдем непосредственно к чтению файла.
Для начала, нам необходимы некоторые данные. Эти переменные будут установлены когда мы будем читать файл:
Первым, в BMP-файлах идет заголовок, размером в 54 байта. Он содержит информацию о том, что файл действительно является файлом BMP, размер изображение, количество бит на пиксель и т. п., поэтому читаем его:
Заголовок всегда начинается с букв BM. Вы можете открыть файл в HEX-редакторе и убедиться в этом самостоятельно, а можете посмотреть на наш скриншот:

Итак, мы проверяем первые два байта и если они не являются буквами “BM”, то файл не является BMP-файлом или испорчен:
Теперь мы читаем размер изображения, смещение данных изображения в файле и т. п.:
Проверим и исправим полученные значения:
Теперь, так как мы знаем размер изображения, то можем выделить область памяти, в которую поместим данные:
Мы вплотную подошли к части, касающейся OpenGL. Создание текстур очень похоже на создание вершинных буферов:
- Создайте текстуру
- Привяжите ее
- Заполните
- Сконфигурируйте
GL_RGB в glTextImage2D указывает на то, что мы работает с 3х компонентным цветом. А GL_BGR указывает на то, как данные представлены в памяти. На самом деле в BMP-файлах цветовые данные хранятся не в RGB, а в BGR (если быть точным, то это связано с тем, как хранятся числа в памяти), поэтому необходимо сообщить об этом OpenGL:
Последние две строки мы поясним позднее, а пока в части C++ мы должны использовать нашу функцию для загрузки текстуры:
**Очень важное замечание: **используйте текстуры с шириной и высотой степени двойки! То есть:
- Хорошие: 128128, 256256, 10241024, 2*2…
- Плохие: 127128, 35, …
- Приемлемые: 128*256
Использование текстуры в OpenGL
Что же, давайте посмотрим на наш Фрагментный шейдер:
- Фрагментному шейдеру требуются UV-координаты. Это понятно.
- Также, ему необходим “sampler2D”, чтобы знать, с какой текстурой работать (вы можете получить доступ к нескольким текстурам в одном шейдере т. н. мультитекстурирование)
- И наконец, доступ к текстуре завершается вызовом texture(), который возвращает vec4 (R, G, B, A). A-компоненту мы разберем немного позднее.
Вершинный шейдер также прост. Все, что мы делаем — это передаем полученные UV-координаты в фрагментный шейдер:
Помните “layout(location = 1) in vec3 vertexColor” из Урока 4? Здесь мы делаем абсолютно тоже самое, только вместо передачи буфера с цветом каждой вершины мы будем передавать буфер с UV-координатами каждой вершины:
Указанные UV-координаты относятся к такой модели:

Остальное очевидно. Мы создаем буфер, привязываем его, заполняем, настраиваем и выводим Буфер Вершин как обычно. Только будьте осторожны, так как в glVertexAttribPointer для буфера текстурных координат второй параметр (размер) будет не 3, а 2.
И вот такой результат мы получим:

в увеличенном варианте:

Фильтрация и мип-маппинг.
Как вы можете видеть на скриншоте выше, качество текстуры не очень хорошее. Это потому, что в нашей процедуре загрузки BMP-изображения (loadBMP_custom) мы указали:
Это означает, что в нашем фрагментном шейдере, texture() возвращает строго тексель, который находится по указанным текстурным координатам:

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

Это будет выглядить значительно лучше и используется часто, но если вы хотите очень высокого качества, то вам понадобится анизотропная фильтрация, которая работает несколько медленнее.
Аппроксимирует часть изображения, которая действительно видна через фрагмент. К примеру, если указанная текстура просматривается сбоку и немного повернута, то анизотропная фильтрация будет вычислять цвет, который находится в синем прямоугольнике, с помощью фиксированного количество сэмплов (Уровень анизотропии) вдоль его направления:

И линейная, и анизотропная фильтрация имеют недостаток. Если текстура просматривается с большого расстояния, то смешивать 4 текселя будет недостаточно. То есть, если ваша 3D модель находится так далеко, что занимает на экране всего 1 фрагмент, то фильный цвет фрагмента будет являться средним всех текселей текстуры. Естественно, это не реализовано из-за соображений производительности. Для этой цели существует так называемый мип-маппинг:

- При инициализации вы уменьшаете масштаб текстуры до тех пор, пока не получите изображение 1х1 (которое по сути будет являться средним значением всех текселей текстуры)
- Когда вы выводите объект, то вы выбираете тот мип-мап, который наиболее приемлем в данной ситуации.
- Вы применяете к этому мип-мапу фильтрацию
- А для большего качества вы можете использовать 2 мип-мапа и смешать результат.
К счастью для нас, все это делается очень просто с помощью OpenGL:
Загрузка текстур с помощью GLFW
Наша процедура loadBMP_custom великолепна, так как мы сделали ее сами, но использование специальных библиотек может быть предпочтительнее (в конечном итоге мы в своей процедуре многое не учли). GLFW может сделать это лучше (но только для TGA-файлов):
Сжатые текстуры
На этом шаге вы наверное хотите узнать, как же все-таки загружать JPEG файлы вместо TGA?
Короткий ответ: даже не думайте об этом. Есть идея получше.
##Создание сжатых текстур
- Скачайте The Compressonator, утилита от ATI
- Загрузите в нее текстуру, размер которой является степенью двойки
- Сожмите ее в DXT1, DXT3 или в DXT5 (о разнице между форматами можете почитать на Wikipedia)

- Создайте мипмапы, чтобы не создавать их во время выполнения программы.
- Экспортируйте это как .DDS файл
После этих шагов вы имеете сжатое изображение, которое прямо совместимо с GPU. И когда вы вызовите texture() в шейдере, то текстура будет распакована на лету. Это может показаться более медленным, однако это требует гораздо меньше памяти, а значит пересылаемых данных будет меньше. Пересылка данных всегда будет дорогой операцией, в то время как декомпрессия является практически бесплатной. Как правило, использование сжатия текстур повышает быстродействие на 20%.
##Использование сжатой текстуры
Теперь перейдем непосредственно к загрузке нашей сжатой текстуры. Процедура будет очень похожа на загрузку BMP, с тем исключением, что заголовок файла будет организован немного иначе:
После заголовку идут данные, в которые входят все уровни мип-мап. К слову, мы можем прочитать их все сразу:
Сделано. Так как мы можем использовать 3 разных формата (DXT1, DXT3, DXT5), то необходимо в зависимости от флага “fourCC”, сказать OpenGL о формате данных.
Создание текстуры выполняется как обычно:
Следующим шагом мы загружаем мип-мапы:
DXT компрессия пришла к нам из DirectX, где координатная текстура V является инвертированной по сравнению с OpenGL. Поэтому, если вы используете сжатые текстуры, то вам необходимо использовать (coord.u, 1.0 — coord.v), чтобы исправить тексель. Вы можете выполнять это как при экспорте текстуры, так и в загрузчике или в шейдере.
Заключение
В данном уроке вы узнали как создавать, загружать и использовать текстуры в OpenGL.
Стоит отметить, что в своих проектах мы настоятельно рекомендуем вам использовать только сжатые текстуры, так как они занимают меньше места, быстрее загружаются и используются. Для этих целей можете также использовать The Compressonator.
Системы координат OpenGL

Разработчикам также была облегчена работа с матрицами преобразований. В частности разработчик мог использовать стек матриц в различных режимах работы с матрицами. Режим работы задавался с помощью функции glMatrixMode, например можно было задать режим GL_MODELVIEW или GL_PROJECTION, который давал доступ к модельно-видовой (ModelView) и проекционной (Projection) матрицам, соответственно. Для работы со стеком матриц предназначались функции glPushMatrix и glPopMatrix. Однако в OpenGL версии 3 и выше все эти функции были объявлены устаревшими и исключены из API.
В OpenGL 3.3 за первые два пункта преобразований координат теперь отвечает разработчик: используя шейдерную программу он должен перевести локальные координаты объекта в однородные. Каким образом он это сделает — неважно, он может эмулировать старую схему или придумать что-то свое, главное получить однородные координаты.
Однородные координаты называются так неспроста, они переводят все имеющиеся координаты в единое пространство, ограниченное по всем осям системы координат параметром W. В итоге, после перевода однородных координат в нормализованные, любые координаты вершин, которые необходимо отобразить, находятся в пределах [-1, 1] по всем трем осям нормализованной системы координат. Если координаты вершины не попадают в этот интервал — вершина отбрасывается.
- Трансформация координат матрицей объекта (ModelMatrix)
- Трансформация координат матрицей наблюдателя (ViewMatrix)
Такое разбиение очень удобно — у каждого объекта есть матрица преобразований, которая переводит локальную систему координат в мировую и есть наблюдатель, положение которого задано в мировых координатах. Таким образом мы можем оперировать неким дополнительным пространством — мировым (world), это пространство служит для описания сцены и расположения на ней объектов и наблюдателей.
Терминология обозначения матриц и систем координат одна из многих причин путаниц при работе с разными графическими API, например в DirectX матрица перевода локальной системы координат объекта в мировые называется не ModelMatrix, а WorldMatrix.
Кстати, в OpenGL правая система координат, т.е. построенная по правилу правой руки. В начальном положении ось Z направлена на нас, ось X направлена вправо и ось Y направлена вверх, относительно экрана монитора.
Работа с матрицами
В этом и последующих уроках будет использоваться подход к матрицам описанный выше, для описания объекта на сцене необходимо будет задать три матрицы: ModelMatrix, ViewMatrix и ProjectionMatrix.
Матрицы ViewMatrix и ProjectionMatrix обычно привязаны к специальному объекту — камере. Матрица ViewMatrix меняется если наблюдатель изменил свое положение или направление взгляда. Матрица ProjectionMatrix меняется гораздо реже, например при переключении из меню приложения к сцене и т.п.
Матрица ModelMatrix закреплена за объектом, она меняется при движении объекта или его вращении.
- Указать в шейдере тип принимаемой матрицы — uniform matN matrixName
- После сборки шейдерной программ (link) получить индекс юниформа (location)
- Передать матрицу в шейдерную программу используя одну из функций glUniformMatrix
Передавать все три матрицы в шейдерную программу весьма расточительно — необходимо будет вычислять матрицу преобразования локальных координат в видовые на каждую вершину объекта, поэтому обычно итоговую матрицу преобразований вычисляют в самом приложении, отдельно для каждого объекта, и передают ее в шейдерную программу перед выводом этого объекта на экран.
В этом уроке мы будем выводить на экран вращающийся куб, для этого нам потребуется рассчитать матрицу вращения ModelMatrix:
Функция Matrix4Rotation строит матрицу вращения для углов поворота по трем осям координат (x, y, z). Если вас интересует подробное описание построение матрицы поворота, то рекомендую ознакомиться с детальным The Matrix and Quaternions FAQ. Отдельно на этой теме мы в уроке останавливаться не будем.
Помимо этого мы отодвинем точку наблюдения от куба, чтобы видеть его полностью, для этого нам понадобиться построить матрицу переноса ViewMatrix:
Функция Matrix4Translation строит матрицу переноса по трем координатным осям (x, y, z). Опять же детальную информацию вы можете получить в The Matrix and Quaternions FAQ.
Также нам понадобиться матрица проекции ProjectionMatrix, о которой было рассказано в предыдущем уроке. Также как и там мы будем использовать перспективную матрицу проекции, построенную при помощи функции Matrix4Perspective.
Однако это еще не все, как уже было сказано выше, нам необходимо рассчитать итоговую матрицу преобразований координат прежде чем передавать ее в шейдерную программу, для композиции трансформаций используется матричное умножение:
Функция Matrix4Mul перемножает матрицы A и B и помещает результат в матрицу M. Надеюсь как выполняется матричное умножение знает каждый из вас и подробно объяснить смысл производимых в функции действий вам не надо 🙂
Стоит также вспомнить, что умножение матриц операция ассоциативная, т.е. A*(B*C) = (A*B)*C, поэтому мы можем вычислить часть выражения заранее, а часть в случае необходимости.
В данном уроке мы не будем менять матрицу проекции и матрицу наблюдателя, поэтому их мы можем объединить заранее и не вычислять постоянно:
Для вращения куба нам необходимо построить матрицу вращения:
Для передачи матрицы в шейдерную программу нам необходим узнать ее индекс:
После получения итоговой матрицы и ее индекса мы можем передать ее в шейдерную программу:
Очень частая проблема, которая приводит к путанице в среде разработчиков графических приложений — формат самих матриц и формат расположения их в памяти.
Формат матрицы можно определить по формату векторов, это либо row-vector матрица (используется вектор-строка), либо column-vector матрица (используется вектор-столбец). Соответственно при использовании row-vector необходимо вектор умножать на матрицу, а при использовании column-vector умножать матрицу на вектор.
По формату расположения в памяти матрицы также делятся на два типа: row-major матрица (матрица в памяти записана по строкам) и column-major матрица (матрица в памяти записана по столбцам). Стоит отметить, что переход между форматами осуществляется путем транспонирования матрицы.
По историческим причинам OpenGL использует column-major матрицы, соответственно ожидая на входе в свои функции именно этот формат, однако такой формат неудобен, поэтому в этих уроках используется row-major формат расположения в памяти, а при передачи в шейдерную программу функцией glUniformMatrix устанавливается флаг transpose, которые переводит матрицу в column-major формат.
Загрузка и создание текстуры
Тема текстур в OpenGL очень объемная, существуют различные типа текстур для разных целей. В этом уроке мы создадим самую простую и наиболее часто используемую текстуру — двумерную текстуру, в OpenGL такая текстура обозначается как GL_TEXTURE_2D.
Изображение для текстуры в этом уроке хранится в формате TGA. Этот очень простой формат, местами даже проще чем BMP. Для загрузки изображения из этого формата и создания текстуры используется функция TextureCreateFromTGA:
Добавить к кооментариям исходного кода приведенного выше практически нечего, используя функцию TextureCreateFromTGA полчаем готовую для исопльзования текстуру:
Функция glActiveTexture задает активный текстурный юнит видеокарты, который мы будем использовать. Привязка текстуры к активному текстурному юниту осуществляется функцией glBindTexture.
Теперь необходимо сообщить шейдерной программе в каком текстурном юните распологается наша текстура:
Теперь шейдерная программа знает какой текстурный юнит использовать. Максимальное количество доступных текстурных юнитов можно узнать используя функцию glGetIntegerv с параметром GL_MAX_TEXTURE_IMAGE_UNITS.
Геометрия куба и буферы для ее хранения
Также ккак и в прошлом уроке для тругольника мы вручную зададим геометрию куба, однако в этот раз не будем задавать цвет вершин, а назначим им текстурные координаты:
Конечно мы можем рисовать куб используя прямоугольники, однак обычно геометрия моделей представлена в виде граней (face), которые представляют собой треугольники. Для того чтобы нарисовать куб треугольниками не дублируя вершин на понадобиться индексный буфер, в котором последовательно идут по три индекса для каждого из треугольников, котоыре мы будем рисовать:
Индексы определяют номера вершин в вершинном буфере, которые будет использоваться при выводе объекта на экран.
Теперь необходимо создать VBO для хранения вершин куба и индексного буфера, также не забываем про VAO, в котором будут хранится все созданные связи между VBO и вершинными атрибутами в шейдерной программе:
Обратите внмание, что вершинный буфер задается параметром GL_ARRAY_BUFFER, а индексный буфер задается параметром GL_ELEMENT_ARRAY_BUFFER.
С VAO может быть связан только один индексный буфер, в отличии от вершинных буферов, которых может быть несколько. Таким образом предопагается, что VAO будет использоваться для вывода на экран одной модели, в данном случае — куба.
OpenGL: How do I apply a texture to this cube? [duplicate]
I’m trying to learn OpenGL, and I’ve been going through a lot of tutorials on loading a texture, but every single one seems to miss the most important step: how do I actually put a texture on something?
I’m using Python for this, and here is my function that loads the texture:
And here is the function that loads my Cube:
And here’s the main loop:
But the cube is untextured. I don’t know how to actually use the loaded texture on the cube, and every texture tutorial I find takes me as far as the loadTexture function without actually telling me how to use it. Where do I call it? What do I do with texid?