О чем этот пример
Эффект огня — один из ключевых элементов атмосферы во многих играх. В этом руководстве мы разберем, как создать реалистичное и производительное пламя, используя встроенные в Phaser объекты шума, текстуры рендера и систему фильтров. Вы научитесь генерировать основу огня из симплекс-шума, накладывать маски и искажения, а затем анимировать результат для создания живого, динамичного эффекта, который можно адаптировать под разные нужды — от факелов до магических заклинаний.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
fireNoise;
fireWarp;
fireRope;
create()
{
const { width, height } = this.scale;
this.add.gradient({
bands: {
colorStart: 0xffffff,
colorEnd: 0x000000,
// interpolation: 4, // Enable this for thicker fire, but with more obvious edges.
},
shapeMode: 2,
start: { x: 0.5, y: 0.5 },
shape: { x: 0.5, y: 0 }
}, 0, 0, 256, 256).setRenderToTexture('fire-mask');
this.fireNoise = this.add.noisesimplex2d({
noiseCells: [ 4, 4 ],
noiseIterations: 3,
noiseValueAdd: 0.2
}, 0, 0, 256, 256).setRenderToTexture('fire-noise');
this.fireWarp = this.add.noisesimplex2d({
noiseCells: [ 2, 2 ],
noiseSeed: [ 11, 12 ],
noiseIterations: 3,
noiseNormalMap: true,
noiseNormalScale: 2
}, 0, 0, 256, 256).setRenderToTexture('fire-warp');
this.add.image(128, 128, 'fire-noise');
this.add.image(128, 128 + 256, 'fire-mask');
this.add.image(128, 128 + 256 * 2, 'fire-warp');
const fire = this.add.image(0, 0, 'fire-noise')
.setOrigin(0)
.setVisible(false)
.enableFilters();
fire.filters.internal.addBlend('fire-mask', Phaser.BlendModes.MULTIPLY);
fire.filters.internal.addDisplacement('fire-warp', 1, 1);
fire.filters.internal.addGradientMap({
ramp: [
{
colorStart: [ 0, 0, 0, 0 ], // Transparent
colorEnd: [ 0.5, 0.1, 0.1 ],
size: 0.1
},
{
colorStart: [ 0.5, 0.1, 0.1 ],
colorEnd: [ 1, 1, 0.3 ],
size: 0.3
},
{
colorStart: [ 1, 1, 0.3 ],
colorEnd: [ 1, 1, 1 ],
size: 0.7
},
]
});
const fireTexture = this.add.renderTexture(128 + 256, 128, 256, 256)
.setRenderMode('all', true)
.clear()
.draw(fire);
fireTexture.saveTexture('fire');
// This is one way you might stylize fire.
const fireSprite = this.add.image(128 + 256, 128 + 256, 'fire')
.enableFilters();
fireSprite.filters.internal.addQuantize({
mode: 1,
steps: [ 16, 2, 2, 1 ],
// dither: true
});
fireSprite.filters.internal.addBlocky();
// This is a way to sculpt fire to your needs.
this.fireRope = this.add.rope(width * 3 / 4 - 256, height * 5 / 7, 'fire', null, 16, false);
this.fireRope.setScale(0.5);
this.add.pointlight(this.fireRope.x, this.fireRope.y, 0xffcc88, 128, 0.2);
// This is a surprisingly effective way to use several fire particles together.
// They don't actually move!
// They seem to accelerate and disperse as they rise by increasing sprite scale with height.
// They seem to swirl by varying sprite rotation.
const compScale = 0.75;
const compCount = 16;
for (let i = 0; i < compCount; i++)
{
const x = Math.random() * 128 * compScale;
const y = (i * i / compCount + Math.random()) * 32 * compScale;
const fire = this.add.image(1000 + x, 500 - y, 'fire')
.setBlendMode(Phaser.BlendModes.ADD)
.setRotation(Math.random() - 0.5)
.setAlpha(1 - i * i / compCount / compCount)
.setScale((Math.random() * 0.5 + 0.25 + i / compCount) * compScale);
}
}
update (time)
{
const t = time / 1000;
this.fireNoise.noiseOffset[1] = -t * 6;
this.fireNoise.noiseFlow = t * 2;
this.fireWarp.noiseOffset[1] = -t * 2;
this.fireWarp.noiseFlow = t * 2;
// Animate fireRope.
const pointCount = this.fireRope.points.length;
const fireRopePoints = [];
for (let i = 0; i < pointCount; i++)
{
const flail = 1 - (i + 1) / pointCount;
fireRopePoints.push({
x: Math.sin(t * 8 + i / 4) * flail * 128 + flail * 256,
y: -768 * flail + Math.sin(t * 7 + i / 5) * 128 * flail
});
}
this.fireRope.setPoints(fireRopePoints);
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1280,
height: 720,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка компонентов: маска, шум и искажение
Первым делом создаются три текстуры, которые станут основой для эффекта огня. Они рендерятся в текстуры (setRenderToTexture) для последующего использования в фильтрах.
Градиент создает маску, которая придаст огню форму, сужающуюся кверху. Симплекс-шум генерирует текстуру с турбулентностью, имитирующей движение пламени. Второй слой шума используется как карта нормалей для искажения (displacement) первого слоя, добавляя эффект "потока".
this.add.gradient({
bands: {
colorStart: 0xffffff,
colorEnd: 0x000000,
},
shapeMode: 2,
start: { x: 0.5, y: 0.5 },
shape: { x: 0.5, y: 0 }
}, 0, 0, 256, 256).setRenderToTexture('fire-mask');
this.fireNoise = this.add.noisesimplex2d({
noiseCells: [ 4, 4 ],
noiseIterations: 3,
noiseValueAdd: 0.2
}, 0, 0, 256, 256).setRenderToTexture('fire-noise');
this.fireWarp = this.add.noisesimplex2d({
noiseCells: [ 2, 2 ],
noiseSeed: [ 11, 12 ],
noiseIterations: 3,
noiseNormalMap: true,
noiseNormalScale: 2
}, 0, 0, 256, 256).setRenderToTexture('fire-warp');
Сборка и окрашивание огня с помощью фильтров
Здесь мы берем текстуру шума (fire-noise) и применяем к ней цепочку фильтров (enableFilters), чтобы превратить черно-белый шум в цветное пламя.
1. **Blend (MULTIPLY):** Накладывает маску (fire-mask), чтобы придать огню форму.
2. **Displacement:** Использует текстуру искажения (fire-warp) для деформации основного шума, создавая иллюзию живого, колышущегося пламени.
3. **GradientMap:** Заменяет градации серого на цветовую карту, задавая переход от прозрачного черного через красный и оранжевый к белому. Это "раскрашивает" шум.
После применения фильтров результат отрисовывается в RenderTexture и сохраняется как новая текстура 'fire'.
const fire = this.add.image(0, 0, 'fire-noise')
.setOrigin(0)
.setVisible(false)
.enableFilters();
fire.filters.internal.addBlend('fire-mask', Phaser.BlendModes.MULTIPLY);
fire.filters.internal.addDisplacement('fire-warp', 1, 1);
fire.filters.internal.addGradientMap({
ramp: [
{ colorStart: [ 0, 0, 0, 0 ], colorEnd: [ 0.5, 0.1, 0.1 ], size: 0.1 },
{ colorStart: [ 0.5, 0.1, 0.1 ], colorEnd: [ 1, 1, 0.3 ], size: 0.3 },
{ colorStart: [ 1, 1, 0.3 ], colorEnd: [ 1, 1, 1 ], size: 0.7 },
]
});
const fireTexture = this.add.renderTexture(128 + 256, 128, 256, 256)
.setRenderMode('all', true)
.clear()
.draw(fire);
fireTexture.saveTexture('fire');
Стилизация и использование готового огня
Готовую текстуру 'fire' можно использовать по-разному. В примере показаны три подхода.
**Квантование и пикселизация:** К спрайту применяются фильтры addQuantize и addBlocky, чтобы придать огню стилизованный, «игровой» вид.
**Огонь как верёвка (Rope):** Объект Rope использует текстуру огня, деформируя её вдоль набора точек. Это позволяет создавать длинные, гибкие языки пламени.
**Партиклы-иллюзия:** Группа статичных спрайтов, разбросанных по высоте с разным масштабом, вращением и прозрачностью, создаёт иллюзию поднимающихся и рассеивающихся искр. Ключевую роль играет бленд-режим ADD для яркого свечения.
// Стилизация
const fireSprite = this.add.image(128 + 256, 128 + 256, 'fire').enableFilters();
fireSprite.filters.internal.addQuantize({ mode: 1, steps: [ 16, 2, 2, 1 ] });
fireSprite.filters.internal.addBlocky();
// Верёвка
this.fireRope = this.add.rope(width * 3 / 4 - 256, height * 5 / 7, 'fire', null, 16, false);
this.fireRope.setScale(0.5);
// Псевдо-частицы
for (let i = 0; i < compCount; i++) {
const fire = this.add.image(1000 + x, 500 - y, 'fire')
.setBlendMode(Phaser.BlendModes.ADD)
.setRotation(Math.random() - 0.5)
.setAlpha(1 - i * i / compCount / compCount)
.setScale((Math.random() * 0.5 + 0.25 + i / compCount) * compScale);
}
Анимация: вдыхаем жизнь в пламя
В методе update анимируются исходные компоненты шума и объект Rope. Изменяя свойства noiseOffset и noiseFlow у объектов noisesimplex2d, мы заставляем текстуры шума плавно "течь" вверх, создавая основное движение огня.
Для fireRope в каждом кадре пересчитываются позиции точек (setPoints). Используя синусоидальные функции от времени и индекса точки, мы добиваемся волнообразного, живого движения всего шлейфа пламени. Параметр flail уменьшает амплитуду к верхнему концу, делая движение более затухающим.
update (time) {
const t = time / 1000;
this.fireNoise.noiseOffset[1] = -t * 6;
this.fireNoise.noiseFlow = t * 2;
this.fireWarp.noiseOffset[1] = -t * 2;
this.fireWarp.noiseFlow = t * 2;
// Анимация верёвки
const pointCount = this.fireRope.points.length;
const fireRopePoints = [];
for (let i = 0; i < pointCount; i++) {
const flail = 1 - (i + 1) / pointCount;
fireRopePoints.push({
x: Math.sin(t * 8 + i / 4) * flail * 128 + flail * 256,
y: -768 * flail + Math.sin(t * 7 + i / 5) * 128 * flail
});
}
this.fireRope.setPoints(fireRopePoints);
}
Что попробовать дальше
Вы создали гибкую систему для генерации огня на основе процедурного шума. Главное преимущество этого подхода — его производительность и контроль: вся динамика рассчитывается в шейдерах, а результат можно стилизовать под любой арт-стиль игры. Для экспериментов попробуйте изменить параметры noiseCells и noiseIterations для другой детализации шума, поиграйте с цветами в GradientMap для получения синего магического или зелёного ядовитого пламени, либо анимируйте масштаб и альфу у частиц, чтобы создать эффект взрыва.
