Шейдеры
Собственные WebGL-эффекты в Phaser 4 — встроенная библиотека фильтров (Filter), свой GLSL через игровой объект Shader и место RenderNodes во всём этом.
Система Pipeline из Phaser 3 в Phaser 4 отсутствует. Три замены покрывают почти все сценарии:
| Чего хотите | Используйте |
|---|---|
| Встроенный эффект на спрайте или камере | Фильтр (obj.filters.internal.addX()) |
| Собственный фрагментный шейдер GLSL в виде квада | Игровой объект Shader |
| Целый новый шаг отрисовки внутри рендерера | Собственный RenderNode |
Пайплайны, setPipeline, setPostPipeline и PostFXPipeline больше не существуют. Если вы портируете проект с v3, см. Миграция с Phaser 3 для механических замен.
Фильтры: встроенный каталог
Каждый игровой объект и каждая камера предоставляют свойство filters со списками фильтров internal и external. Внутренние (internal) фильтры воздействуют на сам объект; внешние (external) фильтры воздействуют на контекст отрисовки (как правило, на весь экран).
const camera = this.cameras.main;
// Встроенные эффекты в одну строку.
camera.filters.internal.addBarrel(0.6);
camera.filters.external.addVignette(0.5);
camera.filters.external.addGlow(0xffaa00);
Полный набор поставляется с v4: Barrel, Blend, Blocky, Blur, Bokeh, ColorMatrix, CombineColorMatrix, Displacement, Glow, GradientMap, ImageLight, Key, Mask, NormalTools, PanoramaBlur, ParallelFilters, Pixelate, Quantize, Sampler, Shadow, Threshold, TiltShift, Vignette, Wipe. Каждый вызов addX() возвращает контроллер, который можно настроить или удалить позже:
const glow = camera.filters.internal.addGlow();
glow.outerStrength = 4;
camera.filters.internal.remove(glow);
Фильтры компонуются стопкой — порядок добавления совпадает с порядком применения.
:::note[Bloom в v4 не является отдельным фильтром]
В v3 был эффект Bloom; в v4 метода addBloom() нет. Используйте Phaser.Actions.AddEffectBloom(target, config?) — он внутренне настраивает несколько фильтров через ParallelFilters и возвращает объект со ссылками на каждый из них, которые можно изменять для анимации bloom.
:::
Игровой объект Shader
Для произвольного GLSL this.add.shader(config, x, y, w, h) рисует один квад с вашим фрагментным шейдером. Конструктор принимает ShaderQuadConfig — единый объект, а не позиционные аргументы, как в v3.
const fragSource = `
precision mediump float;
uniform vec2 uResolution;
uniform float uTime;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy;
vec3 col = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0));
gl_FragColor = vec4(col, 1.0);
}
`;
create() {
const shader = this.add.shader(
{
name: 'rainbow',
fragmentSource: fragSource,
initialUniforms: { uTime: 0, uResolution: [480, 320] },
setupUniforms: (setUniform) => {
setUniform('uTime', this.time.now / 1000);
setUniform('uResolution', [this.scale.width, this.scale.height]);
},
},
240, 160, 480, 320,
);
}
Ключевые поля:
fragmentSource(илиfragmentKeyдля ключа кэша, загруженного черезthis.load.glsl) — ваш GLSL.initialUniforms— значения для однократной инициализации.setupUniforms(setUniform, drawingContext)— выполняется каждый кадр. Здесь передавайте покадровые значения (время, разрешение, мышь, уровень звука).
Phaser поставляет юниформ uProjectionMatrix по умолчанию. Автоматические iResolution/iTime в стиле Shadertoy из v3 исчезли — объявляйте и передавайте их самостоятельно, если они вам нужны.
Загрузка шейдеров из файлов
Для нетривиальных шейдеров поставляйте GLSL отдельным файлом:
preload() {
this.load.glsl('rainbow', 'shaders/rainbow.frag');
}
create() {
this.add.shader({ name: 'rainbow', fragmentKey: 'rainbow' }, 240, 160, 480, 320);
}
В v3 загрузчику требовался вид 'fragment' | 'vertex'. В v4 GLSL загружается как непрозрачный исходный код; вид определяется там, где вы конструируете Shader.
Разобранный пример: пост-эффект CRT
Фильтр CRT на всю камеру, собранный из собственного Shader, наложенного поверх виньетки. Шейдер сэмплирует RenderTexture сцены, а не фреймбуфер камеры напрямую — это сохраняет компонуемость эффекта.
const crtFrag = `
precision mediump float;
uniform sampler2D uMainSampler;
uniform vec2 uResolution;
uniform float uTime;
varying vec2 outTexCoord;
void main() {
vec2 uv = outTexCoord;
// Хроматическая аберрация: сэмплируем R/G/B в слегка смещённых позициях.
float aberr = 0.0015;
vec3 col;
col.r = texture2D(uMainSampler, uv + vec2(aberr, 0.0)).r;
col.g = texture2D(uMainSampler, uv).g;
col.b = texture2D(uMainSampler, uv - vec2(aberr, 0.0)).b;
// Развёртка (scanlines): затемняем каждый второй ряд пикселей.
float scan = 0.92 + 0.08 * sin(uv.y * uResolution.y * 3.14159);
col *= scan;
// Виньетка: затемнение к углам.
vec2 d = uv - 0.5;
col *= 1.0 - dot(d, d) * 0.6;
gl_FragColor = vec4(col, 1.0);
}
`;
create() {
// 1. Отрисовываем сцену в текстуру каждый кадр.
const rt = this.add.renderTexture(0, 0, this.scale.width, this.scale.height).setOrigin(0);
// 2. Рисуем квад Shader, который сэмплирует эту текстуру и применяет CRT.
const crt = this.add.shader(
{
name: 'crt',
fragmentSource: crtFrag,
setupUniforms: (setUniform) => {
setUniform('uResolution', [this.scale.width, this.scale.height]);
setUniform('uTime', this.time.now / 1000);
},
},
this.scale.width / 2, this.scale.height / 2,
this.scale.width, this.scale.height,
[rt.texture],
);
// Поддерживаем rt в актуальном состоянии каждый кадр.
this.events.on('update', () => {
rt.clear();
rt.draw(this.cameras.main);
rt.render();
});
}
В Phaser 4 RenderTexture буферизует команды — нужно вызвать render(), чтобы их сбросить (см. DynamicTexture / RenderTexture в гайде по миграции).
Практические замечания
mediump, как правило, достаточно.highpудваивает нагрузку на регистры на некоторых GPU.- Текстурные координаты используют соглашения GL. Y=0 находится внизу. Сжатые текстуры нужно перекодировать с отражённой осью Y; стандартные изображения обрабатываются автоматически.
- Стоимость масштабируется с размером фреймбуфера. Уменьшите разрешение вдвое и применяйте шейдер развёртки к нему — дешёвая ретро-эстетика.
- Отлаживайте упрощением. Если шейдер выглядит неправильно, сначала замените его тело на
gl_FragColor = vec4(outTexCoord, 0.0, 1.0);, чтобы проверить UV-маппинг. - Шейдеры — это самостоятельные отрисовки. Каждый игровой объект
Shaderзавершает текущий батч и отрисовывается отдельно. Умеренное использование нормально; тысячи таких объектов — нет.
Когда вместо этого стоит взяться за RenderNode
Квад Shader стоит одного вызова отрисовки. Если нужен собственный батчируемый эффект — скажем, тип спрайта со своей вершинной раскладкой — вы пишете собственный RenderNode и регистрируете его через RenderConfig#renderNodes при загрузке игры. Это самый нижний слой рендерера и существенно больший объём кода; см. Phaser.Renderer.WebGL.RenderNodes в справочнике API для оценки объёма работы.
Для 95% эффектов — встроенных фильтров или игрового объекта Shader — это не требуется.
Связанное
- Миграция с Phaser 3 — изменения API рендерера/шейдеров между v3 и v4.
- Игровые объекты —
Shaderявляется одним из них. - Камеры — у каждой камеры есть список фильтров.