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

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

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

Живой запуск

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

Исходный код


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

        this.r = 0;
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('apple', 'assets/sprites/apple.png');
        this.load.glsl('tunnel', 'assets/shaders/tunnel.frag');
    }

    create ()
    {
        const texture = this.textures.addDynamicTexture('shaderTexture', 512, 512);

        this.apples = [];

        for (let i = 0; i < 64; i++)
        {
            const x = Phaser.Math.Between(25, 487);
            const y = Phaser.Math.Between(25, 487);

            this.apples.push({ x, y });
        }

        this.texture = texture;

        this.add.shader({
            name: 'Tunnel',
            fragmentKey: 'tunnel',
            initialUniforms: {
                resolution: [ 800, 600 ],
                iChannel0: 0,
                alpha: 1,
                origin: 2
            },
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 400, 300, 800, 600, [ 'shaderTexture' ]);
    }

    update ()
    {
        this.texture.fill(0x000066);

        this.apples.forEach(apple => {

            this.texture.stamp('apple', null, apple.x, apple.y, { rotation: this.r });

        });

        this.texture.render();

        this.r += 0.1;
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload загружаются необходимые ресурсы: спрайт яблока и GLSL-шейдерный фрагмент. Ключевое действие происходит в create.

Здесь создаётся динамическая текстура — специальный объект, который можно рисовать в рантайме. Мы задаём ей имя и размеры.

const texture = this.textures.addDynamicTexture('shaderTexture', 512, 512);

Далее мы создаём массив из 64 объектов с координатами для будущих спрайтов. Эти координаты сохраняются для последующего использования.

this.apples = [];
for (let i = 0; i < 64; i++) {
    const x = Phaser.Math.Between(25, 487);
    const y = Phaser.Math.Between(25, 487);
    this.apples.push({ x, y });
}
this.texture = texture;

Текстура сохраняется в свойство сцены, чтобы быть доступной в методе update.

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

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

this.add.shader({
    name: 'Tunnel',
    fragmentKey: 'tunnel',
    initialUniforms: {
        resolution: [ 800, 600 ],
        iChannel0: 0,
        alpha: 1,
        origin: 2
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 400, 300, 800, 600, [ 'shaderTexture' ]);

Параметр iChannel0: 0 в initialUniforms указывает, что в шейдере первый текстурный канал (обычно iChannel0) должен быть взят из первого элемента переданного массива текстурных ключей — то есть из 'shaderTexture'. Функция setupUniforms обновляет uniform-переменную time каждый кадр, передавая общее время работы игры, что необходимо для анимации в шейдере.

Анимация текстуры в реальном времени

Вся магия оживления происходит в методе update, который вызывается каждый кадр.

Сначала мы полностью очищаем текстуру, заливая её сплошным цветом.

this.texture.fill(0x000066);

Затем для каждой сохранённой позиции мы «штампуем» на текстуру изображение яблока. Метод stamp — это способ отрисовки спрайта или изображения непосредственно на динамическую текстуру.

this.apples.forEach(apple => {
    this.texture.stamp('apple', null, apple.x, apple.y, { rotation: this.r });
});

Каждому яблоку передаётся текущий угол вращения this.r. После того как все спрайты отрисованы, необходимо вызвать render(), чтобы изменения вступили в силу и текстура обновилась.

this.texture.render();
this.r += 0.1;

Увеличение переменной вращения this.r каждый кадр приводит к плавному повороту всех яблок на текстуре. Поскольку эта текстура является входным каналом для шейдера, шейдер видит её обновлённое состояние каждый кадр, что и создаёт эффект живого, вращающегося фона.

Как это работает в связке

Цикл обновления выглядит так: 1. Каждый кадр в update динамическая текстура очищается и на неё штампуются вращающиеся спрайты. 2. Текстура рендерится и становится актуальной. 3. Шейдер, добавленный в сцене, в этом же кадре получает обновлённую текстуру через uniform-переменную iChannel0. 4. Шейдер выполняет свои вычисления (в данном случае создаёт «туннельный» эффект) на основе новой текстуры и обновлённого времени time. 5. Результат шейдера отрисовывается на холсте.

Таким образом, шейдер обрабатывает не статичную картинку, а полноценный анимированный буфер, который мы контролируем с помощью API Phaser.

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

Комбинация динамических текстур и шейдеров — мощный инструмент для создания сложных визуальных эффектов в Phaser. Вы научились создавать и обновлять текстуру в реальном времени, а затем передавать её в шейдер для постобработки. **Идеи для экспериментов:** * Измените логику в update: двигайте яблоки по простым траекториям, меняйте их масштаб или используйте разные спрайты. * Вместо fill попробуйте накладывать на текстуру градиенты или другие примитивы. * Используйте несколько динамических текстур как разные каналы (iChannel1, iChannel2) для одного шейдера, создавая композицию из нескольких анимированных слоёв. * Сделайте позиции яблок реагирующими на ввод игрока (мышь, касание), чтобы шейдерный фон стал интерактивным.