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

Стандартные режимы наложения вроде `ADD` или `MULTIPLY` — это лишь верхушка айсберга. Phaser на WebGL позволяет создавать собственные режимы blend, используя низкоуровневые константы и уравнения WebGL. Это открывает двери к уникальным визуальным эффектам, которые невозможно достичь стандартными средствами — от сложных переходов между слоями до симуляции нестандартных материалов и свечения. В этой статье разберем пример, который даст вам прямой доступ к механике наложения пикселей.

Версия 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('face', 'assets/pics/bw-face.png');
        this.load.image('src', 'assets/tests/blendmode/src.png');
        this.load.image('dst', 'assets/tests/blendmode/dst.png');
        this.load.image('logo', 'assets/sprites/phaser-large.png');
    }

    create ()
    {
        //  WebGL only:
        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 = [
            { val: 0, text: 'ZERO' },
            { val: 1, text: 'ONE' },
            { val: 2, text: 'SRC_COLOR' },
            { val: 3, text: 'ONE_MINUS_SRC_COLOR' },
            { val: 4, text: 'DST_COLOR' },
            { val: 5, text: 'ONE_MINUS_DST_COLOR' },
            { val: 6, text: 'SRC_ALPHA' },
            { val: 7, text: 'ONE_MINUS_SRC_ALPHA' },
            { val: 8, text: 'DST_ALPHA' },
            { val: 9, text: 'ONE_MINUS_DST_ALPHA' },
            { val: 10, text: 'CONSTANT_COLOR' },
            { val: 11, text: 'ONE_MINUS_CONSTANT_COLOR' },
            { val: 12, text: 'CONSTANT_ALPHA' },
            { val: 13, text: 'ONE_MINUS_CONSTANT_ALPHA' },
            { val: 14, text: 'SRC_ALPHA_SATURATE' }
        ];

        const list2 = [
            { val: 0, text: 'FUNC_ADD' },
            { val: 1, text: 'FUNC_SUBTRACT' },
            { val: 2, text: 'FUNC_REVERSE_SUBTRACT' }
        ];

        const newMode = [ gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA ];
        let equation = equations[0];

        let renderer = this.sys.game.renderer;

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

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

        //  zero, one, one, zero, add

        //  Create the 4 select lists

        let srcRGB = $('<select>').attr('id', 'srcRGB').data('idx', 0).appendTo('body');
        let dstRGB = $('<select>').attr('id', 'dstRGB').data('idx', 1).appendTo('body');
        let srcAlpha = $('<select>').attr('id', 'srcAlpha').data('idx', 2).appendTo('body');
        let dstAlpha = $('<select>').attr('id', 'dstAlpha').data('idx', 3).appendTo('body');

        $(list).each(function() {

            srcRGB.append($("<option>").attr('value', this.val).text(this.text));
            dstRGB.append($("<option>").attr('value', this.val).text(this.text));
            srcAlpha.append($("<option>").attr('value', this.val).text(this.text));
            dstAlpha.append($("<option>").attr('value', this.val).text(this.text));

        });

        srcRGB.val('6').change();
        dstRGB.val('7').change();
        srcAlpha.val('1').change();
        dstAlpha.val('7').change();

        $('#srcRGB, #dstRGB, #srcAlpha, #dstAlpha').change(function () {

            let idx = $(this).data('idx');

            newMode[idx] = consts[this.value];

            renderer.updateBlendMode(modeIndex, newMode, equation);

        });

        let equ = $('<select>').attr('id', 'equ').appendTo('body');

        $(list2).each(function() {
            equ.append($("<option>").attr('value', this.val).text(this.text));
        });

        $(equ).on('change', function () {

            equation = equations[this.value];

            renderer.updateBlendMode(modeIndex, newMode, equation);

        });

    }

}

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

const game = new Phaser.Game(config);

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

В основе любого наложения в WebGL лежит простое уравнение, определяющее, как цвет пикселя источника (source) комбинируется с цветом пикселя назначения (destination) в буфере кадра. Phaser абстрагирует WebGL, но предоставляет доступ к его "сырым" константам через свойство gl рендерера.

Каждый режим описывается четырьмя параметрами: - srcRGB: Как учитывается цвет источника для RGB-компонент. - dstRGB: Как учитывается цвет назначения для RGB-компонент. - srcAlpha: Как учитывается альфа-канал источника. - dstAlpha: Как учитывается альфа-канал назначения.

И уравнением (например, FUNC_ADD), которое определяет операцию (сложение, вычитание) между этими учтенными значениями.

Доступ к WebGL и подготовка констант

Первым делом в методе create мы получаем доступ к объекту WebGL рендерера игры. Все последующие операции будут опираться на его константы.

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

Далее создаются массивы с доступными константами и уравнениями WebGL. В примере они дублируются в удобном для интерфейса формате (list, list2). Ключевые константы: - gl.ONE: Полное использование цвета/альфы (умножение на 1). - gl.SRC_ALPHA: Использование альфа-канала источника. - gl.ONE_MINUS_SRC_ALPHA: Использование инвертированного альфа-канала источника (1 - alpha). - gl.ZERO: Игнорирование цвета/альфы (умножение на 0).

Массив newMode изначально настроен на классический режим прозрачности: [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA].

Создание и применение нового режима

Phaser позволяет добавить новый режим наложения в рендерер и получить его индекс для использования. Это делается методом addBlendMode.

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

Полученный modeIndex можно передать в метод .setBlendMode() любого игрового объекта (спрайта, изображения). В примере режим применяется к изображению лица, которое накладывается поверх логотипа Phaser.

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

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

Самая мощная часть примера — возможность менять параметры режима на лету с помощью выпадающих списков (селектов). Для каждого параметра (srcRGB, dstRGB, srcAlpha, dstAlpha и уравнение) создается HTML-элемент <select>, наполненный вариантами из подготовленных списков.

При изменении значения в любом селекте вызывается обработчик, который: 1. Обновляет соответствующий элемент в массиве newMode на новую WebGL-константу. 2. Вызывает renderer.updateBlendMode(modeIndex, newMode, equation) для мгновенного обновления режима в рендерере.

$('#srcRGB, #dstRGB, #srcAlpha, #dstAlpha').change(function () {
    let idx = $(this).data('idx'); // 0,1,2,3
    newMode[idx] = consts[this.value]; // Обновляем константу в массиве
    renderer.updateBlendMode(modeIndex, newMode, equation); // Применяем
});

Это позволяет в реальном времени видеть, как каждая константа влияет на финальное изображение.

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

Кастомные режимы наложения — это продвинутый инструмент для создания эксклюзивных визуальных эффектов в ваших играх на Phaser. Экспериментируйте: попробуйте симулировать эффекты типа «экран» (screen) или «жесткого света» (hard light), которых нет в стандартном наборе Phaser. Используйте разные уравнения (FUNC_SUBTRACT) для создания эффектов вычитания. Помните, что этот функционал работает только при рендеринге через WebGL (type: Phaser.WEBGL).