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

Визуальные эффекты — мощный инструмент для создания атмосферы в игре. Шейдеры позволяют применять сложные пост-обработки, такие как свечение, искажение или стилизация, ко всему, что видит камера. Эта статья на практическом примере показывает, как загрузить GLSL-шейдер, применить его как фильтр к основной камере и динамически управлять его параметрами, чтобы оживить сцену. Вы научитесь работать с фрагментными шейдерами в Phaser, настраивать униформы (uniforms) для анимации эффекта и комбинировать шейдерный слой с обычными спрайтами. Этот подход открывает путь к созданию уникального визуального стиля для вашего проекта.

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

Живой запуск

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

Исходный код


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

    preload()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.glsl('machineshaman', 'assets/shaders/machine-shaman.frag');
        this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
        this.load.image('mask', 'assets/tests/camera/grunge-mask.png');
    }

    create()
    {
        this.cameras.main.filters.internal.addMask('mask');

        this.add.shader({
            name: 'machineshaman',
            fragmentKey: 'machineshaman',
            setupUniforms: (setUniform, drawingContext) => {
                setUniform('time', this.game.loop.getDuration());
            },
        }, 400, 300, 800, 800);

        this.add.image(400, 300, 'logo');
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка ресурсов: шейдер и изображения

Для работы с шейдером его исходный код нужно загрузить как текстовый ресурс. В Phaser для этого используется метод load.glsl(). Вместе с ним загружаются изображения, которые будут использоваться в сцене: логотип для отображения и маска для фильтра.

preload() {
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.glsl('machineshaman', 'assets/shaders/machine-shaman.frag');
    this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
    this.load.image('mask', 'assets/tests/camera/grunge-mask.png');
}

* load.setBaseURL() задаёт базовый URL для всех последующих загрузок, что удобно при работе с ресурсами из одного источника. * load.glsl('machineshaman', ...) загружает файл с фрагментным шейдером и присваивает ему ключ 'machineshaman'. Этот ключ понадобится позже для создания шейдерного объекта. * load.image() загружает два спрайта: логотип и маску.

Применение маски к фильтрам камеры

Перед созданием шейдера к основной камере применяется маска. Маска ограничивает область действия фильтров камеры, что позволяет создавать эффекты только на части экрана (например, виньетирование).

create() {
    this.cameras.main.filters.internal.addMask('mask');
    // ... остальной код
}

* this.cameras.main получает доступ к основной камере сцены. * filters.internal.addMask('mask') применяет загруженное ранее изображение с ключом 'mask' в качестве маски для системы фильтров этой камеры. Всё, что отрендерит камера, будет обрезано или замаскировано согласно прозрачности этого изображения.

Создание и настройка шейдерного объекта

Сердце примера — добавление шейдера на сцену. Он создаётся как отдельный объект с заданными размером и позицией, поверх которого можно отрисовывать обычные спрайты.

this.add.shader({
    name: 'machineshaman',
    fragmentKey: 'machineshaman',
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    },
}, 400, 300, 800, 800);
*   `this.add.shader()` создаёт новый объект шейдера и добавляет его на сцену.
*   Конфигурационный объект:
    *   `fragmentKey: 'machineshaman'` указывает, какой загруженный шейдерный код использовать.
    *   `setupUniforms` — это коллбэк, который вызывается каждый кадр для обновления униформ шейдера (переменных, передаваемых из JavaScript в шейдер).
    *   Внутри коллбэка `setUniform('time', this.game.loop.getDuration())` передаёт в шейдер под именем `'time'` текущее время работы игры в миллисекундах. Это стандартный приём для анимации шейдерных эффектов (например, для движения волн или изменения цвета).
*   Аргументы `400, 300, 800, 800` задают позицию (`x`, `y`) и размеры (`width`, `height`) шейдерного объекта на сцене.

Добавление графики поверх шейдера

Шейдерный объект рендерится как слой. Чтобы поверх него отображалась обычная 2D-графика (спрайты, текст), её нужно добавить после создания шейдера.

this.add.image(400, 300, 'logo');

* this.add.image() добавляет загруженное изображение логотипа в центр сцены (координаты 400, 300). * Поскольку этот вызов идёт после add.shader(), логотип будет отрисован поверх шейдерного слоя, что позволяет комбинировать сложные фоновые эффекты с чёткими интерфейсными элементами.

Базовая конфигурация игры (WebGL)

Для работы с шейдерами и фильтрами камеры рендерер игры должен быть Phaser.WEBGL. В Canvas-режиме эти функции недоступны.

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

* type: Phaser.WEBGL — обязательный параметр, активирующий WebGL-рендерер. * scene: Example указывает, что класс Example будет использоваться как начальная сцена.

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

Вы освоили базовый паттерн добавления анимированных шейдерных эффектов в Phaser 3: от загрузки кода до интеграции в игровую сцену. Теперь вы можете экспериментировать: попробуйте заменить шейдер на свой собственный из GLSL Sandbox, передавать в setupUniforms не только время, но и позицию мыши (this.input.activePointer), или применить фильтр не к фону, а к конкретному спрайту, используя sprite.setPipeline(). Сочетание таких техник поможет создать уникальную визуальную идентичность вашей игры.