О чем этот пример
Стандартные режимы наложения вроде `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).
