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

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

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

    create ()
    {
        const shader = this.add.shader({
            name: 'Colorful Voronoi',
            fragmentKey: 'Colorful Voronoi',
            initialUniforms: {
                resolution: [this.scale.width, this.scale.height]
            },
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 0, 0, 128, 128);

        shader.setRenderToTexture('wibble');

        const blocks = this.add.group({ key: 'wibble', repeat: 63, setScale: { x: 0.5, y: 0.5 } });

        Phaser.Actions.GridAlign(blocks.getChildren(), {
            width: 9,
            height: 7,
            cellWidth: 128,
            cellHeight: 128,
            x: 0,
            y: 0
        });

        let i = 0;

        blocks.children.forEach(function (child)
        {

            this.tweens.add({
                targets: child,
                props: {
                    scale: { value: 1, duration: 500 },
                    angle: { value: 360, duration: 4000 }
                },
                ease: 'Sine.easeInOut',
                delay: i * 50,
                repeat: -1,
                yoyo: true
            });

            i++;

            if (i % 10 === 0)
            {
                // i = 0;
            }

        }, this);
    }
}

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

const game = new Phaser.Game(config);

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

Всё начинается с загрузки шейдерного кода. В методе preload мы используем специальный загрузчик load.glsl для фрагментного шейдера.

this.load.glsl('Colorful Voronoi', 'assets/shaders/colorful-voronoi.frag');

В методе create создается объект шейдера. Ключевые параметры: * fragmentKey: имя, под которым был загружен шейдер. * initialUniforms: начальные значения uniform-переменных шейдера. Здесь передается разрешение экрана, чтобы шейдер мог корректно масштабироваться. * setupUniforms: функция, вызываемая каждый кадр для обновления uniform-переменных. В примере она передает шейдеру общее время работы игры в миллисекундах через this.game.loop.getDuration(), что позволяет анимировать эффект.

const shader = this.add.shader({
    name: 'Colorful Voronoi',
    fragmentKey: 'Colorful Voronoi',
    initialUniforms: {
        resolution: [this.scale.width, this.scale.height]
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 0, 0, 128, 128);

Рендеринг шейдера в текстуру

Самый важный шаг — превратить вывод шейдера из отображаемого на экране объекта в текстуру, которую можно переиспользовать. Для этого у объекта шейдера вызывается метод setRenderToTexture.

shader.setRenderToTexture('wibble');

После этого вызова в текстовом кэше игры появляется текстура с именем 'wibble'. Эта текстура представляет собой "снимок" текущего состояния шейдера размером 128x128 пикселей (как было задано при создании). Теперь к этой текстуре можно обращаться по имени, как к любому другому графическому ресурсу. Шейдер продолжает анимироваться в реальном времени, и текстура 'wibble' автоматически обновляется каждый кадр.

Массовое создание объектов из текстуры

Phaser позволяет создавать группы (Group) объектов, используя текстуру как их изображение. В примере создается группа из 64 объектов (один оригинал + 63 повторения), каждый из которых использует текстуру 'wibble'. Параметр setScale сразу уменьшает размер всех объектов вдвое.

const blocks = this.add.group({ key: 'wibble', repeat: 63, setScale: { x: 0.5, y: 0.5 } });

Созданные объекты изначально находятся в одной точке. Чтобы расположить их в виде сетки, используется хелпер Phaser.Actions.GridAlign. Он принимает массив детей группы и конфигурацию сетки 9x7.

Phaser.Actions.GridAlign(blocks.getChildren(), {
    width: 9,
    height: 7,
    cellWidth: 128,
    cellHeight: 128,
    x: 0,
    y: 0
});

Анимация объектов группы

Чтобы оживить сцену, для каждого объекта в группе создается твин (анимация). Используется цикл forEach, где `i— счетчик для создания задержки (delay`) между запуском анимаций у разных объектов.

blocks.children.forEach(function (child) {
    this.tweens.add({
        targets: child,
        props: {
            scale: { value: 1, duration: 500 },
            angle: { value: 360, duration: 4000 }
        },
        ease: 'Sine.easeInOut',
        delay: i * 50,
        repeat: -1,
        yoyo: true
    });
    i++;
}, this);

Каждый твин одновременно анимирует два свойства спрайта: 1. scale: плавно увеличивает масштаб от 0.5 до 1 за 500 мс, а затем обратно (благодаря yoyo: true). 2. angle: непрерывно вращает объект на 360 градусов за 4 секунды.

Параметр delay: i * 50 создает "волну" анимаций, запуская твины для объектов не одновременно, а с небольшим сдвигом.

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

Техника рендеринга в текстуру — это ключ к высокой производительности при использовании шейдеров. Вместо десятков вызовов отрисовки шейдера за кадр, мы имеем всего один, результат которого тиражируется на множество спрайтов. Для экспериментов попробуйте: 1. Изменять параметры шейдера (например, скорость анимации через uniform-переменную) и наблюдать, как это мгновенно отражается на всех объектах. 2. Применять эту технику для создания анимированного фона из тысяч частиц. 3. Комбинировать несколько шейдерных текстур на одном объекте, используя маски или blend-режимы.