Как работают шейдеры в GMS2? Как их писать и использовать? Что означают термины attribute, varying и uniform? Какой тип шейдера выбрать? Почему шейдер всегда состоит из двух файлов? Чем отличается вершинный шейдер от фрагментного? И причём здесь треугольники?
В этом гайде я опишу базовые принципы работы шейдеров в GMS2. Важно понимать, что существуют исключения, которые я не затрагиваю в рамках этого гайда, чтобы не усложнять повествование.
При изучении новых материалов я использую аналитический подход. В разных частях программы я устанавливаю отладочные функции и анализирую полученные значения. Однако с шейдерами такой подход не работает, поэтому их изучение какое-то время давалось мне с трудом.
Для написания простых шейдеров удобнее всего использовать GLSL ES. Благодаря этому шейдеры будут работать на любой целевой платформе, так как GMS2 автоматически их конвертирует. Язык GLSL ES отличается от GML, и в этом гайде я постараюсь внести ясность в его использование.
Шейдер состоит из двух частей: программы вершин и программы фрагментов.
Шейдер вершин — программа, выполняющая операции для всех вершин. На входе она получает параметры вершины, на выходе — её позицию на экране.
Шейдер фрагментов — программа, которая выполняется для всех пикселей, подлежащих отрисовке на экране. На входе она получает интерполированные значения из шейдера вершин, среди которых, как минимум, позиция точки на экране. На выходе задаётся значение цвета этой точки.
Каждый отрисовываемый на экране спрайт состоит из двух треугольников. У каждого треугольника по три вершины. Следовательно, на вход шейдеру вершин поступает шесть вершин для каждого отрисовываемого спрайта.
У каждой вершины есть позиция, текстурные координаты и цвет. Эта информация передаётся на вход в шейдер вершин как attribute. Результат выполнения функций для каждой вершины передаётся в шейдер фрагментов как varying. Задача шейдера фрагментов — после выполнения вычислений задать цвет точки на экране.
gl_Position — это переменная, значение которой должно быть задано в результате работы шейдера вершин. Она обозначает позицию точки на экране. Тип этой переменной — vec4, который, в свою очередь, состоит из четырёх значений типа float.
gl_FragColor — это переменная, которую необходимо задать в результате работы шейдера фрагментов. Она обозначает цвет пикселя на экране и также имеет тип vec4.
Когда я добавляю шейдер в GMS2, то вижу в нём всегда заранее написанный базовый код. Разберу его далее.
Код шейдера вершин:
Здесь, с 4 по 7 строку, определяются переменные входных параметров: in_Position — позиция вершины; in_Colour — цвет; in_TextureCoord — текстурная координата.
Немного о текстурных координатах. GMS2 на этапе сборки объединяет все мои изображения в одну большую текстурную карту. Чтобы отобразить нужное изображение на экране, необходимо знать его положение внутри этой карты. Это положение задаётся текстурными координатами.
В строках 9-10: v_vTexcoord — интерполированное значение текстурных координат; v_vColour — интерполированное значение цвета.
Мне нужно присвоить этим переменным значения, чтобы передать их в шейдер фрагментов.
varying обозначает переменную, которая будет интерполирована между значениями вершин. На входе 3 вершины для каждого треугольника. Логично было бы предположить, что на экране отобразятся 3 закрашенных пикселя. Однако благодаря интерполяции между этими вершинами я получаю закрашенный текстурированный треугольник.
В строке 12 начинается реализация основной функции, которая будет вызываться для каждой входящей вершины.
В строке 14 я преобразую vec3 позицию в vec4, чтобы в строке 15 умножить её на матрицу 4х4. GMS2 передаёт в шейдер несколько матриц 4х4, одна из которых представляет собой результат умножения матриц мира, вида и проекции. Именно она используется для вычисления конечной позиции точки на экране. Для 2D-графики это не так важно, но для 3D это необходимо.
В строках 17 и 18 я присваиваю входные значения цвета и текстурных координат переменным varying для их последующей интерполяции.
Код шейдера фрагментов:
В строках 4 и 5 я получаю на вход уже интерполированные значения varying из шейдера вершин. В строке 7 начинается реализация основной функции, которая будет вызываться для каждой отрисовываемой на экране точки.
Как же теперь это применить? Для этого есть две функции: shader_set() — включает переданный шейдер; shader_reset() — сбрасывает текущий шейдер, возвращаясь к дефолтному шейдеру GMS2.
Здесь, в событии Draw объекта, я включаю шейдер на 3-й строке и выключаю его на 5-й. Между ними происходит отрисовка спрайта. Так как шейдер базовый, я не вижу никаких изменений — отображается только мой спрайт.
Теперь я изменяю шейдер.
На выходе я задаю стандартный зелёный цвет с альфа-каналом, равным 1, на строке 10. Значения каждого канала цвета в шейдере задаются от 0 до 1, в отличие от GML, где цвет задаётся в диапазоне от 0 до 255. В результате я получаю залитый зелёный прямоугольник.
Значение альфы для этой картинки можно взять из текстуры, чтобы вместо прямоугольника отрисовывалась форма изображения.
В строке 9, с помощью интерполированных координат, я получаю цвет текущего фрагмента из текстуры и в строке 10 использую его альфу. Получается вот такой результат:
Теперь я хочу контролировать цвет из GML. Для этого нужно использовать константу uniform в шейдере. Это такие значения, которые остаются постоянными на протяжении работы шейдера.
Я изменяю код шейдера фрагментов следующим образом:
В строке 7 объявляется переменная u_color, а в строке 12 с её помощью формируется финальный цвет.
Вот как константа передаётся в шейдер:
У константы есть своё место в памяти. В строке 3 я получаю ссылку на это место, а в строке 6 использую эту ссылку, чтобы задать цвет. В результате получаю следующий эффект:
Теперь для демонстрации я сделаю простой эффект. Допустим, героя кто-то задел, и мне нужно показать, что он получил урон. Для этого я временно буду отрисовывать спрайт белым цветом.
Я изменил код фрагментного шейдера. Теперь в строке 7 вместо vec3 у меня используется float u_mix_value. В строке 12 с помощью этого значения и функции mix происходит интерполяция между цветом спрайта и белым цветом, после чего результат сохраняется в переменной vec4 mixed. В строке 13 через .rgb используются только первые три компонента цвета из переменной mixed, чтобы получить итоговый цвет.
Код в событии Draw тоже был изменён. В строке 3 я задаю глобальную переменную lerp_value. В четвёртой строке получаю локацию новой константы u_mix_value. В шестой строке по нажатию пробела для глобальной переменной lerp_value задаётся значение 1, а в строке 7 с помощью функции lerp оно интерполируется обратно в 0. В строке 10 я передаю интерполированное значение в шейдер, и получается вот такой результат.
Таким образом, я рассмотрел базовую информацию о работе шейдеров в GMS2 и надеюсь, что она окажется вам полезной.
Если статья вам понравилась, то можете ознакомиться со статьёй о трёхмерной графике: