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

Применение эффектов PostFX к объектам с масками — частая задача в разработке игр. Однако стандартный способ может не сработать, если эффект применяется через `this.cameras.main.postFX`. Эта статья объясняет, почему эффекты обходят маскированные объекты, и предлагает рабочий альтернативный подход, используя `setPostPipeline`. Вы научитесь контролировать визуальные эффекты на сложных сценах, где маскирование обязательно.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


const fragShader = `
#define SHADER_NAME TEST

precision mediump float;

varying vec2 outTexCoord;
uniform sampler2D uMainSampler;

void main(){
    // do nothing
    gl_FragColor =  texture2D(uMainSampler, outTexCoord);
}
`;

 class TestPostFxPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline{
    constructor(game) {
        super({
            game,
            // renderTarget: true,
            fragShader,
            uniforms: ['uMainSampler'],
        });
    }
}

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    create ()
    {
        const text = this.add.text(20, 20, 'please click! post-fx pipeline: off').setFontSize(16);

        const graphics = this.add.graphics();

        graphics.fillStyle(0x00ff00).fillCircle(70, 100, 50);

        const arc = this.add.arc(120, 100, 50).setFillStyle(0xff0000, 1.0);

        arc.setMask(graphics.createGeometryMask());

        this.input.once('pointerdown', () => {

            text.text = 'post-fx pipeline: on';

            // this.cameras.main.setPostPipeline(TestPostFxPipeline);
            this.cameras.main.postFX.addPixelate(4);

        });
    }
}

const config = {
  width: 800,
  height: 600,
  scene: Example,
  parent: 'phaser-example',
  pipeline: [TestPostFxPipeline],
};

const game = new Phaser.Game(config);

Проблема: PostFX игнорирует маски

В предоставленном примере создаются две фигуры: зеленый круг, нарисованный через graphics, и красный круг (arc). Красному кругу назначается маска, созданная из графики. По клику включается пиксельный эффект через this.cameras.main.postFX.addPixelate(4).

Ожидается, что эффект пикселизации затронет всю сцену, включая замаскированный красный круг. Однако на практике эффект применяется только к фону и зеленой графике, в то время как красный круг остаётся неизменным. Это происходит потому, что стандартный метод добавления эффектов через postFX рендерит объекты сцены в отдельный буфер, не учитывая маски, которые применяются на более позднем этапе рендеринга.

this.cameras.main.postFX.addPixelate(4);

Решение: использование PostFX Pipeline

Для корректного применения эффектов к замаскированным объектам необходимо использовать систему пайплайнов напрямую. Вместо добавления эффекта через менеджер postFX, нужно установить пользовательский пайплайн для камеры с помощью метода setPostPipeline. Этот метод гарантирует, что эффект применяется после того, как все объекты сцены, включая их маски, отрендерены в основной буфер камеры.

В исходном коде уже объявлен пользовательский пайплайн TestPostFxPipeline. Хотя его шейдер по умолчанию ничего не изменяет (просто передаёт исходный цвет), он служит основой для добавления любых эффектов.

// Рабочий подход: установка пайплайна для камеры
this.cameras.main.setPostPipeline(TestPostFxPipeline);

Как работает кастомный пайплайн

Класс TestPostFxPipeline расширяет базовый класс Phaser.Renderer.WebGL.Pipelines.PostFXPipeline. Он требует минимальной конфигурации: ссылки на игру (game), фрагментный шейдер (fragShader) и массив униформ (uniforms). В данном случае шейдер просто берёт цвет из текстуры (uMainSampler) по координатам (outTexCoord) и выводит его без изменений.

Этот пайплайн регистрируется в конфигурации игры в поле pipeline. После этого его можно назначить любой камере.

class TestPostFxPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline{
    constructor(game) {
        super({
            game,
            fragShader,
            uniforms: ['uMainSampler'],
        });
    }
}

// Регистрация пайплайна в конфиге игры
const config = {
  // ... другие настройки
  pipeline: [TestPostFxPipeline],
};

Модификация шейдера для реального эффекта

Чтобы пайплайн действительно что-то делал, нужно изменить его фрагментный шейдер. Например, можно реализовать тот же эффект пикселизации. Шейдер будет дискретизировать текстуру с уменьшенным разрешением.

Здесь показан пример шейдера для пикселизации. Ключевые изменения: введена переменная pixelSize и математика для округления координат текстуры.

const fragShader = `
#define SHADER_NAME PIXELATE_FX

precision mediump float;

varying vec2 outTexCoord;
uniform sampler2D uMainSampler;
uniform float pixelSize;

void main() {
    // Округляем координаты текстуры, создавая эффект крупных пикселей
    vec2 coord = floor(outTexCoord / pixelSize) * pixelSize;
    gl_FragColor = texture2D(uMainSampler, coord);
}
`;

Затем в классе пайплайна нужно добавить униформу pixelSize и установить её значение, например, в методе onPreRender.

// В конструкторе пайплайна добавить uniform
uniforms: ['uMainSampler', 'pixelSize'],

// Где-то в коде сцены установить значение
const pipelineInstance = this.cameras.main.getPostPipeline(TestPostFxPipeline);
if (pipelineInstance) {
    pipelineInstance.setFloat1('pixelSize', 4.0 / this.game.scale.width);
}

Что попробовать дальше

Для применения PostFX-эффектов к объектам с масками используйте setPostPipeline вместо postFX.add. Это обеспечивает корректный порядок рендеринга. Готовый пайплайн можно модифицировать, добавив в шейдер эффекты инверсии цвета, размытия, хроматической аберрации или эффекта волны. Экспериментируйте, комбинируя несколько пайплайнов через MultiPipeline или анимируя uniform-переменные для создания динамичных визуальных переходов.