Для начала, кратко ответим на простой вопрос: что такое шейдеры и для
чего они нужны?! Если не вдаваться в подробности, шейдеры – это
программы, служащие для описания визуального представления объекта.
Такое описание может содержать в себе информацию о свете, текстурах,
цвете, отражении и других параметрах. Следовательно, используя шейдеры
при создании игрового проекта, мы можем легко манипулировать
отображающейся на экране картинкой.
Game Maker Studio,
начиная с версии 1.2, так же включил в себя эту замечательную
возможность. Шейдеры здесь представлены отдельными ресурсами, а работа с
ними частично похожа на работу со скриптами. Добавляя новый шейдер в
папку "Shaders”, перед нами открывается вполне привычное окошко
редактирования кода. Разработчики немного облегчили нам задачу и уже
произвели первоначальные настройки, таким образом, как только шейдер
создан, он уже готов к работе. Но, само собой, без нашего вмешательства,
он ничего изменять не станет.
В окне редактирования шейдера, мы можем видеть две вкладки, проименованные как
vertex и
fragment.
Эти вкладки представляют собой два различных типа шейдеров: первый из
которых - вершинные шейдеры, служащие для работы с вершинами
многогранников. Второй тип – это фрагментные или пиксельные шейдеры,
которые, в свою очередь, отвечают за работу с фрагментами растрового
изображения, т.е. с пикселями. Для корректной работы шейдеров необходимо
задействовать оба этих типа, независимо от того, какие именно изменения
вы производите.
На первой вкладке мы можем видеть изначальные
атрибуты, представленные векторами с N компонентами. Они служат для
получения первоначального визуального представления об объекте и
получаются автоматически.
Код
attribute vec3 in_Position; //отвечает за координаты вершины (x,y,z)
attribute vec4 in_Colour; //отвечает за цвет вершины и ее прозрачность (r,g,b,a)
attribute vec2 in_TextureCoord; //отвечает за координаты текстуры, при ее натягивании на фигуру (u,v)
Выше, были объявлены входные параметры, а далее задаются те данные, что
мы получим на выходе, после всех необходимых преобразований. В
стандартном случае это новые координаты текстуры и ее цвет.
Код
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
Далее идет процесс вычисления значения положения вершины, вдаваться в
который мы пока не будем. И, наконец, получаем результат с новым цветом и
координатами, которые в нашем случае остаются изначальными.
Код
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
Таким образом, шейдер взял наш объект и преобразовал его, используя
изначальные параметры цвета и координаты, что, как понятно, не привело
ни к каким изменениям.
Теперь посмотрим, что происходит на вкладке
fragment, где идет работа с каждым отдельным пикселям.
Изначально, как вы уже, наверное, догадались, мы получаем те самые "новые” параметры координат и цветов.
Код
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
Далее, в переменную
gl_FragColor, служащую для аппаратной визуализации, передается информация о цвете в соответствующих координатах текущей текстуры.
Код
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
Этот созданный шейдер завершен и готов к работе, но при его
использовании, наш объект будет выглядеть точно так же, как выглядел
изначально. Ведь все, что сделал шейдер – это получил информацию о
текстуре и вернул ее, без каких-либо изменений.
Создание первого шейдера.
Пришло время эти самые изменения внести. Начнем, пожалуй, с самого
простого, на мой взгляд – эксперименты с цветом. Чтобы эти самые
эксперименты не были бесцельными, поставим конкретную задачу. Лично я
иногда сталкивался с необходимостью сделать цветное изображение
черно-белым непосредственно во время игрового процесса. Шейдеры позволят
нам совершить данную манипуляцию без особых усилий.
Приступим. В созданном шейдере переходим на вкладку
fragment, где в функции
main() пишем следующий код:
Код
vec4 color = texture2D(gm_BaseTexture, v_vTexcoord);
Эта строка задает атрибут
color, который представлен, как 4-компонентный вектор
vec4. Функция
texture2D,
как уже было замечено ранее, возвращает информацию о цвете текстуры в
соответствующих координатах. Таким образом, в color мы записываем
информацию о цвете пикселя (rgb) и его прозрачности (a). Теперь мы можем
работать с
color, обращаясь к его компонентам по вышеприведенным значениям.
Color.r – отвечает за красную составляющую цвета
Color.g - отвечает за зеленую составляющую цвета
Color.b - отвечает за синию составляющую цвета
Color.a - отвечает за прозрачность пикселя
Следует
сразу отметить, что цвета записываются не привычным диапазоном от 0 до
255, а от 0 до 1, где (0,0,0) - черный цвет, а (1,1,1) - белый.
На самом деле, вполне можно обойтись без этой строки, ведь все что мы сделали, это переписали в
color параметры из
texture2D(gm_BaseTexture, v_vTexcoord). Следовательно, теперь
texture2D(gm_BaseTexture, v_vTexcoord).r = color.r texture2D(gm_BaseTexture, v_vTexcoord).g = color.g и т.д.
Но, согласитесь, работать с
color намного удобнее, чем с аналогичной более громоздкой конструкцией.
Имеющихся данных нам вполне достаточно, чтобы сделать изображение
черно-белым. Есть несколько формул для подобного преобразования. Одна из
них выглядит следующим образом:
Y = (R + G + B ) / 3. Где на выходе
Y = R = G = B для обесцвеченного изображения.
Как видите, все данные для этой формулы у нас имеются, остается лишь записать все понятным для системы языком.
Код
Float Y = (color.r + color.b + color.g) / 3.0;
Таким образом, мы создаем переменную
Y,
которая будет хранить полученное значение для нового цвета. Данные у
нас представлены 32 битным числом с плавающей точкой, потому для
переменной мы задает тип
Float. Обратите внимание, что делим мы не просто на 3, а на 3.0, что так же обусловлено установленным типом данных.
Теперь, все что остается сделать, это передать на выход новое значение цвета для каждого пикселя.
Код
gl_FragColor = vec4(Y, Y, Y,1.0);
С переменной
gl_FragColor
мы уже сталкивались выше, занесем в нее 4-компонентный вектор с
полученными цветами и прозрачностью 1.0. Весь полученный код выглядит
следующим образом:
Код
void main()
{
vec4 color = texture2D(gm_BaseTexture, v_vTexcoord);
float Y = (color.r + color.b + color.g)/3.0;
gl_FragColor = vec4(Y,Y,Y,color.a);
}
Вот и готов наш шейдер, который сделает изображение черно-белым. Осталось лишь применить его к объекту.
Создадим тестовый объект и зададим ему цветной спрайт, который будем обесцвечивать. Далее, в событии
Draw этого объекта пропишем следующее:
Код
shader_set(shader0);
draw_sprite(sprite_index,0,x,y);
shader_reset();
Где функция
shader_set() активирует созданный нами шейдер
shader0. Далее, стандартный вывод спрайта нашего объекта и, наконец, функция
shader_reset(), которая отключает шейдер.
Теперь можно смело запустить приложение и любоваться нашим творением.
Однако, если в установленном для объекта спрайте присутствует
прозрачность, то после использования шейдера она пропадет. Почему это
происходит?! Все просто, ведь для всех новых цветов мы установили
прозрачность, равную 1.0. Если подумать, выход из этой ситуации лежит на
поверхности. Вспомним, что помимо информации о цвете, что мы получили
изначально и благополучно поместили в
color, там же мы храним и информацию о первоначальной прозрачности спрайта -
color.a. Эту же прозрачность мы можем передать на выходе, для соответствия изначальным параметрам:
Код
gl_FragColor = vec4(y,y,y,color.a);
Теперь наш спрайт имеет первоначальную прозрачность и новые цвета. Как я
упомянул, есть несколько вариантов подобного преобразования. Например,
вместо указанной выше формулы, можно использовать
Y = 0.3*R + 0.59*G + 0.11*B. Для закрепления полученной информации, можете самостоятельно сделать изображение черно-белым, используя данную формулу.
На этом первая часть завершается. Спасибо за внимание.