О чем этот пример

Эффект огня — один из ключевых элементов атмосферы во многих играх. В этом руководстве мы разберем, как создать реалистичное и производительное пламя, используя встроенные в 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 для получения синего магического или зелёного ядовитого пламени, либо анимируйте масштаб и альфу у частиц, чтобы создать эффект взрыва.