О чем этот пример

Шейдеры — мощный инструмент для создания уникальных графических эффектов в играх, от психоделических искажений до плавных анимаций. В Phaser вы можете использовать GLSL-шейдеры для рендеринга непосредственно на канвас, не прибегая к сторонним библиотекам. Эта статья на примере официальной песочницы покажет, как создавать, управлять и анимировать кастомные шейдеры, интегрируя их в игровой процесс с помощью встроенных систем анимации и твинов.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('alien', 'assets/shaders/alien.png');
        this.load.image('sandbox', 'assets/shaders/sandbox.png');
        this.load.image('logo', 'assets/sprites/phaser-large.png');
    }

    create ()
    {
        const base = new Phaser.Display.BaseShader('wobble', frag);
        const base2 = new Phaser.Display.BaseShader('laser', frag2, null, { alpha: { type: '1f', value: 1 } });

        const shader1 = this.add.shader(base, 400, 400, 800, 800);
        const shader2 = this.add.shader(base2, 400, 400, 800, 800);

        const alien = this.add.image(400, 1200, 'alien');

        this.add.image(400, 350, 'logo');
        this.add.image(400, 450, 'sandbox').setScale(0.5);

        this.tweens.add({
            targets: alien,
            y: -400,
            duration: 4000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        const v = {
            alpha: 1
        };

        this.tweens.add({
            targets: v,
            delay: 2000,
            repeatDelay: 4000,
            hold: 4000,
            alpha: 0,
            duration: 2000,
            yoyo: true,
            repeat: -1,
            onUpdate: () => {
                shader2.setUniform('alpha.value', v.alpha);
            }
        });
    }
}

const game = new Phaser.Game({
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 800,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: Example
});

const frag = `
#ifdef GL_ES
precision mediump float;
#endif

#extension GL_OES_standard_derivatives : enable

uniform vec2 resolution;
uniform float time;

/*
* @author Hazsi (kinda)
*/
mat2 m(float a) {
    float c=cos(a), s=sin(a);
    return mat2(c,-s,s,c);
}

float map(vec3 p) {
    p.xz *= m(time * 0.4);p.xy*= m(time * 0.1);
    vec3 q = p * 2.0 + time;
    return length(p+vec3(sin(time * 0.7))) * log(length(p) + 1.0) + sin(q.x + sin(q.z + sin(q.y))) * 0.5 - 1.0;
}

void main() {
    vec2 a = gl_FragCoord.xy / resolution.y - vec2(0.9, 0.5);
    vec3 cl = vec3(0.0);
    float d = 2.5;

    for (int i = 0; i <= 5; i++) {
        vec3 p = vec3(0, 0, 4.0) + normalize(vec3(a, -1.0)) * d;
        float rz = map(p);
        float f =  clamp((rz - map(p + 0.1)) * 0.5, -0.1, 1.0);
        vec3 l = vec3(0.1, 0.3, 0.4) + vec3(5.0, 2.5, 3.0) * f;
        cl = cl * l + smoothstep(2.5, 0.0, rz) * 0.6 * l;
        d += min(rz, 1.0);
    }

    gl_FragColor = vec4(cl, 1.0);
}
`;

const frag2 = `
precision mediump float;

uniform float time;
uniform float alpha;
uniform vec2 resolution;

void main(void) {
	vec2 uPos = ( gl_FragCoord.xy / resolution.xy );
	uPos -= .5;
	vec3 color = vec3(-0.1);
	float vertColor = 0.0;
	for( float i = 0.; i < 6.5; ++i ) {
		uPos.y += sin( uPos.x*(i+1.0) + (time * 1.5)   +i/0.00000001 ) * 0.1;
		float fTemp = abs(1.0 / uPos.y / 100.0);
		vertColor += fTemp;
		color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,0.99)*1.5 );
	}
	gl_FragColor = vec4(color * alpha, alpha);
}
`;

Загрузка ресурсов и инициализация сцены

В методе preload мы загружаем три изображения, которые будут использоваться в сцене. Обратите внимание, что базовый URL задаётся через this.load.setBaseURL, что удобно для загрузки ресурсов из удалённого репозитория.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('alien', 'assets/shaders/alien.png');
    this.load.image('sandbox', 'assets/shaders/sandbox.png');
    this.load.image('logo', 'assets/sprites/phaser-large.png');
}

В методе create происходит основная настройка сцены. Именно здесь мы создаём и размещаем наши шейдеры и игровые объекты. Для корректной работы шейдеров важно, чтобы игра была инициализирована с рендерером Phaser.WEBGL, так как Phaser.CANVAS их не поддерживает.

Создание базовых шейдеров: BaseShader

Перед добавлением шейдера в сцену необходимо создать его описание — объект класса Phaser.Display.BaseShader. В конструктор передаётся уникальное имя, исходный код фрагментного шейдера (GLSL), опциональный вершинный шейдер и объект с пользовательскими uniform-переменными.

const base = new Phaser.Display.BaseShader('wobble', frag);
const base2 = new Phaser.Display.BaseShader('laser', frag2, null, { alpha: { type: '1f', value: 1 } });

Первый шейдер (wobble) использует только обязательные параметры. Второй (laser) определяет дополнительную uniform-переменную alpha типа float ('1f') с начальным значением 1. Это переменная, которую мы позже будем менять из кода игры для управления прозрачностью эффекта.

Добавление и размещение шейдеров в сцене

Созданные базовые шейдеры используются как фабрики для игровых объектов. Метод this.add.shader добавляет шейдер как полноценный игровой объект (Game Object) в дерево отображения сцены.

const shader1 = this.add.shader(base, 400, 400, 800, 800);
const shader2 = this.add.shader(base2, 400, 400, 800, 800);

Аргументы метода: базовый шейдер, координаты X и Y центра, ширина и высота области отрисовки. Оба шейдера в примере размещены в одном месте и имеют одинаковый размер, полностью покрывая игровую область 800x800 пикселей. Они будут отрисовываться в порядке добавления: shader1 — нижний слой, shader2 — верхний.

Анимация объектов и управление uniform-переменными

Phaser предоставляет мощную систему твинов для анимации свойств любых объектов. В примере анимируется два элемента: спрайт пришельца и пользовательская uniform-переменная alpha второго шейдера.

Анимация пришельца использует this.tweens.add, задавая движение по оси Y с эффектом yoyo и бесконечным повторением.

this.tweens.add({
    targets: alien,
    y: -400,
    duration: 4000,
    ease: 'Sine.easeInOut',
    yoyo: true,
    repeat: -1
});

Для анимации прозрачности шейдера создаётся промежуточный объект `v, чьё свойствоalphaтвинится от 1 до 0. В колбекеonUpdateтекущее значение передаётся в шейдер с помощью методаsetUniform`.

const v = { alpha: 1 };

this.tweens.add({
    targets: v,
    // ... настройки твина ...
    onUpdate: () => {
        shader2.setUniform('alpha.value', v.alpha);
    }
});

Ключевой момент: для обновления uniform-переменной используется полный путь 'alpha.value', где alpha — имя переменной, заданное при создании BaseShader, а value — её внутреннее поле.

Структура GLSL-шейдеров: время и разрешение

Исходный код шейдеров написан на GLSL. Phaser автоматически передаёт в них несколько стандартных uniform-переменных, которые можно использовать без объявления в конфиге BaseShader.

glsl
// Эти переменные предоставляет Phaser
uniform vec2 resolution; // Разрешение области шейдера
uniform float time;      // Время в миллисекундах с начала сцены

Первый шейдер (wobble) создаёт абстрактную, постоянно вращающуюся 3D-голограмму, используя функции sin, cos и time для анимации. Второй шейдер (laser) генерирует волнообразный лазерный луч, интенсивность которого управляется переменной alpha. Обратите внимание, как alpha используется в финальном вычислении цвета:

glsl
gl_FragColor = vec4(color * alpha, alpha);

Именно эта строчка позволяет нам управлять прозрачностью всего эффекта из основного кода игры.

Что попробовать дальше

Интеграция кастомных шейдеров в Phaser открывает безграничные возможности для визуального оформления игр. Вы можете создавать эффекты воды, огня, искажений пространства и динамических фонов, которые реагируют на игровые события. Для экспериментов попробуйте

  1. Привязать uniform-переменные (например, интенсивность эффекта) к здоровью игрока или скорости его движения
  2. Использовать шейдер в качестве маски или фильтра для группы спрайтов
  3. Комбинировать несколько шейдеров с разными режимами наложения (blendMode)