О чем этот пример
Иногда стандартных визуальных эффектов недостаточно, и хочется добавить в игру собственную обработку изображения. В Phaser для этого предусмотрена система пост-обработки на основе WebGL. В этой статье мы разберём пример создания плагина с простым, но полезным эффектом — постепенным обесцвечиванием (grayscale). Вы научитесь писать собственные шейдеры, управлять их параметрами и интегрировать эффект в камеру сцены. Этот навык откроет дорогу к созданию уникальных визуальных стилей для ваших игр, будь то стилизация под старую плёнку, эффекты повреждения или смены времени суток.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
const frag = `\
#ifdef GL_FRAGMENT_PRECISION_HIGH
#define highmedp highp
#else
#define highmedp mediump
#endif
precision highmedp float;
// Scene buffer
uniform sampler2D uMainSampler;
varying vec2 outTexCoord;
// Effect parameters
uniform float intensity;
void main (void) {
vec4 front = texture2D(uMainSampler, outTexCoord);
float gray = dot(front.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = mix(front, vec4(gray, gray, gray, front.a), intensity);
}\
`;
class GrayScalePostFxPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
constructor(game) {
super({
game: game,
renderTarget: true,
fragShader: frag,
uniforms: [
'uMainSampler',
'intensity'
]
});
this._intensity = 1;
}
onPreRender() {
this.set1f('intensity', this._intensity);
}
// intensity
get intensity() {
return this._intensity;
}
set intensity(value) {
this._intensity = Clamp(value, 0, 1);
}
setIntensity(value) {
this.intensity = value;
return this;
}
}
class GrayScalePipelinePlugin extends Phaser.Plugins.BasePlugin {
constructor(pluginManager) {
super(pluginManager);
}
start() {
console.log('plugin start');
var eventEmitter = this.game.events;
eventEmitter.on('destroy', this.destroy, this);
this.game.renderer.pipelines.addPostPipeline('rexGrayScalePostFx', GrayScalePostFxPipeline);
}
}
class Demo extends Phaser.Scene {
constructor() {
super({
key: 'examples'
})
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bob', 'assets/sprites/apple.png');
}
create() {
this.add.sprite(200, 200, 'bob');
this.add.sprite(400, 200, 'bob');
this.add.sprite(600, 200, 'bob');
this.cameras.main.setPostPipeline(GrayScalePostFxPipeline);
}
}
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Demo,
plugins: {
global: [{
key: 'rexGrayScalePipeline',
plugin: GrayScalePipelinePlugin,
start: true
}]
}
};
var game = new Phaser.Game(config);
Архитектура пост-эффектов в Phaser
Phaser использует конвейеры (pipelines) для обработки графики. Пост-эффекты реализуются через PostFXPipeline. Это специальный конвейер, который применяется после отрисовки всей сцены (или конкретной камеры) и может модифицировать итоговый кадр.
Ключевые компоненты для создания эффекта: 1. **Фрагментный шейдер (GLSL код)**: программа, которая обрабатывает каждый пиксель итогового изображения. 2. **Класс конвейера**: JavaScript-класс, который управляет шейдером, передаёт в него параметры (uniforms) и настраивает его работу. 3. **Плагин**: необязательный, но удобный компонент для регистрации вашего конвейера в системе Phaser, чтобы использовать его в любом месте проекта.
В нашем примере эффект применяется ко всей основной камере с помощью метода setPostPipeline.
Разбор шейдера: как работает обесцвечивание
Сердце эффекта — фрагментный шейдер, написанный на GLSL. Он получает на вход текстуру отрендеренной сцены (uMainSampler) и для каждого пикселя вычисляет его яркость, а затем смешивает оригинальный цвет с оттенком серого.
const frag = `\
#ifdef GL_FRAGMENT_PRECISION_HIGH
#define highmedp highp
#else
#define highmedp mediump
#endif
precision highmedp float;
// Scene buffer
uniform sampler2D uMainSampler;
varying vec2 outTexCoord;
// Effect parameters
uniform float intensity;
void main (void) {
vec4 front = texture2D(uMainSampler, outTexCoord);
float gray = dot(front.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = mix(front, vec4(gray, gray, gray, front.a), intensity);
}\
`;
Разберём ключевые моменты:
- uniform sampler2D uMainSampler — текстура, содержащая изображение сцены.
- uniform float intensity — параметр интенсивности эффекта (от 0 до 1), который мы будем контролировать из JavaScript.
- float gray = dot(front.rgb, vec3(0.299, 0.587, 0.114)) — стандартная формула для перевода цвета в оттенок серого. Она учитывает восприятие яркости разными цветовыми каналами человеческим глазом.
- gl_FragColor = mix(front, vec4(gray, gray, gray, front.a), intensity) — функция mix линейно интерполирует между исходным цветом (front) и серым вариантом, основываясь на значении intensity. При intensity = 0 цвет не меняется, при intensity = 1 — полностью чёрно-белый.
Создание класса конвейера PostFXPipeline
Класс GrayScalePostFxPipeline наследуется от Phaser.Renderer.WebGL.Pipelines.PostFXPipeline. Его задача — связать шейдер с движком и предоставить API для управления.
class GrayScalePostFxPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
constructor(game) {
super({
game: game,
renderTarget: true,
fragShader: frag,
uniforms: [
'uMainSampler',
'intensity'
]
});
this._intensity = 1;
}
В конструкторе мы передаём конфигурацию в родительский класс:
- fragShader: исходный код нашего шейдера.
- uniforms: массив строк с именами uniform-переменных, которые нужно связать. uMainSampler связывается автоматически, а для intensity мы создаём сеттер.
- renderTarget: true указывает, что конвейеру требуется собственный буфер для рендеринга.
Метод onPreRender вызывается перед каждым рендером кадра и обновляет значение uniform-переменной в шейдере:
onPreRender() {
this.set1f('intensity', this._intensity);
}
Для удобства управления интенсивностью реализованы геттер, сеттер и метод-чейн:
get intensity() {
return this._intensity;
}
set intensity(value) {
this._intensity = Clamp(value, 0, 1);
}
setIntensity(value) {
this.intensity = value;
return this;
}
}
Обратите внимание на Clamp — это вспомогательная функция (не показана в примере, но подразумевается), которая ограничивает значение в диапазоне [0, 1].
Интеграция через плагин и применение к сцене
Плагин GrayScalePipelinePlugin регистрирует наш конвейер в рендерере Phaser, делая его доступным по строковому ключу 'rexGrayScalePostFx'.
class GrayScalePipelinePlugin extends Phaser.Plugins.BasePlugin {
start() {
this.game.renderer.pipelines.addPostPipeline('rexGrayScalePostFx', GrayScalePostFxPipeline);
}
}
Плагин добавляется в конфигурацию игры:
plugins: {
global: [{
key: 'rexGrayScalePipeline',
plugin: GrayScalePipelinePlugin,
start: true
}]
}
В сцене Demo эффект применяется к основной камере после создания спрайтов:
create() {
this.add.sprite(200, 200, 'bob');
this.add.sprite(400, 200, 'bob');
this.add.sprite(600, 200, 'bob');
this.cameras.main.setPostPipeline(GrayScalePostFxPipeline);
}
Метод setPostPipeline принимает класс конвейера. После этого каждый кадр, отрендеренный этой камерой, будет проходить через наш шейдер. Если нужно применить эффект к конкретному игровому объекту, можно использовать свойство postPipeline у самого объекта.
Управление эффектом в реальном времени
Сила кастомных конвейеров — в возможности динамически менять их параметры. Получив экземпляр конвейера, можно анимировать интенсивность.
Сначала получим ссылку на конвейер после его установки:
create() {
// ... создание спрайтов
this.cameras.main.setPostPipeline(GrayScalePostFxPipeline);
this.postFX = this.cameras.main.getPostPipeline(GrayScalePostFxPipeline);
}
Затем, например, в методе update, можно плавно менять интенсивность:
update(time, delta) {
if (this.postFX) {
// Плавное колебание интенсивности от 0 до 1
let newIntensity = 0.5 + 0.5 * Math.sin(time / 1000);
this.postFX.intensity = newIntensity;
}
}
Такой приём можно использовать для создания эффекта "вспышки" при получении урона, постепенного перехода в чёрно-белую гамму при потере здоровья или стилистического затемнения. Все изменения параметра intensity автоматически передаются в шейдер перед каждым рендером благодаря методу onPreRender.
Что попробовать дальше
Создание собственных пост-эффектов в Phaser — мощный инструмент для уникализации визуального стиля игры. Мы разобрали полный цикл: от написания GLSL-шейдера до интеграции эффекта через плагин. Экспериментируйте с формулами внутри шейдера: попробуйте создать эффект сепии, инверсии цвета, размытия или виньетирования. Помните, что пост-эффекты могут быть ресурсоёмкими — применяйте их точечно к конкретным камерам или объектам для оптимальной производительности.
