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

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

Версия 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('Marble', 'assets/shaders/marble.frag');
    }

    create ()
    {
        // Here we create our shader. It has a size of 128 x 128.
        const shader = this.add.shader({
            name: 'Marble',
            fragmentKey: 'Marble',
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 0, 0, 128, 128);

        //  Now we tell it to render to a texture, instead of on the display list.
        //  The string given here is the key that is used when saving it to the Texture Manager:

        shader.setRenderToTexture('wibble');

        //  And now some images that use the texture

        this.add.image(200, 300, 'wibble');

        //  A scaled image
        this.add.image(400, 300, 'wibble').setScale(2);

        //  A tweened image
        const mover = this.add.image(600, 100, 'wibble');

        this.tweens.add({
            targets: mover,
            angle: 180,
            y: 500,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true,
            duration: 2000
        });
    }
}

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

const game = new Phaser.Game(config);

Загрузка и создание шейдера

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

this.load.glsl('Marble', 'assets/shaders/marble.frag');

В методе create мы создаём сам шейдер. Для этого используется метод this.add.shader. Мы передаём конфигурационный объект, где указываем ключ загруженного шейдера и функцию setupUniforms для передачи uniform-переменных. В данном примере мы передаём uniform time, равный времени работы игры, что позволит шейдеру анимироваться. Последние четыре аргумента метода — это x, y, ширина и высота области рендеринга шейдера (128x128 пикселей).

const shader = this.add.shader({
    name: 'Marble',
    fragmentKey: 'Marble',
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 0, 0, 128, 128);

Волшебный метод: setRenderToTexture

Ключевой момент — метод setRenderToTexture. Он перенаправляет вывод шейдера с экрана в текстуру внутри Texture Manager Phaser. В качестве аргумента мы передаём строковый ключ, по которому эта текстура будет доступна. После вызова этого метода шейдер больше не отрисовывается напрямую на дисплейный список (display list), а его результат сохраняется в текстуре с заданным ключом.

shader.setRenderToTexture('wibble');

Теперь в Texture Manager существует текстура с ключом 'wibble', которая содержит кадр, отрендеренный шейдером. Если шейдер анимируется (как в нашем случае, благодаря uniform time), текстура также будет обновляться каждый кадр.

Использование текстуры шейдера в игре

После того как текстура создана, с ней можно работать как с любым другим загруженным изображением. Мы создаём игровые объекты Image, используя ключ текстуры 'wibble'.

Первый объект — обычное изображение, отображающее текстуру в её исходном размере 128x128.

this.add.image(200, 300, 'wibble');

Второй объект демонстрирует, что текстуру можно масштабировать стандартными методами Phaser, например, setScale.

this.add.image(400, 300, 'wibble').setScale(2);

Третий объект показывает, что с такими изображениями можно работать в Tweens. Мы создаём твин, который анимирует положение по оси Y и угол поворота изображения. Это доказывает, что динамическая текстура шейдера и анимация его контейнера (изображения) работают полностью независимо и могут комбинироваться.

const mover = this.add.image(600, 100, 'wibble');

this.tweens.add({
    targets: mover,
    angle: 180,
    y: 500,
    ease: 'Sine.easeInOut',
    repeat: -1,
    yoyo: true,
    duration: 2000
});

Конфигурация игры и важное замечание

Для работы с шейдерами и рендерингом в текстуру тип рендерера в конфигурации игры должен быть установлен в Phaser.WEBGL. Рендерер Phaser.CANVAS не поддерживает эти функции.

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

Важно понимать, что после вызова setRenderToTexture сам объект шейдера (shader) больше не является частью дисплейного списка. Его единственная задача теперь — обновлять текстуру. Все визуальные манипуляции производятся с объектами Image (или другими), которые используют эту текстуру.

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

Метод setRenderToTexture открывает новые возможности для работы с шейдерами в Phaser. Вы можете создавать динамические текстуры для воды, огня, магии, помещать их в спрайты, анимировать эти спрайты и даже использовать текстуры в системах частиц. Для экспериментов попробуйте: создать текстуру шейдера большего размера и использовать её для фона уровня; применить одну текстуру шейдера одновременно к десяткам спрайтов для оптимизации; или сохранить текущий кадр шейдера в статическую текстуру с помощью this.textures.get('wibble').snapshot() для последующего использования.