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

Создание сложных визуальных эффектов, таких как подводная дисторсия, часто кажется нетривиальной задачей. Однако Phaser 3 предоставляет мощный набор инструментов для рендеринга в текстуры и постобработки, которые позволяют реализовать такие эффекты с минимальными усилиями. Эта статья разберет пример создания "подводной" сцены, где мы научимся захватывать кадр в текстуру, динамически генерировать текстуру искажения и применять к основному изображению фильтр смещения (displacement filter). Эти техники открывают двери к созданию эффектов воды, теплового марева, магических барьеров и многого другого.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    bubbles = [];
    distortionConfig1 = {
        tileRotation: 1
    };
    distortionConfig2 = {
        tileRotation: -1,
        alpha: 0.5
    };
    redrawDistortion;

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bubble', 'assets/particles/bubble.png');
        this.load.image('background', 'assets/sprites/blur-bg.png');
        this.load.image('phaserlogo', 'assets/sprites/phaser2.png');
        this.load.image('alien-metal-n', 'assets/textures/alien-metal-n.jpg');
    }

    create ()
    {
        this.cameras.main.setForceComposite(true);

        const width = this.renderer.width;
        const height = this.renderer.height;

        const bg = this.add.image(width / 2, height / 2, 'background');

        for (let i = 0; i < 64; i++)
        {
            const x = Phaser.Math.Between(0, width);
            const y = Phaser.Math.Between(0, height);

            const bubble = this.add.image(x, y, 'bubble')
                .setScale(0.5 + Math.random())
                .setBlendMode(Phaser.BlendModes.ADD);

            this.bubbles.push(bubble);
        }

        // Capture previous elements to texture.
        const captureFrame = this.add.captureFrame('capture');

        // Render a distortion texture.
        const distortion = this.add.renderTexture(width / 2, height / 2, width, height)
            .setVisible(false)
            .saveTexture('distortion')
        this.redrawDistortion = (time, delta) =>
        {
            this.distortionConfig1.tilePositionX = time * 0.1;
            this.distortionConfig2.tilePositionX = time * 0.1;
            distortion
                .clear()
                .repeat('alien-metal-n', undefined, undefined, undefined, undefined, undefined, this.distortionConfig1)
                .repeat('alien-metal-n', undefined, undefined, undefined, undefined, undefined, this.distortionConfig2)
                .render();
        };

        // Render the capture frame and distortion texture.
        const waterDistortion = this.add.image(width / 2, height / 2, 'capture');
        waterDistortion.enableFilters();
        waterDistortion.filters.internal.addDisplacement('distortion', 0.03, 0.03);

        // Add a logo to the scene.
        const logo = this.add.image(width / 2, height / 2, 'phaserlogo');
        logo.enableFilters();
        logo.filters.internal.addGlow();
    }

    update (time, delta)
    {
        this.redrawDistortion(time, delta);

        this.bubbles.forEach(bubble =>
        {
            bubble.scaleY = (Math.sin(bubble.scaleX * (time + bubble.x * 1234) / 256) * 0.1 + 0.9) * bubble.scaleX;
            bubble.y -= 0.1 * delta * bubble.scaleY;

            if (bubble.y < -bubble.height)
            {
                bubble.y = this.renderer.height + bubble.height;
            }
        });
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    backgroundColor: '#2d2d2d',
    width: 1280,
    height: 720,
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание атмосферы

Перед тем как приступить к сложным эффектам, нужно создать базовую сцену. В методе preload загружаются необходимые ресурсы: фон, пузырьки, логотип Phaser и текстура металла, которая впоследствии станет основой для эффекта искажения.

В методе create первым делом мы настраиваем камеру с помощью this.cameras.main.setForceComposite(true). Этот вызов важен для корректной работы фильтров и композитинга на некоторых платформах, особенно когда применяются прозрачность или смешивание.

Затем создается фон и 64 пузырька. Каждый пузырек — это обычный Image, которому задается случайный размер и режим наложения Phaser.BlendModes.ADD. Этот режим заставляет яркие области пузырьков складываться с фоном, создавая эффект свечения. Пузырьки хранятся в массиве this.bubbles для последующей анимации.

this.cameras.main.setForceComposite(true);
const bg = this.add.image(width / 2, height / 2, 'background');
const bubble = this.add.image(x, y, 'bubble')
    .setScale(0.5 + Math.random())
    .setBlendMode(Phaser.BlendModes.ADD);
this.bubbles.push(bubble);

Магия захвата кадра: `add.captureFrame`

Ключевой момент для создания эффекта — это возможность Phaser захватить всё, что отрендерилось на сцене к определенному моменту, в текстуру. Для этого используется метод this.add.captureFrame().

Вызов const captureFrame = this.add.captureFrame('capture') создает специальный объект, который при рендеринге сцены сохраняет текущий буфер кадра (всё, что было нарисовано до этого вызова) во внутреннюю текстуру с именем 'capture'. В нашем случае это фон и все пузырьки.

Эта текстура затем может быть использована как обычное изображение. Мы создаем объект waterDistortion, который является спрайтом, использующим эту текстуру. Теперь у нас есть "снимок" сцены, который мы можем искажать, не затрагивая исходные объекты.

const captureFrame = this.add.captureFrame('capture');
const waterDistortion = this.add.image(width / 2, height / 2, 'capture');

Динамическая текстура искажения: `RenderTexture`

Для эффекта воды нужна текстура, которая будет управлять смещением пикселей основного изображения. Мы создаем ее динамически с помощью RenderTexture.

RenderTexture — это холст в памяти, на котором можно рисовать другие текстуры или игровые объекты. Мы создаем его невидимым (setVisible(false)) и сохраняем его содержимое как текстуру с именем 'distortion' методом .saveTexture('distortion').

Функция this.redrawDistortion перерисовывает эту текстуру каждый кадр. Она использует метод .repeat(), который заполняет RenderTexture повторяющейся текстурой 'alien-metal-n'. Конфигурационные объекты distortionConfig1 и distortionConfig2 управляют параметрами: tilePositionX смещает текстуру по горизонтали со временем, создавая анимацию, tileRotation задает небольшой поворот, а alpha — прозрачность второго слоя. Два слоя с разными параметрами накладываются друг на друга, создавая более сложную и "живую" карту смещения.

const distortion = this.add.renderTexture(width / 2, height / 2, width, height)
    .setVisible(false)
    .saveTexture('distortion');

this.redrawDistortion = (time, delta) =>
{
    this.distortionConfig1.tilePositionX = time * 0.1;
    this.distortionConfig2.tilePositionX = time * 0.1;
    distortion
        .clear()
        .repeat('alien-metal-n', undefined, undefined, undefined, undefined, undefined, this.distortionConfig1)
        .repeat('alien-metal-n', undefined, undefined, undefined, undefined, undefined, this.distortionConfig2)
        .render();
};

Применение фильтров: Дисторсия и свечение

Теперь у нас есть два ключевых компонента: основное изображение (waterDistortion с текстурой 'capture') и карта смещения (текстура 'distortion'). Фильтры Phaser позволяют их совместить.

Сначала для изображения waterDistortion включается поддержка фильтров вызовом .enableFilters(). Затем добавляется фильтр смещения: .filters.internal.addDisplacement('distortion', 0.03, 0.03). Этот фильтр использует текстуру 'distortion' (где яркость пикселей интерпретируется как величина смещения) для искажения waterDistortion. Второй и третий аргументы (0.03) — это множители смещения по осям X и Y, они контролируют силу эффекта.

Отдельно для логотипа Phaser включается простой фильтр свечения (addGlow()), чтобы показать, что фильтры можно применять и к обычным объектам независимо.

waterDistortion.enableFilters();
waterDistortion.filters.internal.addDisplacement('distortion', 0.03, 0.03);

logo.enableFilters();
logo.filters.internal.addGlow();

Анимация: Оживляем пузырьки и дисторсию

Вся магия происходит в методе update. Здесь вызывается функция this.redrawDistortion(time, delta), которая, как мы уже знаем, каждый кадр анимирует текстуру искажения, смещая ее.

Параллельно анимируются пузырьки. Для каждого пузырька вычисляется вертикальный масштаб (scaleY) на основе синусоиды, которая зависит от времени, горизонтальной позиции пузырька и его базового масштаба. Это создает эффект пульсации, уникальный для каждого пузырька. Затем пузырек медленно всплывает вверх (bubble.y -= 0.1 * delta * bubble.scaleY). Когда пузырек уходит за верхнюю границу, он возвращается вниз, создавая бесконечный цикл.

Важно отметить, что пузырьки анимируются независимо от эффекта дисторсии, так как последний применяется к "снимку" сцены, сделанному до начала анимации. Это демонстрирует разделение логики рендеринга и постобработки.

this.redrawDistortion(time, delta);

bubble.scaleY = (Math.sin(bubble.scaleX * (time + bubble.x * 1234) / 256) * 0.1 + 0.9) * bubble.scaleX;
bubble.y -= 0.1 * delta * bubble.scaleY;
if (bubble.y < -bubble.height)
{
    bubble.y = this.renderer.height + bubble.height;
}

Что попробовать дальше

Комбинируя захват кадра, динамический рендеринг в текстуру и фильтры постобработки, Phaser позволяет создавать потрясающие визуальные эффекты с относительно небольшим объемом кода. Этот пример — отправная точка. Экспериментируйте: попробуйте использовать другие текстуры для дисторсии (например, шум Перлина), изменяйте параметры фильтра addDisplacement для создания эффектов ветра или землетрясения, примените фильтр смещения к анимированным спрайтам или попробуйте комбинировать несколько фильтров на одном объекте для создания уникальных стилей.