О чем этот пример
Использование шейдеров и масок открывает двери к созданию уникальной атмосферы и сложных визуальных эффектов в играх на Phaser, которые сложно достичь стандартными средствами рендеринга. Эта статья на примере официального демо покажет, как загружать и применять GLSL-шейдеры, создавать маски для камеры и отдельных объектов, а также динамически обновлять uniform-переменные шейдера для анимации эффекта прямо во время выполнения игры. Вы научитесь комбинировать эти техники для создания слоистой графики с фильтрами, что особенно полезно для стилизации сцен, создания порталов, искажений или магических эффектов.
Версия 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.glsl('spiralTime', 'assets/shaders/spiralTime.frag');
this.load.image('pic', 'assets/pics/rick-and-morty-by-sawuinhaff-da64e7y.png');
this.load.image('logo', 'assets/sprites/phaser3-logo-x2.png');
this.load.image('splat1', 'assets/pics/splat1.png');
this.load.image('splat3', 'assets/pics/splat3.png');
}
create ()
{
const maskImage1 = this.make.image({ x: 400, y: 300, key: 'splat1', add: false });
const maskImage2 = this.make.image({ x: 400, y: 300, key: 'splat3', add: false });
this.cameras.main.filters.external.addMask(maskImage1);
this.add.image(400, 300, 'pic');
const shader = this.add.shader({
name: 'spiralTime',
fragmentKey: 'spiralTime',
setupUniforms: (setUniform, drawingContext) =>
{
setUniform('time', this.game.loop.getDuration());
},
}, 400, 300, this.scale.width, this.scale.width);
shader.enableFilters().filters.external.addMask(maskImage2);
this.text = this.add.text(80, 320, '', { font: '16px Courier', fill: '#00ff00' }).setName('text');
this.add.image(400, 300, 'logo').setName('logo');
}
update ()
{
if (this.text)
{
this.text.setText([
this.sys.game.loop.getDuration(),
this.sys.game.loop.getDurationMS()
]);
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Загрузка ресурсов: шейдеры и изображения
В методе preload происходит подготовка всех необходимых ресурсов. Ключевой момент — загрузка GLSL-шейдера с помощью this.load.glsl. Phaser обрабатывает файл с исходным кодом шейдера как текстовый ресурс с заданным ключом. Также загружаются фоновое изображение, логотип и две текстуры, которые будут использоваться в качестве масок.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.glsl('spiralTime', 'assets/shaders/spiralTime.frag');
this.load.image('pic', 'assets/pics/rick-and-morty-by-sawuinhaff-da64e7y.png');
this.load.image('logo', 'assets/sprites/phaser3-logo-x2.png');
this.load.image('splat1', 'assets/pics/splat1.png');
this.load.image('splat3', 'assets/pics/splat3.png');
}
Создание масок и применение к камере
В методе create сначала создаются два объекта-изображения, которые не добавляются на дисплейный список сцены (add: false). Они служат исключительно как источники данных для масок. Первая маска (maskImage1) применяется непосредственно к основному фильтру камеры (this.cameras.main.filters.external). Это означает, что всё, что рисует эта камера, будет обрезано по форме этой маски. После этого на сцену добавляется фоновое изображение, которое уже будет отрисовано с учётом маски камеры.
const maskImage1 = this.make.image({ x: 400, y: 300, key: 'splat1', add: false });
const maskImage2 = this.make.image({ x: 400, y: 300, key: 'splat3', add: false });
this.cameras.main.filters.external.addMask(maskImage1);
this.add.image(400, 300, 'pic');
Добавление и настройка шейдера с маской
Далее создаётся объект шейдера с помощью this.add.shader. В его конфигурации указывается ключ загруженного фрагментного шейдера и функция setupUniforms, которая вызывается для установки uniform-переменных. В данном примере в шейдер передаётся текущее время работы игры this.game.loop.getDuration(). Это классический приём для анимации шейдерных эффектов. После создания шейдера для него включаются фильтры (enableFilters()), и к ним добавляется вторая маска (maskImage2). Эта маска влияет только на область отрисовки самого шейдера, а не на всю сцену.
const shader = this.add.shader({
name: 'spiralTime',
fragmentKey: 'spiralTime',
setupUniforms: (setUniform, drawingContext) =>
{
setUniform('time', this.game.loop.getDuration());
},
}, 400, 300, this.scale.width, this.scale.width);
shader.enableFilters().filters.external.addMask(maskImage2);
Динамическое обновление и отладка
В сцену добавляется текстовый объект и логотип. В методе update каждому кадру обновляется текст, отображающий текущее время работы игры в секундах и миллисекундах, полученное через this.sys.game.loop. Это демонстрирует, как можно передавать динамически меняющиеся данные из игрового цикла в шейдер (через функцию setupUniforms), а также полезно для отладки и понимания работы цикла.
this.text = this.add.text(80, 320, '', { font: '16px Courier', fill: '#00ff00' }).setName('text');
this.add.image(400, 300, 'logo').setName('logo');
update ()
{
if (this.text)
{
this.text.setText([
this.sys.game.loop.getDuration(),
this.sys.game.loop.getDurationMS()
]);
}
}
Что попробовать дальше
Комбинируя маски для камеры и для отдельных объектов со шейдерами, вы получаете мощный инструмент для постобработки и создания сложных визуальных композиций в Phaser. Для экспериментов попробуйте: анимировать положение или масштаб масок во время игры, использовать разные шейдеры для создания эффектов воды, огня или искажения, применять маски не к камере, а к другим слоям или группам объектов для создания многоуровневых эффектов.
