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

В современных играх сложные визуальные эффекты часто строятся по принципу конвейера: один шейдер рендерится в текстуру, которая затем используется как входные данные для другого шейдера. Этот подход открывает огромные возможности для создания динамичных блюмов, искажений, плазмы и других пост-эффектов. В статье разберем пример из официальной документации 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.image('logo', 'assets/sprites/phaser3-logo-small.png');
        this.load.glsl('Flower Plasma', 'assets/shaders/flower-plasma.frag');
        this.load.glsl('Tunnel', 'assets/shaders/tunnel.frag');
    }

    create ()
    {
        //  Here we create our shader. It has a size of 512 x 512.
        const bufferA = this.add.shader({
            name: 'Flower Plasma',
            fragmentKey: 'Flower Plasma',
            initialUniforms: {
                resolution: [ 512, 512 ]
            },
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            }
        }, 0, 0, 512, 512);

        //  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:
        bufferA.setRenderToTexture('bufferA');

        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, [ 'bufferA' ]);

        this.add.image(400, 300, 'logo');
    }
}

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

const game = new Phaser.Game(config);

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

В Phaser 3 шейдеры можно загружать как отдельные ресурсы с помощью метода load.glsl. Это удобно для хранения сложных фрагментных шейдеров в отдельных файлах с расширением .frag.

this.load.glsl('Flower Plasma', 'assets/shaders/flower-plasma.frag');
this.load.glsl('Tunnel', 'assets/shaders/tunnel.frag');

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

Создание шейдера-буфера

Первый шейдер создается с помощью this.add.shader. Важные параметры: fragmentKey соответствует ключу загрузки, а initialUniforms задает начальные значения uniform-переменных. Для шейдера «Цветочная плазма» важно передать правильное resolution.

const bufferA = this.add.shader({
    name: 'Flower Plasma',
    fragmentKey: 'Flower Plasma',
    initialUniforms: {
        resolution: [ 512, 512 ]
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 0, 0, 512, 512);

Функция setupUniforms вызывается каждый кадр и обновляет uniform time, используя общее время работы игры this.game.loop.getDuration(). Это обеспечивает анимацию шейдера. Координаты (0,0) и размеры (512x512) задают область рендеринга шейдера в пикселях.

Ключевой момент — следующий вызов:

bufferA.setRenderToTexture('bufferA');

Метод setRenderToTexture перенаправляет вывод шейдера из основного конвейера рендеринга (на экран) в отдельную текстуру в памяти. Эта текстура сохраняется в Texture Manager под ключом 'bufferA' и становится доступной для других объектов.

Использование буфера в другом шейдере

Второй шейдер «Туннель» создается с теми же принципами, но с двумя важными отличиями в параметрах.

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, [ 'bufferA' ]);

Uniform iChannel0: 0 в шейдерах типа «Туннель» часто указывает на индекс текстуры для сэмплирования. Последний, пятый позиционный аргумент при создании шейдера — это массив ключей текстур: [ 'bufferA' ]. Phaser автоматически привязывает текстуру с ключом 'bufferA' (которую мы создали ранее) к соответствующему uniform-сэмплеру в шейдере «Туннель». Таким образом, анимированная «Цветочная плазма» становится текстурой, которую «Туннель» искажает и накладывает на свой собственный паттерн.

Шейдер «Туннель» рендерится непосредственно на экран, так как для него не вызывался setRenderToTexture. Его размер (800x600) и позиция центра (400,300) соответствуют размерам игры из конфигурации.

Композиция сцены и настройка рендерера

После создания цепочки шейдеров на сцену добавляется обычное изображение.

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

Оно будет отрисовано поверх всего, так как добавлено в конец. Порядок вызовов add.shader и add.image определяет порядок отрисовки (z-index).

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

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

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

Использование шейдеров в качестве буферов — мощный метод для создания сложных визуальных эффектов в Phaser. Вы можете экспериментировать: создавать несколько буферов и передавать их по цепочке, менять uniform-переменные на основе игровой логики (например, alpha для эффекта затухания) или использовать буфер в качестве карты высот для псевдо-3D эффектов. Попробуйте привязать выходной буфер не к другому шейдеру, а к спрайту через setTexture, чтобы создать динамические текстуры для игровых объектов.