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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.glsl('wave', 'assets/shaders/shader1.frag');
        this.load.image('pic', 'assets/pics/sao-sinon.png');
        this.load.image('bg', 'assets/pics/purple-dots.png');
    }

    create ()
    {
        this.add.image(400, 300, 'bg');

        const shader = this.make.shader({
            config: {
                name: 'wave',
                fragmentKey: 'wave',
                initialUniforms: {
                    resolution: [ 800, 600 ]
                },
                setupUniforms: (setUniform, drawingContext) => {
                    setUniform('time', this.game.loop.getDuration());
                }
            },
            x: 400,
            y: 300,
            width: 800,
            height: 600,
            add: false
        });

        //  Apply the mask to this image
        this.add.image(400, 300, 'pic').enableFilters().filters.external.addMask(shader);
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка ресурсов: шейдеры и изображения

В методе preload загружаются все необходимые ресурсы. Ключевой момент — загрузка фрагментного шейдера с помощью метода this.load.glsl. Phaser обрабатывает шейдер как обычный ассет.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.glsl('wave', 'assets/shaders/shader1.frag');
    this.load.image('pic', 'assets/pics/sao-sinon.png');
    this.load.image('bg', 'assets/pics/purple-dots.png');
}

Здесь 'wave' — это ключ, под которым шейдер будет доступен в коде. Также загружаются два изображения: основное ('pic'), к которому будет применена маска, и фоновое ('bg').

Создание шейдера как объекта сцены

Маской в Phaser может выступать любой объект, в том числе шейдер. Мы создаем шейдер с помощью фабрики this.make.shader. Важно отметить параметр add: false — он означает, что шейдер не будет автоматически добавлен на дисплейный список сцены как визуальный объект, но останется в памяти как ресурс для маски.

const shader = this.make.shader({
    config: {
        name: 'wave',
        fragmentKey: 'wave',
        initialUniforms: {
            resolution: [ 800, 600 ]
        },
        setupUniforms: (setUniform, drawingContext) => {
            setUniform('time', this.game.loop.getDuration());
        }
    },
    x: 400,
    y: 300,
    width: 800,
    height: 600,
    add: false
});

В конфигурации указываем ключ загруженного шейдера (fragmentKey). initialUniforms задает начальное значение переменной resolution для шейдера. Функция setupUniforms вызывается каждый кадр и обновляет uniform-переменную time, передавая в шейдер текущее время игры в миллисекундах. Это и создает анимацию маски.

Применение шейдерной маски к изображению

Чтобы наложить маску, сначала необходимо создать целевое изображение и включить для него систему фильтров с помощью метода .enableFilters(). После этого к массиву внешних фильтров (filters.external) объекта добавляется наша шейдерная маска.

this.add.image(400, 300, 'pic').enableFilters().filters.external.addMask(shader);

Магия происходит в вызове filters.external.addMask(shader). Шейдер, созданный на предыдущем шаге, используется не для отрисовки цвета, а как маска. Области, где шейдер выводит белый цвет (или значения, близкие к 1.0), будут полностью прозрачными для маски (показывая исходное изображение). Черные области (значения, близкие к 0.0) — полностью замаскированными (прозрачными, показывая фон). Промежуточные значения создают полупрозрачность.

Настройка рендерера и запуск игры

Для работы с шейдерами и фильтрами в Phaser 3 обязателен рендерер WebGL.

const config = {
    type: Phaser.WEBGL, // Важно: используем WEBGL, а не AUTO или CANVAS
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Указание type: Phaser.WEBGL в конфигурации игры критически важно. Фильтры и шейдерные маски не работают в режиме рендеринга на Canvas (2D).

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

Использование шейдера в качестве динамической маски — мощный инструмент для создания сложных визуальных эффектов в реальном времени без редактирования исходных текстур. Вы можете экспериментировать: использовать шейдер, реагирующий на положение мыши (pointer.x, pointer.y), создавать маски на основе карты шума для эффекта статики или дыма, либо комбинировать несколько шейдерных масок на одном объекте для многослойных переходов.