О чем этот пример

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