О чем этот пример
Визуальные эффекты — мощный инструмент для создания атмосферы и выделения игровых объектов. Phaser 3 предоставляет гибкую систему рендер-нодов и фильтров, позволяющую программировать собственные шейдерные эффекты прямо на JavaScript. В этой статье мы разберем, как создать кастомизированный фильтр, динамически меняющий цветовую палитру спрайта через вращение оттенка (hue rotation). Этот прием полезен для визуализации магических заклинаний, помеченных предметов, изменения времени суток или просто для добавления психоделических эффектов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
const hueFragShader = `
#define SHADER_NAME HUE_FS
precision mediump float;
uniform sampler2D uMainSampler;
uniform float uTime;
uniform float uSpeed;
varying vec2 outTexCoord;
varying float outTexId;
varying vec4 outTint;
varying vec2 fragCoord;
void main()
{
vec4 texture = texture2D(uMainSampler, outTexCoord);
float c = cos(uTime * uSpeed);
float s = sin(uTime * uSpeed);
mat4 r = mat4(0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.0, 0.0, 0.0, 1.0);
mat4 g = mat4(0.701, -0.587, -0.114, 0.0, -0.299, 0.413, -0.114, 0.0, -0.300, -0.588, 0.886, 0.0, 0.0, 0.0, 0.0, 0.0);
mat4 b = mat4(0.168, 0.330, -0.497, 0.0, -0.328, 0.035, 0.292, 0.0, 1.250, -1.050, -0.203, 0.0, 0.0, 0.0, 0.0, 0.0);
mat4 hueRotation = r + g * c + b * s;
gl_FragColor = texture * hueRotation;
}
`;
const FILTER_NAME = 'FilterColor';
const FilterColor = {
Controller: class ControllerColor extends Phaser.Filters.Controller {
constructor(camera) {
super(camera, FILTER_NAME);
this.hueSpeed = 0.001;
}
setHueSpeed(speed = 0.001) {
this.hueSpeed = speed;
return this;
}
},
Filter: class FilterColor extends Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader {
constructor(manager) {
super(FILTER_NAME, manager, null, hueFragShader);
}
setupUniforms(controller, drawingContext) {
const programManager = this.programManager;
programManager.setUniform('uTime', drawingContext.renderer.game.loop.time);
programManager.setUniform('uSpeed', controller.hueSpeed);
}
}
};
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('fish', 'assets/tests/pipeline/fish.png');
this.load.image('flower', 'assets/tests/pipeline/flower.png');
}
create ()
{
this.add.sprite(200, 300, 'fish');
this.filterController = new FilterColor.Controller();
const flower = this.add.sprite(400, 300, 'flower').enableFilters().filters.internal.add(this.filterController);
this.add.sprite(600, 300, 'fish');
this.input.on('pointerdown', () => {
this.filterController.setHueSpeed(0.001 + Math.random() * 0.01);
});
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#0a0067',
parent: 'phaser-example',
scene: Example,
renderNodes: { 'FilterColor': FilterColor.Filter }
};
let game = new Phaser.Game(config);
Архитектура кастомного фильтра: Контроллер и Шейдер
В Phaser 3 пользовательский фильтр состоит из двух основных частей: JavaScript-класса контроллера и GLSL-шейдера. Контроллер управляет параметрами фильтра (например, скоростью эффекта) из игровой логики, а шейдер выполняет пиксельные вычисления на GPU.
В нашем примере эти компоненты объединены в объект FilterColor. Класс ControllerColor наследуется от Phaser.Filters.Controller. Его ключевая задача — хранить состояние фильтра (параметр hueSpeed) и предоставлять метод setHueSpeed для его изменения.
const FILTER_NAME = 'FilterColor';
const FilterColor = {
Controller: class ControllerColor extends Phaser.Filters.Controller {
constructor(camera) {
super(camera, FILTER_NAME);
this.hueSpeed = 0.001;
}
setHueSpeed(speed = 0.001) {
this.hueSpeed = speed;
return this;
}
},
// ... Filter class будет рассмотрен далее
};
Класс FilterColor (шейдерный узел) наследуется от Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader. В его конструктор передается исходный код фрагментного шейдера на GLSL. Метод setupUniforms связывает переменные шейдера (uniforms) со значениями из контроллера и игрового времени.
Сердце эффекта: GLSL-шейдер вращения оттенка
Вся магия цвета происходит во фрагментном шейдере. Его код хранится в многострочной строке hueFragShader. Шейдер получает тексель (пиксель текстуры) и применяет к его цвету (vec4 texture) специальную матрицу преобразования hueRotation.
Ключевые шаги:
1. Шейдер объявляет uniform-переменные uTime (текущее время игры) и uSpeed (скорость вращения, задаваемая контроллером).
2. На основе времени и скорости вычисляются синус (`s) и косинус (c`).
3. Задаются три базовые матрицы (`r,g,b`), которые представляют разложение цвета в модели RGB.
4. Итоговая матрица вращения оттенка вычисляется по формуле: hueRotation = r + g * c + b * s. Эта линейная комбинация позволяет циклически менять цвет в пространстве RGB.
5. Итоговый цвет пикселя gl_FragColor получается умножением исходного цвета текстуры на рассчитанную матрицу.
const hueFragShader = `
#define SHADER_NAME HUE_FS
precision mediump float;
uniform sampler2D uMainSampler;
uniform float uTime;
uniform float uSpeed;
varying vec2 outTexCoord;
void main()
{
vec4 texture = texture2D(uMainSampler, outTexCoord);
float c = cos(uTime * uSpeed);
float s = sin(uTime * uSpeed);
mat4 r = mat4(0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.0, 0.0, 0.0, 1.0);
mat4 g = mat4(0.701, -0.587, -0.114, 0.0, -0.299, 0.413, -0.114, 0.0, -0.300, -0.588, 0.886, 0.0, 0.0, 0.0, 0.0, 0.0);
mat4 b = mat4(0.168, 0.330, -0.497, 0.0, -0.328, 0.035, 0.292, 0.0, 1.250, -1.050, -0.203, 0.0, 0.0, 0.0, 0.0, 0.0);
mat4 hueRotation = r + g * c + b * s;
gl_FragColor = texture * hueRotation;
}
`;
Метод setupUniforms в классе фильтра передает вычисленные значения в шейдер.
setupUniforms(controller, drawingContext) {
const programManager = this.programManager;
programManager.setUniform('uTime', drawingContext.renderer.game.loop.time);
programManager.setUniform('uSpeed', controller.hueSpeed);
}
Интеграция фильтра в сцену Phaser
Чтобы фильтр заработал в игре, его необходимо зарегистрировать в конфигурации рендерера и применить к нужным игровым объектам.
**Регистрация:** В конфиг игры, в поле renderNodes, добавляется наш класс шейдерного фильтра. Это говорит движку, что такой тип фильтра существует.
const config = {
// ... другие настройки
renderNodes: { 'FilterColor': FilterColor.Filter }
};
**Создание и применение:** В методе create сцены создается экземпляр контроллера фильтра. Затем спрайт подготавливается к работе с фильтрами вызовом .enableFilters(). Фильтр добавляется во внутренний список фильтров спрайта через .filters.internal.add(). Обратите внимание, что фильтр применяется только к спрайту flower, оставляя спрайты fish без изменений.
create ()
{
this.add.sprite(200, 300, 'fish');
this.filterController = new FilterColor.Controller();
const flower = this.add.sprite(400, 300, 'flower').enableFilters().filters.internal.add(this.filterController);
this.add.sprite(600, 300, 'fish');
// ... обработчик клика
}
**Интерактивность:** Для демонстрации динамического управления к сцене добавлен обработчик клика, который случайным образом меняет скорость вращения оттенка через метод контроллера setHueSpeed.
this.input.on('pointerdown', () => {
this.filterController.setHueSpeed(0.001 + Math.random() * 0.01);
});
Что попробовать дальше
Вы создали полноценный динамический визуальный фильтр для Phaser 3, работающий на GPU. Этот подход открывает путь к реализации множества эффектов: рассеивания (blur), свечения (glow), искажений (displacement) и цветокоррекции. Для экспериментов попробуйте изменить формулу в шейдере — закомментируйте матричное умножение и добавьте волновые эффекты через sin(outTexCoord.y * 10.0 + uTime). Или привяжите скорость вращения hueSpeed к здоровью игрока, чтобы цвет объекта менялся по мере получения урона. Изучите другие uniform-переменные, например, разрешение экрана, для создания более сложных зависимостей.
