Игра про робота: Эффект секретной зоны
Всё же решил написать небольшую статью-обзор про шейдер, как писал в комментарии.
Постараюсь рассказать как оно работает и можно будет скачать для ГМС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
4 комментария