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

Стандартные визуальные эффекты в 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.