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

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

Версия 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.glsl('spiralTime', 'assets/shaders/spiralTime.frag');
        this.load.image('pic', 'assets/pics/rick-and-morty-by-sawuinhaff-da64e7y.png');
        this.load.image('logo', 'assets/sprites/phaser3-logo-x2.png');
        this.load.image('splat1', 'assets/pics/splat1.png');
        this.load.image('splat3', 'assets/pics/splat3.png');
    }

    create ()
    {
        const maskImage1 = this.make.image({ x: 400, y: 300, key: 'splat1', add: false });
        const maskImage2 = this.make.image({ x: 400, y: 300, key: 'splat3', add: false });

        this.cameras.main.filters.external.addMask(maskImage1);

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

        const shader = this.add.shader({
            name: 'spiralTime',
            fragmentKey: 'spiralTime',
            setupUniforms: (setUniform, drawingContext) =>
            {
                setUniform('time', this.game.loop.getDuration());
            },
        }, 400, 300, this.scale.width, this.scale.width);
        shader.enableFilters().filters.external.addMask(maskImage2);

        this.text = this.add.text(80, 320, '', { font: '16px Courier', fill: '#00ff00' }).setName('text');

        this.add.image(400, 300, 'logo').setName('logo');
    }

    update ()
    {
        if (this.text)
        {
            this.text.setText([
                this.sys.game.loop.getDuration(),
                this.sys.game.loop.getDurationMS()
            ]);
        }
    }
}

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

const game = new Phaser.Game(config);

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

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

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.glsl('spiralTime', 'assets/shaders/spiralTime.frag');
    this.load.image('pic', 'assets/pics/rick-and-morty-by-sawuinhaff-da64e7y.png');
    this.load.image('logo', 'assets/sprites/phaser3-logo-x2.png');
    this.load.image('splat1', 'assets/pics/splat1.png');
    this.load.image('splat3', 'assets/pics/splat3.png');
}

Создание масок и применение к камере

В методе create сначала создаются два объекта-изображения, которые не добавляются на дисплейный список сцены (add: false). Они служат исключительно как источники данных для масок. Первая маска (maskImage1) применяется непосредственно к основному фильтру камеры (this.cameras.main.filters.external). Это означает, что всё, что рисует эта камера, будет обрезано по форме этой маски. После этого на сцену добавляется фоновое изображение, которое уже будет отрисовано с учётом маски камеры.

const maskImage1 = this.make.image({ x: 400, y: 300, key: 'splat1', add: false });
const maskImage2 = this.make.image({ x: 400, y: 300, key: 'splat3', add: false });

this.cameras.main.filters.external.addMask(maskImage1);

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

Добавление и настройка шейдера с маской

Далее создаётся объект шейдера с помощью this.add.shader. В его конфигурации указывается ключ загруженного фрагментного шейдера и функция setupUniforms, которая вызывается для установки uniform-переменных. В данном примере в шейдер передаётся текущее время работы игры this.game.loop.getDuration(). Это классический приём для анимации шейдерных эффектов. После создания шейдера для него включаются фильтры (enableFilters()), и к ним добавляется вторая маска (maskImage2). Эта маска влияет только на область отрисовки самого шейдера, а не на всю сцену.

const shader = this.add.shader({
    name: 'spiralTime',
    fragmentKey: 'spiralTime',
    setupUniforms: (setUniform, drawingContext) =>
    {
        setUniform('time', this.game.loop.getDuration());
    },
}, 400, 300, this.scale.width, this.scale.width);
shader.enableFilters().filters.external.addMask(maskImage2);

Динамическое обновление и отладка

В сцену добавляется текстовый объект и логотип. В методе update каждому кадру обновляется текст, отображающий текущее время работы игры в секундах и миллисекундах, полученное через this.sys.game.loop. Это демонстрирует, как можно передавать динамически меняющиеся данные из игрового цикла в шейдер (через функцию setupUniforms), а также полезно для отладки и понимания работы цикла.

this.text = this.add.text(80, 320, '', { font: '16px Courier', fill: '#00ff00' }).setName('text');
this.add.image(400, 300, 'logo').setName('logo');

update ()
{
    if (this.text)
    {
        this.text.setText([
            this.sys.game.loop.getDuration(),
            this.sys.game.loop.getDurationMS()
        ]);
    }
}

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

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