Найти тему
ZDG

OpenGL #6: Текстуры

Оглавление

Что ж, научившись раскрашивать вершины треугольника разными цветами, перейдём к текстурированию.

Предыдущие части: Оптимизация и uniform, Раскрашиваем вершины, Наш первый зелёный шейдер, Рисуем треугольник, Приступим, помолясь, OpenGL на пальцах

Чтобы закрасить треугольник текстурой, нужно:

  • Загрузить текстуру из файла
  • Переслать текстуру в OpenGL
  • Назначить вершинам треугольника текстурные координаты
  • Переписать вершинный и пиксельный шейдеры

Загрузка текстуры из файла

Здесь нет ничего OpenGL-специфичного, нужно просто как угодно получить в памяти массив, заполненный RGB-значениями пикселов. Для загрузки картинок можно использовать тонну самых разных библиотек. Так как данный проект базируется на SDL2, я буду пользоваться его функциями, не привлекая лишнего.

Текстурой будет картинка с Моной Лизой из проекта генетического алгоритма.

Её размер 256*256 пикселов, так как текстуры должны иметь размер, кратный степени двойки (8, 16, 32 и т.д.) Файловый формат – BMP, потому что чистый SDL умеет только в такой.

Загрузка делается одной строчкой (и это уже делалось в проекте про Сапёра):

SDL_Surface *bmpSurface = SDL_LoadBMP("textures/mona-256.bmp");

В результате мы получаем bmpSurface – переменную-указатель на поверхность (буфер памяти) SDL_Surface.

В этой поверхности хранятся пикселы картинки в виде RGB RGB RGB и т.д. Вообще говоря, формат поверхности будет зависеть от того, какой формат у оригинальной картинки (или нет?) Для полного счастья нужно убедиться, что мы получили именно тот формат, который нам нужен, а если нет, то преобразовать его в другой. Но пока не будем на этом застревать, так как это чисто рутинный вопрос.

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

Как и ранее для других объектов, нам нужно получить от OpenGL "номерок" (handle) для нашей текстуры.

-2

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

Затем опять же знакомо привязываем номерок texture_handle к цели GL_TEXTURE_2D, и она становится текущей для выполнения дальнейших действий с ней.

-3

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

  • GL_TEXTURE_2D – цель, к которой сейчас привязан номерок текстуры
  • 0 – уровень детализации (мипмаппинга), для которого мы загружаем текстуру (про это потом).
  • GL_RGB – формат текстуры. В нашем случае это соответствует трём компонентам RGB.
  • bmpSurface->w, bmpSurface->h – ширина и высота текстуры, которые мы берём из ширины и высоты SDL-поверхности
  • 0"какая-то херня для совместимости", то есть да, именно так
  • GL_RGB – формат нашего изображения
  • GL_UNSIGNED_BYTE – тип цветовых компонентов в нашем изображении (байты 0..255)
  • bmpSurface->pixels – указатель непосредственно на пикселы в SDL-поверхности

Настройка поведения текстуры

Далее настраиваем, как должна вести себя текстура:

-4

Семейство функций glTextParameter*() настраивает параметры текстур, а окончание "i" говорит о том, что мы устанавливаем целочисленные (int) параметры, поэтому не удивляйтесь, что имя такое странное.

Первым делом устанавливаем параметры GL_TEXTURE_WRAP_S и GL_TEXTURE_WRAP_T. Они отвечают за то, как будет рисоваться текстура, если текстурные координаты выходят за её пределы. Текстуру можно просто повторять, или повторять с зеркальным отражением, или повторять только её край, и т.д. В нашем случае мы выбираем GL_REPEAT, т.е. повторение, но на самом деле нам пока без разницы. Также обратите внимание, что повторение задаётся вдоль оси X и вдоль оси Y, но в текстуре эти оси называются S и T.

Далее настраиваем фильтрацию для уменьшения (GL_TEXTURE_MIN_FILTER) и увеличения (GL_TEXTURE_MAG_FILTER) масштаба текстуры. Ставим оба параметра в GL_NEAREST. Эта самая простая фильтрация, которая приводит к "пиксельному" виду текстур. Для получения сглаженного вида можно использовать GL_LINEAR. Другие опции включают использование мипмапов и пр., но пока не паримся и движемся дальше.

Текстурные координаты

Текстурные координаты указываются для каждой вершины треугольника. Текстура имеет собственные координаты, которые в нормализованном виде меняются от 0 до 1.

-5

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

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

Но мы сейчас наоборот сократим количество входящих параметров до одного.

Сначала я чисто для удобства сделаю тип структуры Vertex:

-6

В ней лежат: координаты вершины x, y и координаты текстуры tx, ty.

Теперь я могу описать треугольник не массивом отдельных чисел, а массивом структур типа Vertex:

-7

Это не играет вообще никакой роли, но стало немножко структурированней, не правда ли? И теперь во всяких вычислениях длин мы можем писать не что-то типа 4 * sizeof(float), а просто sizeof(Vertex), и изменять набор полей структуры Vertex без лишних мучений.

Теперь я просто выкидываю весь код, который относился к параметру шейдера с индексом 1, и оставляю только индекс 0.

-8

Меняем настройку VAO-объекта для параметра 0: буфер будет рубиться порциями по 4 элемента, а наш вершинный атрибут также будет длиной 4 элемента (vec4).

Иии.... всё. Теперь нужно исправить шейдеры.

Вершинный шейдер

Выбрасываем из него всё, что относилось к цвету. Теперь он получает только один параметр размером vec4.

-9

Выходным параметром шейдера является переменная texCoord типа vec2. Текстурированием будет заниматься пиксельный шейдер, а всё, что делает вершинный – добывает vec2 из входного vec4. Обратите внимание на запись

Vertex.zw

Это один из вариантов разрешённого в GLSL синтаксиса. Тип vec4 – структура с 4 полями. Эти поля могут называться следующими способами:

  • x, y, z, w
  • r, g, b, a
  • s, t, p, q

Абсолютно никакой разницы между этими названиями нет. Просто, когда мы работаем с координатами, нам удобно использовать xyz, а когда с цветом, то rgb, и т.д.

В общем, Vertex.zw – это два последних поля в vec4. И эти два последних поля переходят в vec2.

А Vertex.xy – это два первых поля, которые в выходном (встроенном) параметре gl_Position расширяются до vec4.

Так мы раскидали наш один входной параметр на два выходных.

Пиксельный шейдер

-10

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

Для этого шейдеру требуется сэмплер. Это специальный uniform-объект, имеющий тип sampler2D, который привязывается к нашей текстуре. Вызывая функцию texture() с параметрами sampler и texCoord, мы через этот самый sampler получаем цвет пиксела по текстурным координатам texCoord.

Вопрос: где конкретно сэмплер привязывается к текстуре? Как выяснилось, нигде. Достаточно назначить текстуру текущей с помощью

glBindTexture(GL_TEXTURE_2D, texture_handle);

(что мы уже сделали), и завести в шейдере объект типа uniform sampler2D. И связывание произойдёт автоматически.

Да, обратите также внимание, что uniform-переменная progress перешла из вершинного в пиксельный шейдер, и теперь у нас будет пульсировать яркость текстуры.

Что ж, запускаем программу и видим:

-11

Охохо, что же это такое! Текстура вниз головой, и цвета неправильные. Но радует, что она вообще есть!

Насчёт цветов – очевидно, что в нашей SDL-поверхности компоненты RGB идут не в том порядке, в каком их ожидает OpenGL. Исправим это, поменяв R и B местами прямо в поверхности:

-12

Проверим ещё раз:

-13

Значительно лучше. Но что делать с перевёрнутой текстурой? Проверим, правильно ли заданы текстурные координаты в массиве.

-14

Да, всё верно. Получается, что руководство по OpenGL врёт. Координаты текстуры начинаются не в левом нижнем углу, а в левом верхнем. Но это вряд ли. Ведь тогда все бы об этом знали.

Второй вариант – картинка сама по себе была каким-то образом загружена вверх ногами. Так бывает именно с форматом BMP.

В общем, пока что я просто поменял текстурные координаты, перевернув их "вниз головой".

-15

Смотрим опять:

-16

Теперь всё в порядке. Конечно, этот случай требует расследования, но я это сделаю в другое время. Пока же можно сказать, что эксперимент с текстурами удался, и в следующий раз мы сможем сделать что-то новое.

Код для данного выпуска лежит на github в ветке textures.