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

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

Версия 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('hannae', 'assets/shaders/love-u-hanne-e.frag');
        this.load.glsl('yinyang', 'assets/shaders/yin-yang.frag');
        this.load.image('block', 'assets/sprites/block.png');
    }

    create ()
    {
        const shader = this.add.shader({
            name: 'hannae',
            fragmentKey: 'hannae',
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            },
        }, 400, 300, 800, 800);

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

        const shader2 = this.add.shader({
            name: 'yinyang',
            fragmentKey: 'yinyang',
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            },
        }, 400, 300, 256, 256);

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

        this.add.image(600, 300, 'block');

        this.tweens.add({
            targets: shader2,
            scaleX: 4,
            scaleY: 4,
            repeat: -1,
            yoyo: true,
            duration: 2000
        });

        this.input.on('pointermove', function (pointer)
        {

            shader2.setPosition(pointer.x, pointer.y);
        });
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов: шейдеры как ассеты

Перед использованием шейдеры необходимо загрузить как ресурсы. Phaser предоставляет для этого специальный метод load.glsl(). Он принимает уникальный ключ и путь к файлу с исходным кодом шейдера.

this.load.glsl('hannae', 'assets/shaders/love-u-hanne-e.frag');
this.load.glsl('yinyang', 'assets/shaders/yin-yang.frag');

В данном примере загружаются два фрагментных шейдера (.frag). Ключи 'hannae' и 'yinyang' будут использоваться для их идентификации при создании. Обратите внимание, что для работы с шейдерами рендерер игры должен быть Phaser.WEBGL (это указано в конфигурации type).

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

Основной метод для добавления шейдера на сцену — this.add.shader(). Он принимает объект конфигурации и параметры позиционирования.

const shader = this.add.shader({
    name: 'hannae',
    fragmentKey: 'hannae',
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    },
}, 400, 300, 800, 800);

В конфигурации: - fragmentKey указывает на ключ загруженного шейдера. - setupUniforms — это функция обратного вызова, которая выполняется каждый кадр для обновления uniform-переменных шейдера. Здесь переменной time передается общее время работы игры в миллисекундах, полученное через this.game.loop.getDuration(). Это стандартный приём для анимации шейдеров. Последние четыре аргумента метода задают позицию (x, y) и размер (width, height) прямоугольной области, в которой будет отрисовываться шейдер.

Интеграция шейдеров и спрайтов

Шейдеры в Phaser — это полноценные игровые объекты (GameObject). Их можно добавлять на сцену вместе с обычными изображениями, и они будут отрисовываться в порядке добавления (z-order).

this.add.image(200, 300, 'block');
const shader2 = this.add.shader(...);
this.add.image(400, 300, 'block');

На примере видно, что блок, шейдер и еще один блок добавляются последовательно. Они могут перекрывать друг друга. Это открывает возможности для создания сложных композиций, где шейдеры служат динамическим фоном или эффектами поверх/под спрайтами.

Динамическое управление: твины и интерактивность

Как и любой GameObject, шейдеру можно анимировать свойства, например, масштаб, используя систему твинов Phaser.

this.tweens.add({
    targets: shader2,
    scaleX: 4,
    scaleY: 4,
    repeat: -1,
    yoyo: true,
    duration: 2000
});

Этот твин заставляет второй шейдер пульсировать, плавно увеличиваясь и уменьшаясь. Кроме того, шейдеры можно перемещать в реальном времени в ответ на действия игрока.

this.input.on('pointermove', function (pointer) {
    shader2.setPosition(pointer.x, pointer.y);
});

Обработчик события pointermove вызывает метод setPosition() у объекта shader2, привязывая его положение к курсору мыши. Метод setPosition наследуется от базового класса GameObject.

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

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