О чем этот пример
Создание визуально насыщенных игровых миров часто требует нестандартных подходов. В этом примере мы совместим мощь 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) для одного шейдера, создавая композицию из нескольких анимированных слоёв.
* Сделайте позиции яблок реагирующими на ввод игрока (мышь, касание), чтобы шейдерный фон стал интерактивным.
