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

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

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

Живой запуск

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

Исходный код


const colorWarp = `
vec3 p = vec3(outTexCoord, sin(uTime * 0.2));
for (int i = 0; i < 10; i++)
{
    p.xzy = vec3(1.3,0.999,0.7)*(abs((abs(p)/dot(p,p)-vec3(1.0,1.0,cos(uTime * 0.2)*0.5))));
}
fragColor.rgb *= p;
`;

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

        this.t = 0;
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/tests/pipeline/');

        this.load.image('cake', 'cake.png');
        this.load.image('crab', 'crab.png');
        this.load.image('fish', 'fish.png');
        this.load.image('pudding', 'pudding.png');
    }

    create ()
    {
        const renderNodeManager = this.renderer.renderNodes;

        // Create a custom render node.
        this.customBatchHandler = new Phaser.Renderer.WebGL.RenderNodes.BatchHandlerQuad(renderNodeManager);

        // An addition appends code into the shader at specific locations.
        // This shader adds a uniform variable to the shader,
        // and then uses it to warp the color of the texture during fragment processing.
        this.customBatchHandler.programManager.addAddition({
            name: 'ColorWarp',
            additions: {
                fragmentHeader: 'uniform float uTime;',
                fragmentProcess: colorWarp
            }
        });

        this.add.sprite(100, 300, 'pudding');
        const crab = this.add.sprite(400, 300, 'crab').setScale(1.5);
        crab.setRenderNodeRole('BatchHandler', this.customBatchHandler);
        const fish = this.fish = this.add.sprite(400, 300, 'fish');
        fish.setRenderNodeRole('BatchHandler', this.customBatchHandler);
        this.add.sprite(700, 300, 'cake');

        this.input.on('pointermove', pointer => {

            this.fish.x = pointer.worldX;
            this.fish.y = pointer.worldY;

        });

        this.input.on('pointerdown', () => {

            if (this.fish.customRenderNodes.BatchHandler)
            {
                this.fish.setRenderNodeRole('BatchHandler', null);
            }
            else
            {
                this.fish.setRenderNodeRole('BatchHandler', this.customBatchHandler);
            }

        });
    }

    update ()
    {
        this.customBatchHandler.programManager.setUniform('uTime', this.t);

        this.t += 0.05;

        this.fish.rotation -= 0.01;
    }
}

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

let game = new Phaser.Game(config);

Подготовка сцены и загрузка ресурсов

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

В методе preload задается базовый URL и путь для загрузки текстур. Используется набор декоративных изображений (пирог, краб, рыба, пудинг), которые будут отрисовываться на сцене.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.setPath('assets/tests/pipeline/');

    this.load.image('cake', 'cake.png');
    this.load.image('crab', 'crab.png');
    this.load.image('fish', 'fish.png');
    this.load.image('pudding', 'pudding.png');
}

Создание кастомного обработчика рендеринга

Сердце механизма — кастомный BatchHandler. Это специальный узел рендеринга, который управляет отрисовкой группы объектов. Мы создаем его через менеджер узлов рендеринга this.renderer.renderNodes.

Затем, используя programManager этого обработчика, мы модифицируем его шейдерную программу. Метод addAddition позволяет встроить наш GLSL-код в стандартный шейдер Phaser. Мы добавляем uniform-переменную uTime в заголовок фрагментного шейдера и встраиваем алгоритм искажения цвета colorWarp в процесс обработки фрагмента.

const renderNodeManager = this.renderer.renderNodes;
this.customBatchHandler = new Phaser.Renderer.WebGL.RenderNodes.BatchHandlerQuad(renderNodeManager);

this.customBatchHandler.programManager.addAddition({
    name: 'ColorWarp',
    additions: {
        fragmentHeader: 'uniform float uTime;',
        fragmentProcess: colorWarp
    }
});

Шейдерный код colorWarp — это математическая функция, которая итеративно трансформирует координаты текстуры и цвет пикселя, используя время (uTime), создавая эффект «плавления» или «завихрения» цвета.

Привязка эффекта к игровым объектам

Не все спрайты на сцене должны использовать эффект. По умолчанию они отрисовываются стандартным способом. Чтобы применить наш кастомный шейдер к конкретному спрайту, используется метод setRenderNodeRole('BatchHandler', handler).

В примере эффект применяется к спрайтам краба и рыбы, в то время как пирог и пудинг остаются без изменений. Это демонстрирует избирательность подхода.

const crab = this.add.sprite(400, 300, 'crab').setScale(1.5);
crab.setRenderNodeRole('BatchHandler', this.customBatchHandler);
const fish = this.fish = this.add.sprite(400, 300, 'fish');
fish.setRenderNodeRole('BatchHandler', this.customBatchHandler);

Динамическое управление эффектом и его параметрами

Эффект становится по-настоящему живым, когда его параметры можно менять в реальном времени. В методе update каждому кадру обновляется uniform-переменная uTime в шейдере, что приводит к анимации искажения цвета.

update ()
{
    this.customBatchHandler.programManager.setUniform('uTime', this.t);
    this.t += 0.05;
}

Кроме того, в примере реализовано интерактивное управление. Движением мыши управляется позиция рыбы, а по клику эффект шейдера для рыбы динамически включается или выключается. Это делается переприсваиванием роли рендеринга — либо нашему кастомному обработчику, либо null (что возвращает стандартный рендеринг).

this.input.on('pointerdown', () => {
    if (this.fish.customRenderNodes.BatchHandler)
    {
        this.fish.setRenderNodeRole('BatchHandler', null);
    }
    else
    {
        this.fish.setRenderNodeRole('BatchHandler', this.customBatchHandler);
    }
});

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

Кастомные шейдеры в Phaser открывают дверь в мир уникальных визуальных эффектов, которые работают на GPU. Вы научились создавать специализированный обработчик рендеринга, модифицировать его шейдерный код и применять эффекты выборочно к объектам с возможностью динамического управления. Для экспериментов попробуйте: изменить алгоритм colorWarp для создания других эффектов (свечение, пикселизация); анимировать другие uniform-переменные (например, силу эффекта); привязать параметры шейдера не ко времени, а к игровым событиям (здоровью персонажа, получению урона).