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

Визуальные эффекты (PostFX) и маски — мощные инструменты для создания стильной графики в играх на Phaser. Однако их совместное использование может привести к неочевидным багам рендеринга: объекты могут становиться невидимыми, эффекты — пропадать, а маски — игнорироваться. В этой статье мы разберем пример из репозитория Phaser, который демонстрирует типичные конфликты, и сформулируем практические правила совместимости. Это сэкономит вам часы отладки при работе со сложными визуальными композициями.

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('pic', 'assets/pics/neotokyo-ai.jpg');
        this.load.image('logo', 'assets/sprites/phaser2.png');
        this.load.image('card', 'assets/pics/slug.png');
    }

    create ()
    {
        const m1 = this.make.graphics();
        m1.fillCircle(200, 300, 200);
        m1.fillCircle(600, 300, 200);
        const mask1 = m1.createGeometryMask();

        const a = this.add.image(400, 300, 'pic');
        const b = this.add.image(600, 300, 'logo');
        const c = this.add.image(200, 300, 'card');

        a.setMask(mask1);

        // const area1 = this.textures.addDynamicTexture('area1', 128, 128);
        // area1.fill(0x00ff00);
        // area1.drawFrame('card');
        // this.add.sprite(650, 100, 'area1');

        // a.preFX.addPixelate(4);
        // a.preFX.addBarrel(4);
        // a.postFX.addBarrel(4.0);
        // b.preFX.addBarrel(0.5);
        // b.postFX.addBarrel(0.5);
        // b.postFX.addPixelate(4);
        // c.postFX.addBarrel(0.5);
        // c.preFX.addBarrel(0.5);

        //  pre + pre + cam post = works
        //  post + pre + cam post = pre invisible
        //  pre + post + cam post = cam post fails
        //  post + pre + post + cam post = everything fails after first post (but cam post works)

        const m = this.make.graphics();
        // m.fillRectShape(new Phaser.Geom.Rectangle(50, 150, 700, 300));
        m.fillCircle(400, 300, 300);
        const mask = m.createGeometryMask();

        //  depth buffer true + camera mask and camera postFX = normal sprites work, postfx ones invisible
        //  depth buffer false + camera mask and camera postFX = mask ignored, fx works
        //  depth buffer false + sprite mask and sprite postFX = works
        //  depth buffer false + camera mask and sprite postFX = works
        //  depth buffer false + sprite mask and camera postFX = mask ignored, fx works
        //  depth buffer true + sprite mask and camera postFX = all works
        //  depth buffer true + sprite mask and sprite postFX = sprite invisible
        //  depth buffer true + camera mask and sprite postFX = sprite invisible
        //  depth buffer true + sprite postFX, camera empty = sprite works

        // mask.invertAlpha = true;

        this.cameras.main.setMask(mask);
        // this.cameras.main.postFX.addPixelate(4);
        // this.cameras.main.postFX.addGradient();
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    scene: Demo
};

const game = new Phaser.Game(config);

Базовый пример: маска для спрайта

В методе create создается простая сцена с тремя изображениями. Ключевой момент — применение геометрической маски к одному из них, изображению с ключом 'pic'.

Сначала создается графический объект m1 с помощью this.make.graphics(). На нем рисуются два круга.

const m1 = this.make.graphics();
m1.fillCircle(200, 300, 200);
m1.fillCircle(600, 300, 200);
const mask1 = m1.createGeometryMask();

Затем маска применяется к спрайту `aс помощью методаsetMask. В результате изображение 'pic' будет видно только в пределах нарисованных кругов. Другие спрайты (bиc`) маской не затрагиваются и отображаются полностью.

const a = this.add.image(400, 300, 'pic');
a.setMask(mask1);

Маски и эффекты на уровне камеры

Более сложный случай — использование маски и PostFX на основном cameras.main. В примере создается вторая маска в виде одного большого круга.

const m = this.make.graphics();
m.fillCircle(400, 300, 300);
const mask = m.createGeometryMask();

Эта маска устанавливается на камеру. Это значит, что ВСЕ, что видит эта камера (все объекты сцены), будет обрезано по форме круга.

this.cameras.main.setMask(mask);

В закомментированном коде также показано, как можно добавить эффекты на саму камеру, например, пикселизацию. Эффекты камеры применяются ко всему итоговому изображению, которое она рендерит, уже ПОСЛЕ применения ее маски.

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

Главный источник проблем: буфер глубины (depth buffer)

Конфликты возникают при одновременном использовании PostFX на отдельных спрайтах и масок на камере. Корень проблемы — состояние WebGL буфера глубины (depth buffer).

Когда depth buffer включен (по умолчанию в WebGL-рендерере Phaser), рендерер пытается корректно отсортировать объекты по глубине. Однако PostFX и маски изменяют порядок и способ отрисовки, что может сбить с толку механизм буфера глубины. В результате объекты с эффектами могут стать невидимыми.

Автор примера в комментариях перечислил множество комбинаций. Вот некоторые выводы: - depth buffer: false часто позволяет эффектам работать, но может ломать маски камеры. - depth buffer: true обеспечивает корректную работу масок, но может «прятать» спрайты с их собственными PostFX.

Настройка типа рендерера (и, следовательно, буфера глубины) задается в конфигурации игры. В данном примере используется Phaser.AUTO, который, скорее всего, выберет WebGL с включенным буфером глубины.

Практические правила совместимости

На основе исследования примера можно сформулировать рабочие подходы:

1. **Разделяй и властвуй.** Старайся не смешивать PostFX на спрайтах (sprite.postFX.add...) с масками на камере (camera.setMask). Это самая проблемная комбинация. 2. **Приоритет эффектам камеры.** Если нужны глобальные эффекты (на всю сцену) и обрезка по маске, применяй и то, и другое на уровне камеры. Это более стабильно. 3. **Отключай буфер глубины для сложных композиций.** Если тебе критически необходимы PostFX на отдельных объектах вместе с масками, попробуй отключить depth buffer в конфиге. Но будь готов к тому, что маски камеры могут перестать работать.

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    scene: Demo,
    render: {
        depthBuffer: false // Экспериментальная настройка
    }
};

4. **Тестируй в целевых браузерах.** Поведение рендерера может отличаться в зависимости от реализации WebGL.

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

Совместное использование масок и PostFX в Phaser требует осознанного выбора архитектуры рендеринга. Самый надежный путь — применять эффекты и маски на одном уровне: либо на отдельных спрайтах (для локальных изменений), либо на камере (для глобальных). Экспериментируй с настройкой depthBuffer в конфиге и тестируй комбинации из закомментированного кода примера, чтобы найти рабочее решение для твоего конкретного визуального замысла.