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