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

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

Версия 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.image('apple', 'assets/sprites/apple.png');
        this.load.glsl('tunnel', 'assets/shaders/tunnel.frag');
    }

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

        texture.fill(0x000066);

        for (let i = 0; i < 64; i++)
        {
            texture.stamp('apple', null, Phaser.Math.Between(25, 487), Phaser.Math.Between(25, 487));
        }
        texture.render();

        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' ]);
    }
}

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

const game = new Phaser.Game(config);

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

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

Сначала мы создаем пустую текстуру в памяти размером 512x512 пикселя с помощью this.textures.addDynamicTexture. Ей сразу присваивается ID 'shaderTexture', чтобы позже к ней можно было обратиться.

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

Затем текстура заливается сплошным цветом, создавая фон.

texture.fill(0x000066);

Заполнение текстуры спрайтами с помощью метода `stamp`

Динамическая текстура — это холст для рисования. Метод stamp позволяет "штамповать" загруженные изображения прямо на нее. В цикле мы 64 раза размещаем спрайт 'apple' в случайных координатах.

for (let i = 0; i < 64; i++)
{
    texture.stamp('apple', null, Phaser.Math.Between(25, 487), Phaser.Math.Between(25, 487));
}

Важно: после всех операций рисования необходимо явно вызвать texture.render(). Без этого вызова изменения не будут применены к текстуре. Этот метод финализирует все операции и делает текстуру готовой к использованию.

Создание шейдера и передача динамической текстуры

Теперь мы создаем объект шейдера с помощью this.add.shader. В его настройках (initialUniforms) мы указываем, что для сэмплера iChannel0 (это стандартное имя для текстуры в шейдере) будет использоваться текстура с индексом 0 из переданного массива.

this.add.shader({
    name: 'Tunnel',
    fragmentKey: 'tunnel',
    initialUniforms: {
        resolution: [ 800, 600 ],
        iChannel0: 0, // Индекс текстуры в массиве textures
        alpha: 1,
        origin: 2
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 400, 300, 800, 600, [ 'shaderTexture' ]);

Ключевой аргумент — последний: массив [ 'shaderTexture' ]. Phaser сопоставляет индекс в этом массиве (0) с uniform-переменной iChannel0 в шейдере. Таким образом, созданная нами динамическая текстура становится источником данных для шейдерного эффекта "туннеля". Функция setupUniforms обновляет uniform time каждый кадр, анимируя шейдер.

Конфигурация игры и важность WebGL

Для работы шейдеров и динамических текстур рендерер должен быть установлен в Phaser.WEBGL. Рендерер Phaser.CANVAS не поддерживает эти функции.

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

Именно этот конфиг передается в конструктор Phaser.Game, инициируя запуск сцены.

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

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