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

Режимы наложения (Blend Modes) — мощный инструмент для создания визуальных эффектов, от свечения и прозрачности до сложных миксов цветов. Встроенные режимы вроде 'ADD' или 'MULTIPLY' удобны, но иногда нужен полный контроль. В этой статье разберем пример из официального репозитория, который позволяет создавать и динамически изменять кастомные режимы наложения, используя низкоуровневые константы WebGL. Вы научитесь напрямую работать с рендерером Phaser для тонкой настройки графики в своих играх.

Версия 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.image('turkey', 'assets/pics/turkey-1985086.jpg');
        this.load.image('logo', 'assets/sprites/phaser-large.png');
    }

    create ()
    {
        const gl = this.sys.game.renderer.gl;

        const consts = [
            gl.ZERO,
            gl.ONE,
            gl.SRC_COLOR,
            gl.ONE_MINUS_SRC_COLOR,
            gl.DST_COLOR,
            gl.ONE_MINUS_DST_COLOR,
            gl.SRC_ALPHA,
            gl.ONE_MINUS_SRC_ALPHA,
            gl.DST_ALPHA,
            gl.ONE_MINUS_DST_ALPHA,
            gl.CONSTANT_COLOR,
            gl.ONE_MINUS_CONSTANT_COLOR,
            gl.CONSTANT_ALPHA,
            gl.ONE_MINUS_CONSTANT_ALPHA,
            gl.SRC_ALPHA_SATURATE
        ];

        const equations = [
            gl.FUNC_ADD,
            gl.FUNC_SUBTRACT,
            gl.FUNC_REVERSE_SUBTRACT
        ];

        const list = [
            'ZERO',
            'ONE',
            'SRC_COLOR',
            'ONE_MINUS_SRC_COLOR',
            'DST_COLOR',
            'ONE_MINUS_DST_COLOR',
            'SRC_ALPHA',
            'ONE_MINUS_SRC_ALPHA',
            'DST_ALPHA',
            'ONE_MINUS_DST_ALPHA',
            'CONSTANT_COLOR',
            'ONE_MINUS_CONSTANT_COLOR',
            'CONSTANT_ALPHA',
            'ONE_MINUS_CONSTANT_ALPHA',
            'SRC_ALPHA_SATURATE'
        ];

        const list2 = [
            'FUNC_ADD',
            'FUNC_SUBTRACT',
            'FUNC_REVERSE_SUBTRACT'
        ];

        let srcRGBIndex = 1;
        let dstRGBIndex = 7;
        let srcAlphaIndex = 1;
        let dstAlphaIndex = 1;
        let equationIndex = 0;

        let srcRGB = consts[srcRGBIndex];
        let dstRGB = consts[dstRGBIndex];
        let srcAlpha = consts[srcAlphaIndex];
        let dstAlpha = consts[dstAlphaIndex];

        let newMode = [ srcRGB, dstRGB, srcAlpha, dstAlpha ];

        let equation = equations[equationIndex];

        let renderer = this.sys.game.renderer;

        let modeIndex = renderer.addBlendMode(newMode, equation);

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

        this.add.rectangle(200, 300, 1, 600, 0xefefef);
        this.add.rectangle(400, 300, 1, 600, 0xefefef);
        this.add.rectangle(600, 300, 1, 600, 0xefefef);
        this.add.rectangle(400, 300, 800, 1, 0xefefef);

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

        var text = this.add.text(0, 0, 'Blend Mode', { color: '#ffffff' });

        text.setText([
            srcRGBIndex + ' = ' + list[srcRGBIndex],
            dstRGBIndex + ' = ' + list[dstRGBIndex],
            srcAlphaIndex + ' = ' + list[srcAlphaIndex],
            dstAlphaIndex + ' = ' + list[dstAlphaIndex],
            '',
            equationIndex + ' = ' + list2[equationIndex] + ' - ASR'
        ]);

        this.input.keyboard.on('keydown_A', function (event) {

            equationIndex = 0;
            equation = equations[equationIndex];

            renderer.updateBlendMode(modeIndex, newMode, equation);

            text.setText([
                srcRGBIndex + ' = ' + list[srcRGBIndex],
                dstRGBIndex + ' = ' + list[dstRGBIndex],
                srcAlphaIndex + ' = ' + list[srcAlphaIndex],
                dstAlphaIndex + ' = ' + list[dstAlphaIndex],
                '',
                equationIndex + ' = ' + list2[equationIndex] + ' - ASR'
            ]);

        });

        this.input.keyboard.on('keydown_S', function (event) {

            equationIndex = 1;
            equation = equations[equationIndex];

            renderer.updateBlendMode(modeIndex, newMode, equation);

            text.setText([
                srcRGBIndex + ' = ' + list[srcRGBIndex],
                dstRGBIndex + ' = ' + list[dstRGBIndex],
                srcAlphaIndex + ' = ' + list[srcAlphaIndex],
                dstAlphaIndex + ' = ' + list[dstAlphaIndex],
                '',
                equationIndex + ' = ' + list2[equationIndex] + ' - ASR'
            ]);

        });

        this.input.keyboard.on('keydown_R', function (event) {

            equationIndex = 2;
            equation = equations[equationIndex];

            renderer.updateBlendMode(modeIndex, newMode, equation);

            text.setText([
                srcRGBIndex + ' = ' + list[srcRGBIndex],
                dstRGBIndex + ' = ' + list[dstRGBIndex],
                srcAlphaIndex + ' = ' + list[srcAlphaIndex],
                dstAlphaIndex + ' = ' + list[dstAlphaIndex],
                '',
                equationIndex + ' = ' + list2[equationIndex] + ' - ASR'
            ]);

        });

        this.input.on('pointerup', function (pointer)
        {
            var x = Phaser.Math.Snap.Floor(pointer.x, 200, 0, true);
            var y = pointer.y;

            if (y > 300)
            {
                if (x === 0)
                {
                    srcRGBIndex = Phaser.Math.Wrap(srcRGBIndex + 1, 0, 15);
                }
                else if (x === 1)
                {
                    dstRGBIndex = Phaser.Math.Wrap(dstRGBIndex + 1, 0, 15);
                }
                else if (x === 2)
                {
                    srcAlphaIndex = Phaser.Math.Wrap(srcAlphaIndex + 1, 0, 15);
                }
                else if (x === 3)
                {
                    dstAlphaIndex = Phaser.Math.Wrap(dstAlphaIndex + 1, 0, 15);
                }
            }
            else
            {
                if (x === 0)
                {
                    srcRGBIndex = Phaser.Math.Wrap(srcRGBIndex - 1, 0, 15);
                }
                else if (x === 1)
                {
                    dstRGBIndex = Phaser.Math.Wrap(dstRGBIndex - 1, 0, 15);
                }
                else if (x === 2)
                {
                    srcAlphaIndex = Phaser.Math.Wrap(srcAlphaIndex - 1, 0, 15);
                }
                else if (x === 3)
                {
                    dstAlphaIndex = Phaser.Math.Wrap(dstAlphaIndex - 1, 0, 15);
                }
            }

            srcRGB = consts[srcRGBIndex];
            dstRGB = consts[dstRGBIndex];
            srcAlpha = consts[srcAlphaIndex];
            dstAlpha = consts[dstAlphaIndex];

            newMode = [ srcRGB, dstRGB, srcAlpha, dstAlpha ];

            renderer.updateBlendMode(modeIndex, newMode, equation);

            text.setText([
                srcRGBIndex + ' = ' + list[srcRGBIndex],
                dstRGBIndex + ' = ' + list[dstRGBIndex],
                srcAlphaIndex + ' = ' + list[srcAlphaIndex],
                dstAlphaIndex + ' = ' + list[dstAlphaIndex],
                '',
                equationIndex + ' = ' + list2[equationIndex] + ' - ASR'
            ]);
        });
    }

}

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

const game = new Phaser.Game(config);

Суть кастомных blend-режимов в WebGL

В основе лежит механизм WebGL, который определяет, как цвет нового пикселя (исходный, source) комбинируется с цветом пикселя уже в буфере кадра (целевой, destination). Это управляется функцией смешивания (blend function), которая задается для RGB и Alpha-каналов отдельно.

Функция описывается четырьмя параметрами: srcRGB, dstRGB, srcAlpha, dstAlpha. Каждый параметр — это константа, определяющая весовой коэффициент. Например, gl.SRC_ALPHA означает, что берется значение альфа-канала исходного пикселя. Также задается уравнение смешивания (equation), например, gl.FUNC_ADD, которое определяет операцию (сложение, вычитание) для комбинации этих коэффициентов.

Phaser абстрагирует работу с этими константами, предоставляя удобный API через объект рендерера.

Инициализация: доступ к WebGL и константы

В методе create() примера первым делом получаем доступ к объекту WebGL-рендерера. Затем создаются массивы, которые понадобятся для работы: числовые константы WebGL и их строковые представления для отображения.

const gl = this.sys.game.renderer.gl;

const consts = [
    gl.ZERO,
    gl.ONE,
    gl.SRC_COLOR,
    // ... другие константы
];

const list = [
    'ZERO',
    'ONE',
    'SRC_COLOR',
    // ... соответствующие названия
];

Инициализируются индексы для выбора параметров из этих массивов. По умолчанию выбраны, например, srcRGBIndex = 1 (gl.ONE) и dstRGBIndex = 7 (gl.ONE_MINUS_SRC_ALPHA). На основе индексов формируется массив newMode и выбирается уравнение.

Создание и применение кастомного режима

Phaser позволяет добавить новый режим наложения в свой внутренний список. Метод addBlendMode рендерера принимает массив параметров [srcRGB, dstRGB, srcAlpha, dstAlpha] и уравнение. Он возвращает индекс, под которым этот режим теперь доступен в игре.

let modeIndex = renderer.addBlendMode(newMode, equation);

Этот индекс можно использовать в методе .setBlendMode() для любого игрового объекта, например, спрайта. В примере сначала добавляется фоновая картинка, а поверх нее — логотип с нашим кастомным blend-режимом.

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

На экран также выводятся серые линии для визуального разделения интерфейса и текстовое поле, которое будет отображать текущие настройки.

Динамическое обновление параметров

Самая интересная часть примера — возможность менять параметры blend-режима «на лету». Для этого используется метод updateBlendMode рендерера. Он принимает индекс обновляемого режима, новый массив параметров и новое уравнение.

Логика изменения параметров привязана к кликам мыши. Координаты клика обрабатываются с помощью Phaser.Math.Snap.Floor и Phaser.Math.Wrap, чтобы определить, по какому из четырех вертикальных сегментов (подписанных индексами srcRGB, dstRGB, srcAlpha, dstAlpha) был клик и был ли он выше или ниже центра для увеличения или уменьшения индекса.

var x = Phaser.Math.Snap.Floor(pointer.x, 200, 0, true);
if (y > 300) {
    if (x === 0) {
        srcRGBIndex = Phaser.Math.Wrap(srcRGBIndex + 1, 0, 15);
    }
    // ... обработка других сегментов
}

После пересчета индексов формируется новый массив newMode и вызывается обновление:

renderer.updateBlendMode(modeIndex, newMode, equation);

Текст на экране также обновляется, чтобы отразить изменения.

Смена уравнения смешивания по клавишам

Помимо параметров, можно менять само уравнение смешивания. В примере это реализовано через обработчики клавиш A, S и R. Каждая клавиша задает свой equationIndex (0, 1, 2), соответствующий FUNC_ADD, FUNC_SUBTRACT или FUNC_REVERSE_SUBTRACT.

this.input.keyboard.on('keydown_A', function (event) {
    equationIndex = 0;
    equation = equations[equationIndex];
    renderer.updateBlendMode(modeIndex, newMode, equation);
    // ... обновление текста
});

Это демонстрирует, как можно полностью перестраивать математическую формулу смешивания цветов в реальном времени, что открывает простор для интерактивных визуальных экспериментов.

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

Этот пример показывает, как выйти за рамки стандартных blend-режимов в Phaser, получив прямой доступ к мощному, но низкоуровневому аппарату смешивания WebGL. Вы можете создавать уникальные визуальные фильтры, реагирующие на действия игрока, или тонко настраивать атмосферу сцены. **Идеи для экспериментов:** 1. Привяжите изменение параметров не к клику, а, например, к положению курсора или скорости движения спрайта. 2. Создайте несколько кастомных режимов для разных слоев игры и переключайтесь между ними. 3. Попробуйте использовать режимы наложения для пост-обработки, применяя их к целым группам объектов или рендер-текстурам.