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

В игровом движке Phaser маски позволяют скрывать часть графики, создавая эффекты проявления, стирания или сложных переходов. Классический подход с `setMask()` может быть неудобен, когда маску нужно динамически менять в реальном времени, например, рисовать её под курсором мыши. В этой статье мы рассмотрим мощную альтернативу: использование Render Texture в качестве динамической маски для фильтра изображения. Этот приём открывает двери для создания интерактивных элементов, таких как стирание слоя пыли, проявление скрытого изображения или рисование невидимой кистью, которая «проявляет» картинку только под собой.

Версия 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.image('brush', 'assets/sprites/brush1.png');
        this.load.image('pic', 'assets/pics/brilliance-jim-sachs.png');
    }

    create ()
    {
        const rt = this.add.renderTexture(400, 300, 800, 600);

        const pic = this.add.image(400, 300, 'pic');

        pic.enableFilters().filters.external.addMask(rt);

        this.input.on('pointermove', pointer =>
        {
            if (pointer.isDown)
            {
                rt.draw('brush', pointer.x, pointer.y).render();
            }

        }, this);

        this.input.keyboard.on('keydown-SPACE', event =>
        {
            // this.game.scale.resize(800, 600);
            rt.resize(800, 600);
        });
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов и создание Render Texture

В методе preload() загружаются два изображения: одно для фоновой картинки (pic), другое — для кисти (brush), которой мы будем «рисовать» маску.

В create() создаётся ключевой объект — Render Texture. Его можно представить как чистый холст в памяти, на который можно рисовать другие игровые объекты. В нашем случае этот холст и станет нашей маской.

const rt = this.add.renderTexture(400, 300, 800, 600);

Параметры конструктора — это координаты центра (x, y) и размеры (width, height) текстуры. Сразу после создания rt представляет собой полностью прозрачный (чёрный) прямоугольник.

Связывание изображения и маски через фильтр

Далее мы создаём фоновое изображение и размещаем его в центре сцены. Чтобы применить к нему маску, используется система фильтров Phaser.

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

1. pic.enableFilters() — активирует систему фильтров для этого конкретного изображения. 2. .filters.external.addMask(rt) — это ключевая строка. Мы обращаемся к внешнему (post-render) фильтру изображения и добавляем ему маску. В качестве маски передаётся наш Render Texture (rt). Логика работы этой маски инвертирована: области Render Texture, где что-то нарисовано (непрозрачные), становятся видимыми частями изображения pic. Изначально rt пуст, поэтому всё изображение pic скрыто.

Рисование маски в реальном времени

Теперь нужно сделать маску интерактивной. Мы подписываемся на событие перемещения указателя (мыши или касания).

this.input.on('pointermove', pointer =>
{
    if (pointer.isDown)
    {
        rt.draw('brush', pointer.x, pointer.y).render();
    }
}, this);

При каждом движении мыши с зажатой кнопкой выполняется: 1. rt.draw('brush', pointer.x, pointer.y) — метод draw рисует текстуру кисти (brush) на Render Texture в координатах курсора. Центр кисти совмещается с этими координатами. 2. .render() — этот вызов принудительно обновляет (перерисовывает) Render Texture, чтобы изменения немедленно вступили в силу. Без него рисование может не отображаться до следующего кадра движка. Таким образом, мы «закрашиваем» маску там, где ведём курсор, и в этих местах начинает проявляться наше фоновое изображение.

Управление маской: Очистка

В примере также показано, как можно управлять маской с клавиатуры. При нажатии пробела Render Texture очищается, снова скрывая изображение.

this.input.keyboard.on('keydown-SPACE', event =>
{
    rt.resize(800, 600);
});

Здесь используется метод resize(). Важно понимать, что вызов resize(width, height) не только меняет размер текстуры, но и полностью очищает её, сбрасывая всё нарисованное содержимое. Это простой способ «стереть» нашу динамическую маску. Альтернативой мог бы быть метод rt.clear().

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

Использование Render Texture в качестве маски для фильтра — это мощный метод для создания динамических эффектов раскрытия контента в Phaser. Вы научились создавать интерактивную маску, которую можно рисовать в реальном времени, и очищать её по команде. **Идеи для экспериментов:** 1. Замените текстуру кисти на спрайт с полупрозрачными границами для создания мягкого, размытого эффекта проявления. 2. Используйте в качестве кисти анимированный спрайт (this.anims.create), чтобы маска «ожила» (например, рисовала мерцающими искрами). 3. Измените логику: изначально сделайте изображение видимым, а рисуемая маска будет его скрывать (эффект стирания). Для этого можно использовать второй Render Texture и инвертировать логику отображения, либо применить другой тип блендинга. 4. Реализуйте несколько слоёв с разными масками для создания сложных интерактивных пазлов или карт с "туманом войны".