Игра про робота: Эффект секретной зоны
Всё же решил написать небольшую статью-обзор про шейдер, как писал в комментарии.
Постараюсь рассказать как оно работает и можно будет скачать для ГМС2 пример-проект.
Для начала определимся с тем, с чем работаем. Есть у нас игрок (точка) и фон\тайлы\слой, который обычно видимый, но при попадании туда игрока начинает исчезать.
Изначально я хотел добавить простой включил\выключил слой или максимум менять альфу. Но такое мне показалось слишком просто\скучно. Кажется, самим эффектом с альфой я вдохновился увидев в одном видео, где на ГМ собственно делают плавное изменение фона по альфе.
Микро-быстрый экскурс по шейдерным вещам: (описываю своими словами)
sampler2D - простыми словами это текстура.
uniform - это переменная, которую мы передаём в шейдер.
varying - это такие переменные шейдера, которые передаются из вершинного в пиксельный.
Вершинный шейдер обрабатывает вершины, а пиксельный собственно пиксели (выводит).
Но перед написанием шейдера накинем сам тест-проект. Для этого будет достаточно: 1 комнаты, 1 объекта, 3 слоёв, 2 спрайта, 1 тайлсет.
У слоёв в ГМ есть такая особенность — можно включить или выключить их отображение. А вот плавно сменить прозрачность нельзя, что и двигает нас на подвиги с шейдерами т.к. это один из способов реализации полупрозрачности целого слоя. Но как совместить шейдер и слой? Есть функция layer_shader, которая устанавливает собственно шейдер для отображения слоя, НО оно нам не подходит, потому что нельзя передать какие-либо значения в шейдер.
Помогут нам сразу две функции — layer_script_begin и layer_script_end. Они устанавливают функцию, которая будет вызываться при любом рисовании (если точнее — перед рисованием и после).
Один единственный объект
Начнём с объекта. Необходимо будет два события — Create и Step, но для наглядности будем рисовать и курсор в Draw (да, я решил использовать мышку, чтобы играться с визуалом).
Create:
// Найдём слой, который станет растворяться
secrets_layer = layer_get_id("Secrets");
// А потом ещё и тайлсет
secrets_tilemap = layer_tilemap_get_id(secrets_layer);
// Наша "новая" прозрачность слоя
secrets_alpha = 1;
// Псевдо-игрок
player_x = 0;
player_y = 0;
// Перед самим рисованием
layer_script_begin( secrets_layer,
function()
{
// Пока оставим пустым
});
// После рисования
layer_script_end( secrets_layer,
function()
{
// Пока оставим пустым
});
Step:
// Псевдоигрок на самом деле мышка
player_x = mouse_x;
player_y = mouse_y;
// Суть кода такова:
// находим тайл в точке игрока и если тайл не пустота(0),
// то снижаем альфу т.е. делаем прозрачным,
// иначе возвращаем альфу к состоянию непрозрачен.
var tile = tilemap_get_at_pixel(secrets_tilemap, player_x, player_y);
if( tile != 0 )
secrets_alpha = lerp( secrets_alpha, 0, 0.05 );
else
secrets_alpha = lerp( secrets_alpha, 1, 0.05 );
lerp — линейная интерпретация. Кажется это настолько же полезная функция, как и sin\cos. Простой код вроде x = lerp (x, target_x, factor) будет двигать x к target_x, сначала резко, но чем ближе, тем медленее т.к. lerp (v0, v1, t) можно описать как v0 + t * (v1 — v0), например.
Надо бы и комнату подготовить
Тут достаточно просто. Есть комната, должен быть тот-самый-объект (назвал его o_secret_logic) на слое Инстансоев\объектов. И собственно мы должны создать слой с тайлами, поставить там несколько для теста. У меня всё это вышло вот так:
Вернёмся к объекту и шейдеру
Ключевой момент. Давайте скопипастим напишем шейдер. Как вы знаете из пары моих слов и целого крутого поста Хейзера — есть пиксельный и вершинный шейдер. Ещё одно очевидное доказательство того, что ГеймМейкер это ТРИ ДЭ ДВИЖОК — там всё рисуется через растеризацию примитивов
Растеризация по сути это заполнение 2д пикселей 2д фигур. Да, 2д. Почему? Да потому что все эти выкрутасы в 3д с разным положением камеры, углов взгляда, перспективой перемножаются и в итоге мы получаем такие координаты, что они выглядят как 2д (но 3-тья компонента такие есть). Это как раз из того забавного факта, что 3д игры отображаются на 2д плоскость монитора и по итогу они не 3д. Но что-то меня унесло куда-то.
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;
varying vec4 v_vPosition;
void main()
{
vec4 object_space_pos = vec4(in_Position, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
// Передаём через varying из вершинного шейдера в пиксельный позицию нашей ВЕРШИНЫ
v_vPosition = gm_Matrices[MATRIX_WORLD] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
}
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec4 v_vPosition;
// Переменная текстура, которую мы передаём в шейдер
uniform sampler2D u_sMask;
const float c_fMaskSize = 128.0;
void main()
{
vec4 maskColor = texture2D( u_sMask, v_vPosition.xy/c_fMaskSize );
vec4 spriteColor = texture2D( gm_BaseTexture, v_vTexcoord );
gl_FragColor = mix(maskColor, spriteColor, 0.0);
}
Вот такой вот результат будет. Это промежуточный вариант, чтобы показать зачем нужен v_vPosition. А именно — через него мы делаем отображение «позиция в мире = текстурные координаты * множитель».
Хорошо, что вас томить — давайте просто доделаем этот пример.
Возвращаемся в объект и в событии создания добавляем получение индексов uniform-ов шейдера:
sh_uniform_progress = shader_get_uniform(shader_secret, "u_fProgress");
sh_uniform_position = shader_get_uniform(shader_secret, "u_vPlayerPosition");
sh_uniform_texture = shader_get_sampler_index(shader_secret, "u_sMask");
// Заполним наши функции рисования слоя:
// Before draw
layer_script_begin( secrets_layer,
function()
{
if( event_type == ev_draw && event_number == 0 )
{
with(o_secret_logic)
{
if( secrets_alpha < 1 )
{
// Установим шейдер текущим
shader_set(shader_secret);
// Установим пременные шейдера для текущего рисования
shader_set_uniform_f(sh_uniform_progress, secrets_alpha);
shader_set_uniform_f(sh_uniform_position, player_x, player_y );
// Данной функцией мы устанавливаем текстуру в uniform
texture_set_stage(sh_uniform_texture, sprite_get_texture(s_cloud_texture, 0));
gpu_set_tex_repeat_ext(sh_uniform_texture, true);
}
}
}
});
// After draw
layer_script_end( secrets_layer,
function()
{
if( event_type == ev_draw && event_number == 0 )
with(o_secret_logic)
if( secrets_alpha < 1 )
shader_reset();
});
В layer_script_* задаются функции, внутри которых есть event_type == ev_draw и event_number == 0 что же это такое? Переданные функции будет работать на все события рисования, а нам нужно только в обычном рисовании. Поэтому мы проверяем, что текущее событие это класс событий рисования (event_type) и что это обычное-классическое рисование (event_number=0), а не draw end, например.
Мы можем даже добавить проверку на view_current и включать шейдер лишь для конкретного включенного вида и получим вот такое:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec4 v_vPosition;
uniform vec2 u_vPlayerPosition;
uniform float u_fProgress;
uniform sampler2D u_sMask;
const float c_fVisionRadius = 128.0;
const float c_fMaskSize = 128.0;
void main()
{
// Пиксель нашей переменной-текстуры, на самом деле нужен только R канал
vec4 maskColor = texture2D( u_sMask, v_vPosition.xy/c_fMaskSize );
// Цвет пикселя из спрайтатайлсета
vec4 spriteColor = texture2D( gm_BaseTexture, v_vTexcoord );
// находим вектор от текущего ПИКСЕЛЯ*** до позиции игрока, которую мы передали
vec2 position = (v_vPosition.xy-u_vPlayerPosition);
// Основываясь на Прогрессе (альфа) и расстоянии между пикселем и игроком
// создаём переменную, которая хранит значение от 0 до 1.0.
float disolve = smoothstep( 0.0, c_fVisionRadius * (1.0-u_fProgress), distance(v_vPosition.xy, u_vPlayerPosition));
// Тут мы уже добавляем нашу маску-облака и disolve в степени 3, чтобы переход был более резкий.
float blend = smoothstep( 0.0, maskColor.r, pow(disolve,3.0) );
// Смешаем чёрно-прозрачный и изначальный цвет тайла
gl_FragColor = mix( vec4(0.0), spriteColor, blend);
// Alpha test. Если альфа очень небольшая, то просто не рисовать пиксель.
if( gl_FragColor.a < 0.01 ) discard;
}
***ПИКСЛЕЯ. Именно так. Из вершинного шейдера передали позицию вершины, но обрабатываем мы пиксель. Здесь вступает в силу интерполяция
Думаю не особо понятно про disolve, поэтому вот скриншот, если подать как выходной цвет (gl_FragColor) значение disolve:
А это если подать на выходной цвет blend:
И как полнейший итог вот так оно работает в свою силу:
На этом всё. Возможно, я что-то упустил. Ах да, ссылку на скачивание. Чтобы оно не затерялось решил выложить это на итч:
https://darkdes.itch.io/secret-area-shader-gamemakerstudio2
Спасибо за внимание!
- 03 декабря 2021, 23:26
- 013
Что-то получилось не сильно понятно. Даже я, знакомый с шейдерами в ГМ, и то понял с трудом. Но в целом прикольно.
Я тоже в своём гаминаторском проекте использовал layer_script_ функции. Рассчитывал, что оно применится на весь слой целиком при его рендере. И, наверное, с тайлами это так. С фонами и ассетами тоже, возможно. Но вот со слоем объектов - нет. Он просто вставляет эти скрипты до и после событий рисования.
Всех событий рисования всех объектов на слое, именно поэтому там и нужна вот эта проверка:
При этом не понятно, зачем вот это вот дальше:
Как я понимаю, это просто много раз включит шейдер (столько раз, сколько объектов o_secret_logic) и нужно только для считывания переменной, которую лучше сделать глобальной.
Так вот, т.к. layer_script_ просто вставляет эти скрипты в события отрисовки, то это чревато двумя вещами.
Во-первых, в draw событии таких объектов не должно быть шейдеров, иначе магия собьётся и shader_reset внутри draw события сработает раньше, чем оно же в layer_script_end.
Во-вторых, шейдер сработает только на рисуемую область, а учитывая, что шейдеры завязаны на входные данные, такие как размер рисуемой области - это чревато разного рода артефактами. У меня в проекте это были куски соседней по памяти текстуры, которые просачивались при отрисовке шейдера. Так же, шейдеры, которые искажают изображения могут начать резать картинку по краям.
Так что для объектов решение по старинке - это создать сурфейс, на него вызвать все события отрисовки объектов (одного слоя, или нескольких) и уже этот сурфейс рисовать с шейдером. Правда нужно учитывать сдвиг камеры, но это решается кастомными функциями с параметрами.
Сложно понятно от самой подачи? Я далеко не мастер кого-то учить.
Вроде как оно так и должно работать? Или ты имеешь ввиду, что для объектов он вставляет функции в draw begin и draw end?
Да, про проверку забыл указать, надо дописать.
Совершенно верно, это можно сделать глобальными переменными, даже без самого o_secret_logic объекта.
Точно! Вот знал же, что забыл что-то, что планировал рассказать. Я обновил в статье изображение "облаков", где показаны настройки спрайта. Он должен быть в отдельной текстурной странице ИЛИ, как я сделал для Стенок, нужно в шейдер передавать Uniform с UV прямоугольником где спрайт на атласе текстурном хранится. Проще конечно же первое.
С объектами действительно сложнее будет и вероятно делать только так, как ты и описал.
Наверное. Ты рассказываешь по большей части конкретный пример, чем концепцию шейдеров. Поэтому не читая мой большой пост въехать оч сложно, практически нереально как мне кажется.
Не только туда, вообще во все события рисования и draw_begin, и draw, и draw_end и draw_GUI и т.д. Считай что прям в код события вставляет. В начало и в конец. Ты потому и делаешь проверку на событие чтобы шейдер включился в нужный момент.
А вот с тайлами я ХЗ. Это не стандартный объект, поэтому там скорее всего скрипт назначается куда-то внутрь отрисовки самого слоя.
Более того, там нужно обязательно gpu_set_texrepeat где-то сделать, иначе оно не сработает. Кстати, может у меня поэтому вся эта шляпа с текстурами на границе что мне как раз нужно отключать повтор текстуры, хммм...
А, ну это да. Правда у меня не было объектов, у которых сразу множество всех draw_* событий (кроме объектов-контроллеров).
Не без этого конечно, да. Но я и предполагал, что читающий и желающий повторить уже хоть что-то понимает в этом всём.