О чем этот пример
Стандартные визуальные эффекты в Phaser 3 — это хорошо, но настоящая гибкость начинается, когда вы создаете свои собственные. В этой статье мы разберем, как написать кастомный пост-обработчик (Post FX Pipeline) для применения эффекта оттенков серого к игровым объектам. Вы научитесь создавать шейдеры, управлять параметрами эффектов и интегрировать их в свой проект через плагины. Этот подход откроет вам путь к созданию уникальных визуальных стилей для ваших игр — от простых цветовых коррекций до сложных дисторсий.
Версия 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(100, 100, 'bob'); // Disappear in ios 15.4
const obj1 = this.add.sprite(110, 110, 'bob');
obj1.setPostPipeline(GrayScalePostFxPipeline);
this.add.sprite(120, 120, 'bob');
}
}
var config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Demo,
plugins: {
global: [{
key: 'rexGrayScalePipeline',
plugin: GrayScalePipelinePlugin,
start: true
}]
}
};
var game = new Phaser.Game(config);
Что такое Post FX Pipeline и зачем он нужен?
Post FX Pipeline (конвейер пост-обработки) в Phaser 3 — это мощный инструмент для применения графических эффектов к игровым объектам или всей сцене после основной отрисовки. В отличие от предустановленных эффектов, создание собственного пайплайна дает полный контроль над пикселями через шейдеры.
В нашем примере мы создаем эффект обесцвечивания (grayscale). Основная логика будет записана на языке шейдеров (GLSL), а Phaser возьмет на себя управление рендер-таргетами и интеграцию с игровым циклом.
Ключевой класс для создания — Phaser.Renderer.WebGL.Pipelines.PostFXPipeline. Наш пайплайн будет применяться не ко всей сцене, а к конкретным спрайтам, что позволяет гибко управлять визуалом.
Пишем шейдер: математика цвета
Сердце любого пост-эффекта — фрагментный шейдер (fragment shader). Именно он определяет, как будет изменен цвет каждого пикселя. Наш шейдер будет преобразовывать цветные пиксели в оттенки серого, смешивая результат с исходным изображением на основе параметра интенсивности intensity.
Формула преобразования цветного изображения в черно-белое использует взвешенную сумму каналов RGB. Коэффициенты (0.299, 0.587, 0.114) учитывают восприятие яркости человеческим глазом.
Вот сам шейдер:
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);
}\
`;
Разберем ключевые моменты:
- uMainSampler — это текстура, содержащая исходное изображение спрайта.
- outTexCoord — координаты текущего пикселя.
- Встроенная функция dot вычисляет скалярное произведение, что эквивалентно формуле: gray = r*0.299 + g*0.587 + b*0.114.
- Функция mix выполняет линейную интерполяцию между исходным цветом (front) и рассчитанным серым (vec4(gray, gray, gray, front.a)). Параметр intensity управляет силой эффекта (0 — исходный цвет, 1 — полностью серый).
Создаем класс пайплайна
Теперь обернем шейдер в класс JavaScript, который будет управлять его работой в Phaser. Наш класс GrayScalePostFxPipeline наследуется от базового PostFXPipeline.
Конструктор класса передает шейдер и настройки в родительский класс. Обратите внимание на закомментированный массив uniforms. В данном примере он не нужен, так как имена uniform-переменных (uMainSampler, 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() вызывается перед каждым рендером кадра и устанавливает текущее значение интенсивности в шейдер с помощью set1f. Это связывает JavaScript-переменную с uniform-переменной в шейдере.
onPreRender() {
this.set1f('intensity', this._intensity);
}
Для удобства управления интенсивностью мы создаем геттер, сеттер и вспомогательный метод setIntensity. Сеттер использует функцию Clamp (предполагается, что она определена в глобальной области видимости в исходном примере), чтобы ограничить значение диапазоном от 0 до 1. Метод setIntensity возвращает this, что позволяет использовать чейнинг.
get intensity() {
return this._intensity;
}
set intensity(value) {
this._intensity = Clamp(value, 0, 1);
}
setIntensity(value) {
this.intensity = value;
return this;
}
}
Интеграция через плагин
Чтобы использовать наш пайплайн в любом месте игры, лучше всего завернуть его в плагин. Плагин регистрирует пайплайн в рендерере Phaser, делая его доступным по строковому ключу.
Класс GrayScalePipelinePlugin наследуется от Phaser.Plugins.BasePlugin. В методе start(), который вызывается при запуске плагина, мы регистрируем наш пайплайн.
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);
}
}
Ключевая строка — this.game.renderer.pipelines.addPostPipeline('rexGrayScalePostFx', GrayScalePostFxPipeline). Она добавляет наш класс в систему пайплайнов рендерера под именем 'rexGrayScalePostFx'. Теперь к любому игровому объекту, поддерживающему пост-эффекты, можно применить наш фильтр, используя это имя.
Плагин подключается в конфигурации игры в разделе plugins.global.
plugins: {
global: [{
key: 'rexGrayScalePipeline',
plugin: GrayScalePipelinePlugin,
start: true
}]
}
Применение эффекта к спрайтам
Давайте посмотрим, как использовать созданный эффект в сцене. В методе create() мы создаем три одинаковых спрайта, но эффект применяем только ко второму.
create() {
this.add.sprite(100, 100, 'bob'); // Обычный цветной спрайт
const obj1 = this.add.sprite(110, 110, 'bob');
obj1.setPostPipeline(GrayScalePostFxPipeline); // Применяем наш пайплайн
this.add.sprite(120, 120, 'bob'); // Еще один обычный спрайт
}
Метод setPostPipeline объекта obj1 принимает в качестве аргумента класс нашего пайплайна (GrayScalePostFxPipeline). После этого к спрайту obj1 автоматически применяется шейдерный эффект. Если бы мы регистрировали пайплайн через строковый ключ (как показано в плагине), то можно было бы использовать альтернативный вариант:
obj1.setPostPipeline('rexGrayScalePostFx');
Интенсивностью эффекта теперь можно управлять в реальном времени, обратившись к экземпляру пайплайна:
// Предположим, у нас есть ссылка на pipelineInstance
pipelineInstance.intensity = 0.5; // Наполовину серый
// Или с использованием чейнинга
obj1.setPostPipeline(GrayScalePostFxPipeline).pipelines[0].setIntensity(0.8);
Что попробовать дальше
Вы создали полноценный кастомный пост-эффект для Phaser 3. Этот пример — отправная точка для экспериментов. Попробуйте изменить шейдер: добавьте сепию, инверсию цвета или простые дисторсии (например, волну). Управляйте параметрами эффекта из игровой логики — например, делайте объект черно-белым при получении урона. Исследуйте возможности применения пайплайна не к отдельным объектам, а ко всей камере через camera.setPostPipeline(). Главное — теперь у вас есть инструмент для воплощения любых визуальных идей прямо на GPU.
