Что ж, научившись раскрашивать вершины треугольника разными цветами, перейдём к текстурированию.
Предыдущие части: Оптимизация и 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) для нашей текстуры.
С помощью glGenTextures() резервируем номерок для одной (1) текстуры. Как видите, это аналогично созданию буферов, только здесь своя функция для текстур.
Затем опять же знакомо привязываем номерок texture_handle к цели GL_TEXTURE_2D, и она становится текущей для выполнения дальнейших действий с ней.
С помощью монструозной функции 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-поверхности
Настройка поведения текстуры
Далее настраиваем, как должна вести себя текстура:
Семейство функций 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.
Если мы наложим сверху наш треугольник, то увидим, какой вершине треугольника соответствует какая текстурная координата. Нужно отметить, что размер и пропорции нашего треугольника абсолютно по барабану – мы буквально за уши притягиваем любую текстурную координату к любой его вершине. Всё, что между вершинами, будет интерполировано.
В прошлом выпуске мы научились хранить в одном буфере координаты вершин и их цвета. Сюда же мы можем добавить и текстурные координаты. Как нетрудно догадаться, они станут третьим по счёту входящим атрибутом для шейдера, который нужно настроить.
Но мы сейчас наоборот сократим количество входящих параметров до одного.
Сначала я чисто для удобства сделаю тип структуры Vertex:
В ней лежат: координаты вершины x, y и координаты текстуры tx, ty.
Теперь я могу описать треугольник не массивом отдельных чисел, а массивом структур типа Vertex:
Это не играет вообще никакой роли, но стало немножко структурированней, не правда ли? И теперь во всяких вычислениях длин мы можем писать не что-то типа 4 * sizeof(float), а просто sizeof(Vertex), и изменять набор полей структуры Vertex без лишних мучений.
Теперь я просто выкидываю весь код, который относился к параметру шейдера с индексом 1, и оставляю только индекс 0.
Меняем настройку VAO-объекта для параметра 0: буфер будет рубиться порциями по 4 элемента, а наш вершинный атрибут также будет длиной 4 элемента (vec4).
Иии.... всё. Теперь нужно исправить шейдеры.
Вершинный шейдер
Выбрасываем из него всё, что относилось к цвету. Теперь он получает только один параметр размером vec4.
Выходным параметром шейдера является переменная 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.
Так мы раскидали наш один входной параметр на два выходных.
Пиксельный шейдер
Выходным параметром пиксельного шейдера является цвет. Но теперь он получает цвет не от вершинного шейдера, а из текстуры, по текстурным координатам.
Для этого шейдеру требуется сэмплер. Это специальный uniform-объект, имеющий тип sampler2D, который привязывается к нашей текстуре. Вызывая функцию texture() с параметрами sampler и texCoord, мы через этот самый sampler получаем цвет пиксела по текстурным координатам texCoord.
Вопрос: где конкретно сэмплер привязывается к текстуре? Как выяснилось, нигде. Достаточно назначить текстуру текущей с помощью
glBindTexture(GL_TEXTURE_2D, texture_handle);
(что мы уже сделали), и завести в шейдере объект типа uniform sampler2D. И связывание произойдёт автоматически.
Да, обратите также внимание, что uniform-переменная progress перешла из вершинного в пиксельный шейдер, и теперь у нас будет пульсировать яркость текстуры.
Что ж, запускаем программу и видим:
Охохо, что же это такое! Текстура вниз головой, и цвета неправильные. Но радует, что она вообще есть!
Насчёт цветов – очевидно, что в нашей SDL-поверхности компоненты RGB идут не в том порядке, в каком их ожидает OpenGL. Исправим это, поменяв R и B местами прямо в поверхности:
Проверим ещё раз:
Значительно лучше. Но что делать с перевёрнутой текстурой? Проверим, правильно ли заданы текстурные координаты в массиве.
Да, всё верно. Получается, что руководство по OpenGL врёт. Координаты текстуры начинаются не в левом нижнем углу, а в левом верхнем. Но это вряд ли. Ведь тогда все бы об этом знали.
Второй вариант – картинка сама по себе была каким-то образом загружена вверх ногами. Так бывает именно с форматом BMP.
В общем, пока что я просто поменял текстурные координаты, перевернув их "вниз головой".
Смотрим опять:
Теперь всё в порядке. Конечно, этот случай требует расследования, но я это сделаю в другое время. Пока же можно сказать, что эксперимент с текстурами удался, и в следующий раз мы сможем сделать что-то новое.
Код для данного выпуска лежит на github в ветке textures.