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

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

Версия 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('bundle', 'assets/shaders/bundle.glsl.js');
    }

    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('Tunnel', 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.clear(0, 0, 100, 100);
        this.r += 0.1;
    }
}

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

const game = new Phaser.Game(config);

Суть примера: текстура как холст

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

Этот подход похож на работу с холстом в 2D-контексте. Мы получаем поверхность, на которой можно штамповать изображения, рисовать фигуры и стирать части содержимого, причем все эти операции выполняются очень быстро на стороне GPU, так как текстура — это, по сути, объект WebGL.

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

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

В методе create происходит инициализация. Сначала создается сама динамическая текстура размером 512x512 пикселя с уникальным ключом 'shaderTexture'.

Затем подготавливаются данные для отрисовки: создается массив this.apples, который хранит 64 случайные позиции (x, y) в пределах текстуры (с отступом от краев). Эти координаты будут использоваться для штамповки спрайта яблока.

Важный момент: сами спрайты НЕ создаются как игровые объекты (this.add.image). Вместо этого мы храним лишь их целевые координаты в простом массиве объектов. Это экономит ресурсы, так как объекты Phaser не инстанциируются.

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

Цикл обновления: рисование и анимация

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

1. **Очистка:** Первым делом текстура заливается цветом 0x000066 (темно-синий) с помощью метода fill. Это сбрасывает изображение на предыдущем кадре. 2. **Штамповка:** Для каждой сохраненной позиции из массива this.apples на текстуру "ставится штамп" — отрисовывается изображение apple. Метод stamp принимает ключ изображения, кадр анимации (null), координаты и опции, такие как вращение. Угол вращения this.r плавно увеличивается каждый кадр, заставляя все яблоки вращаться. 3. **Стирание:** Интересная деталь — после отрисовки всех яблок вызывается clear(0, 0, 100, 100). Этот метод очищает прямоугольную область (от 0,0 до 100,100) на текстуре, делая ее прозрачной. В данном примере это создает постоянно обновляющийся черный квадрат в углу.

this.texture.fill(0x000066);
this.apples.forEach(apple => {
    this.texture.stamp('apple', null, apple.x, apple.y, { rotation: this.r });
});
this.texture.clear(0, 0, 100, 100);
this.r += 0.1;

Использование результата: связь с шейдером

Созданная и анимированная текстура не видна сама по себе. Чтобы ее отобразить, она передается в качестве uniform-переменной (в данном случае — как массив текстур ['shaderTexture']) в шейдер 'Tunnel'.

Шейдер — это программа на GLSL, которая работает с пикселями. Шейдер Tunnel использует нашу динамическую текстуру как источник данных для создания туннельного эффекта или искажения. Таким образом, мы не просто видим плоскую текстуру, а наблюдаем ее преобразование через шейдер.

this.add.shader('Tunnel', 400, 300, 800, 600, [ 'shaderTexture' ]);

Именно эта строка добавляет шейдерный объект в центр сцены (400x300), который растягивается на все окно 800x600 и использует нашу текстуру.

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

Динамические текстуры в Phaser — это мост между высокоуровневым API для работы со спрайтами и низкоуровневой мощью WebGL. Они идеально подходят для ситуаций, где нужно часто и эффективно менять большое растровое изображение. Практическое применение: создание динамических мини-карт, рисование следов или разрушаемых terrain, генерация уникальных иконок предметов "на лету" или реализация сложных частичных систем, где тысячи частиц рисуются на одной текстуре. Для экспериментов попробуйте изменить цвет заливки fill каждый кадр, использовать stamp с разными масштабами или альфа-каналом, или очищать clear область по координатам курсора мыши.