~2 мин чтения

Шейдеры

Собственные 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 является одним из них.
  • Камеры — у каждой камеры есть список фильтров.