О чем этот пример
Визуальные эффекты на основе шума — основа для создания дыма, огня, статики, повреждений и многих других динамичных элементов в играх. Phaser предлагает мощный, но гибкий API для работы с шейдерами, позволяющий генерировать такие текстуры прямо во время выполнения игры. В этой статье мы разберём пример создания трёх вариантов шумовых текстур с помощью кастомного шейдера: монохромного, градиентного серого и цветного. Вы научитесь декомпозировать шейдерный код на переиспользуемые модули и динамически управлять их параметрами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
const randomNoiseFunction = `
uniform vec2 uSeed;
float randomNoise (vec2 uv, vec2 extraSeed) {
return fract(sin(dot(uv, uSeed + extraSeed)) * 43758.5453123);
}
`;
const randomBWProcess = `fragColor = vec4(vec3(step(0.5, randomNoise(outTexCoord, vec2(0.0)))), 1.0);`;
const randomGrayProcess = `fragColor = vec4(vec3(mix(0.25, 1.0, randomNoise(outTexCoord, vec2(0.0)))), 1.0);`;
const randomColorProcess = `fragColor = vec4(randomNoise(outTexCoord, vec2(0.0)), randomNoise(outTexCoord, vec2(1.0)), randomNoise(outTexCoord, vec2(2.0)), 1.0);`;
class Example extends Phaser.Scene
{
constructor()
{
super();
}
preload()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('megaset', 'assets/atlas/megaset-1.png', 'assets/atlas/megaset-1.json');
}
create()
{
this.cameras.main.setBackgroundColor(0x222233);
this.add.image(240, 440, 'megaset', 'atari800').setTint(0x808080);
this.add.image(640, 440, 'megaset', 'atari1200xl').setTint(0x808080);
this.add.image(1040, 440, 'megaset', 'atari130xe').setTint(0x808080);
// Define a list of shader additions to insert.
// We could combine additions in a single entry in the list,
// but this way we can easily swap out individual parts.
const shaderAdditions = [
{
name: 'RandomNoise',
additions: { fragmentHeader: randomNoiseFunction }
},
{
name: 'BWProcess',
additions: { fragmentProcess: randomBWProcess }
}
];
const shaderConfig = {
name: 'Noise',
// By omitting `fragmentSource`, the default fragment shader is used.
// It has template points for accepting additions.
shaderAdditions,
setupUniforms: (setUniform, drawingContext) => {
// Prevent the seed from becoming too large and losing precision.
const time = this.game.loop.time % 100000;
setUniform('uSeed', [time * 12.3456789 + 98.7654321, time * 98.7654321 + 12.3456789]);
}
};
const shader1 = this.add.shader(shaderConfig, 240, 300, 200, 100);
shaderAdditions.pop();
shaderAdditions.push({
name: 'GrayProcess',
additions: { fragmentProcess: randomGrayProcess }
});
const shader2 = this.add.shader(shaderConfig, 640, 300, 200, 100);
shaderAdditions.pop();
shaderAdditions.push({
name: 'ColorProcess',
additions: { fragmentProcess: randomColorProcess }
});
const shader3 = this.add.shader(shaderConfig, 1040, 300, 200, 100);
// Add a bloom filter to the screen.
const parallel = this.cameras.main.filters.internal.addParallelFilters();
parallel.top.addThreshold(0.5, 1.0);
parallel.top.addBlur(1, 4, 4, 1, 0xddddff);
parallel.blend.blendMode = Phaser.BlendModes.ADD;
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1280,
height: 720,
scene: Example
};
const game = new Phaser.Game(config);
Основа шума: функция randomNoise
Вся магия начинается с функции, генерирующей псевдослучайное значение для каждой точки (пикселя) текстуры. В GLSL (язык шейдеров) мы создаём функцию randomNoise. Она использует математическую операцию dot (скалярное произведение) координат текстуры и заранее заданного "зерна" (seed), чтобы получить предсказуемый, но визуально случайный результат.
Функция возвращает значение в диапазоне от 0.0 до 1.0, используя fract для взятия дробной части. Это значение и будет интенсивностью шума в конкретной точке.
const randomNoiseFunction = `
uniform vec2 uSeed;
float randomNoise (vec2 uv, vec2 extraSeed) {
return fract(sin(dot(uv, uSeed + extraSeed)) * 43758.5453123);
}
`;
Ключевые моменты:
- uniform vec2 uSeed; — это uniform-переменная. Её значение едино для всех пикселей и задаётся из JavaScript-кода. Мы будем менять его каждый кадр для анимации шума.
- extraSeed — дополнительный параметр, позволяющий получать разные последовательности шума, используя одну базовую функцию. Это пригодится для цветного шума.
Сборка шейдера из модулей
Phaser позволяет не писать шейдер с нуля, а модифицировать стандартный фрагментный шейдер, подставляя в него собственные куски кода. Это делается через конфигурационный объект с полем shaderAdditions.
Каждое "дополнение" (addition) — это объект с именем и указанием, в какую часть шаблонного шейдера его вставить. В нашем примере мы используем две точки вставки:
- fragmentHeader: для объявления функций (нашу randomNoise).
- fragmentProcess: для кода, который непосредственно вычисляет итоговый цвет пикселя (fragColor).
const shaderAdditions = [
{
name: 'RandomNoise',
additions: { fragmentHeader: randomNoiseFunction }
},
{
name: 'BWProcess',
additions: { fragmentProcess: randomBWProcess }
}
];
const shaderConfig = {
name: 'Noise',
shaderAdditions, // Передаём массив дополнений
setupUniforms: (setUniform, drawingContext) => {
// Код для обновления uniform-переменных каждый кадр
}
};
Объект shaderConfig затем передаётся в this.add.shader() для создания игрового объекта.
Три типа шума: от чёрно-белого к цветному
Меняя всего одну строчку в fragmentProcess, мы кардинально меняем визуальный результат. Всё благодаря тому, что функция randomNoise возвращает гибкое числовое значение.
**1. Пороговый (чёрно-белый) шум:**
Функция step(0.5, value) возвращает 0.0, если значение меньше 0.5, и 1.0 — если больше или равно. Получаются резкие чёрные и белые точки.
const randomBWProcess = `fragColor = vec4(vec3(step(0.5, randomNoise(outTexCoord, vec2(0.0)))), 1.0);`;
**2. Градиентный серый шум:**
Функция mix(0.25, 1.0, value) интерполирует (смешивает) между тёмно-серым (0.25) и белым (1.0) в зависимости от значения шума. Получается плавный серый градации.
const randomGrayProcess = `fragColor = vec4(vec3(mix(0.25, 1.0, randomNoise(outTexCoord, vec2(0.0)))), 1.0);`;
**3. Цветной шум:**
Здесь используется ключевая фишка — параметр extraSeed. Мы трижды вызываем randomNoise с разным extraSeed (vec2(0.0), vec2(1.0), vec2(2.0)), получая три независимых случайных значения для красного, зелёного и синего каналов соответственно.
const randomColorProcess = `fragColor = vec4(randomNoise(outTexCoord, vec2(0.0)), randomNoise(outTexCoord, vec2(1.0)), randomNoise(outTexCoord, vec2(2.0)), 1.0);`;
В коде примера мы последовательно заменяем последний элемент в массиве shaderAdditions и создаём три отдельных объекта-шейдера с разными процессами.
Анимация и пост-обработка
Чтобы шум не был статичной картинкой, его нужно анимировать. Для этого в конфиге шейдера используется функция setupUniforms. Она вызывается каждый кадр и позволяет обновлять uniform-переменные.
В примере мы берём время из игрового цикла (this.game.loop.time) и на его основе вычисляем новое значение для uSeed. Меняя seed, мы получаем совершенно новую последовательность шума каждый кадр, создавая иллюзию движения.
setupUniforms: (setUniform, drawingContext) => {
// Ограничиваем время, чтобы seed не стал слишком большим
const time = this.game.loop.time % 100000;
setUniform('uSeed', [time * 12.3456789 + 98.7654321, time * 98.7654321 + 12.3456789]);
}
**Добавляем свечение (Bloom):**
Пример также показывает, как применить пост-обработку ко всей сцене. Создаётся параллельная цепочка фильтров (addParallelFilters()), где сначала применяется пороговая фильтрация (addThreshold), затем размытие (addBlur), а результат смешивается с оригиналом в режиме ADD (сложение). Это добавляет шумовым текстурам лёгкое свечение.
const parallel = this.cameras.main.filters.internal.addParallelFilters();
parallel.top.addThreshold(0.5, 1.0);
parallel.top.addBlur(1, 4, 4, 1, 0xddddff);
parallel.blend.blendMode = Phaser.BlendModes.ADD;
Что попробовать дальше
Используя модульный подход к шейдерам в Phaser, вы можете создавать богатую библиотеку визуальных эффектов, комбинируя их как конструктор. Шум — лишь один из кирпичиков.
**Идеи для экспериментов:**
1. Замените step и mix на другие функции GLSL (например, smoothstep для более мягких переходов).
2. Используйте несколько слоёв шума с разной частотой (умножая outTexCoord на коэффициент) и смешайте их для получения более сложных текстур (например, для облаков).
3. Привяжите значение seed не ко времени, а к положению игрового объекта, чтобы шум "следовал" за ним.
4. Используйте шумовую текстуру в качестве маски для других эффектов, например, для постепенного "исчезновения" спрайта.
