О чем этот пример
Шейдеры в 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.glsl('Marble', 'assets/shaders/marble.frag');
}
create ()
{
// Here we create our shader. It has a size of 128 x 128.
const shader = this.add.shader({
name: 'Marble',
fragmentKey: 'Marble',
setupUniforms: (setUniform, drawingContext) => {
setUniform('time', this.game.loop.getDuration());
}
}, 0, 0, 128, 128);
// 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:
shader.setRenderToTexture('wibble');
// And now some images that use the texture
this.add.image(200, 300, 'wibble');
// A scaled image
this.add.image(400, 300, 'wibble').setScale(2);
// A tweened image
const mover = this.add.image(600, 100, 'wibble');
this.tweens.add({
targets: mover,
angle: 180,
y: 500,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true,
duration: 2000
});
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Загрузка и создание шейдера
Первым делом в методе preload мы загружаем исходный код шейдера из внешнего файла. Phaser позволяет загружать шейдеры с помощью метода load.glsl. В данном случае загружается фрагментный шейдер с ключом 'Marble'.
this.load.glsl('Marble', 'assets/shaders/marble.frag');
В методе create мы создаём сам шейдер. Для этого используется метод this.add.shader. Мы передаём конфигурационный объект, где указываем ключ загруженного шейдера и функцию setupUniforms для передачи uniform-переменных. В данном примере мы передаём uniform time, равный времени работы игры, что позволит шейдеру анимироваться. Последние четыре аргумента метода — это x, y, ширина и высота области рендеринга шейдера (128x128 пикселей).
const shader = this.add.shader({
name: 'Marble',
fragmentKey: 'Marble',
setupUniforms: (setUniform, drawingContext) => {
setUniform('time', this.game.loop.getDuration());
}
}, 0, 0, 128, 128);
Волшебный метод: setRenderToTexture
Ключевой момент — метод setRenderToTexture. Он перенаправляет вывод шейдера с экрана в текстуру внутри Texture Manager Phaser. В качестве аргумента мы передаём строковый ключ, по которому эта текстура будет доступна. После вызова этого метода шейдер больше не отрисовывается напрямую на дисплейный список (display list), а его результат сохраняется в текстуре с заданным ключом.
shader.setRenderToTexture('wibble');
Теперь в Texture Manager существует текстура с ключом 'wibble', которая содержит кадр, отрендеренный шейдером. Если шейдер анимируется (как в нашем случае, благодаря uniform time), текстура также будет обновляться каждый кадр.
Использование текстуры шейдера в игре
После того как текстура создана, с ней можно работать как с любым другим загруженным изображением. Мы создаём игровые объекты Image, используя ключ текстуры 'wibble'.
Первый объект — обычное изображение, отображающее текстуру в её исходном размере 128x128.
this.add.image(200, 300, 'wibble');
Второй объект демонстрирует, что текстуру можно масштабировать стандартными методами Phaser, например, setScale.
this.add.image(400, 300, 'wibble').setScale(2);
Третий объект показывает, что с такими изображениями можно работать в Tweens. Мы создаём твин, который анимирует положение по оси Y и угол поворота изображения. Это доказывает, что динамическая текстура шейдера и анимация его контейнера (изображения) работают полностью независимо и могут комбинироваться.
const mover = this.add.image(600, 100, 'wibble');
this.tweens.add({
targets: mover,
angle: 180,
y: 500,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true,
duration: 2000
});
Конфигурация игры и важное замечание
Для работы с шейдерами и рендерингом в текстуру тип рендерера в конфигурации игры должен быть установлен в Phaser.WEBGL. Рендерер Phaser.CANVAS не поддерживает эти функции.
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
Важно понимать, что после вызова setRenderToTexture сам объект шейдера (shader) больше не является частью дисплейного списка. Его единственная задача теперь — обновлять текстуру. Все визуальные манипуляции производятся с объектами Image (или другими), которые используют эту текстуру.
Что попробовать дальше
Метод setRenderToTexture открывает новые возможности для работы с шейдерами в Phaser. Вы можете создавать динамические текстуры для воды, огня, магии, помещать их в спрайты, анимировать эти спрайты и даже использовать текстуры в системах частиц. Для экспериментов попробуйте: создать текстуру шейдера большего размера и использовать её для фона уровня; применить одну текстуру шейдера одновременно к десяткам спрайтов для оптимизации; или сохранить текущий кадр шейдера в статическую текстуру с помощью this.textures.get('wibble').snapshot() для последующего использования.
