О чем этот пример
Режимы наложения (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. Попробуйте использовать режимы наложения для пост-обработки, применяя их к целым группам объектов или рендер-текстурам.
