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

Создание статичного фона — это просто. Но как оживить сцену, добавив ей динамики и глубины без анимации спрайтов? В этом примере мы разберем, как использовать встроенный в Phaser генератор симплекс-шума для создания потрясающего эффекта "живой" поверхности с отражениями. Этот прием позволяет превратить обычную текстуру в нечто волнующееся и переливающееся, идеально подходящее для водных поверхностей, магических барьеров или атмосферных искажений, затрачивая минимум ресурсов на отрисовку.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    noise;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/spookysky.jpg');
    }

    create()
    {
        const { width, height } = this.scale;

        this.noise = this.add.noisesimplex2d({
            noiseCells: [ 4, 32 ],
            noiseNormalMap: true,
            noiseNormalScale: 0.3,
            noiseWarpAmount: 1
        }, 0, 0, width, height).setRenderToTexture('noise-normal');

        // Create a blank texture.
        this.textures.addFlatColor('blank', width, height, 0xffffff, 1);

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

        bg.enableFilters().filters.internal.addImageLight({
            environmentMap: 'bg',
            normalMap: 'noise-normal',
            bulge: 1
        });
    }

    update (time)
    {
        const t = time / 800;

        this.noise.noiseFlow = t % (Math.PI * 2);
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 1280,
    height: 720,
    scene: Example
};

const game = new Phaser.Game(config);

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

В методе preload мы загружаем единственную текстуру — фоновое изображение неба. Обратите внимание на использование setBaseURL. Это позволяет задать базовый путь для всех последующих загрузок, что упрощает указание относительных путей к ресурсам.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg', 'assets/skies/spookysky.jpg');
}

Создание текстуры шума

Ключевой объект примера — this.noise. Мы создаем его с помощью this.add.noisesimplex2d. Этот метод генерирует двухмерную текстуру симплекс-шума.

Рассмотрим параметры конструктора: * Первый аргумент — объект конфигурации. * noiseCells: [4, 32] определяет размер ячеек шума по осям X и Y, влияя на крупность паттерна. * noiseNormalMap: true — флаг, указывающий, что нужно создать не просто карту шума, а нормальную карту. Нормальная карта кодирует информацию о направлении поверхности для расчета освещения и отражений. * noiseNormalScale: 0.3 — масштаб эффекта на нормальной карте. Чем больше значение, тем "выше" будут "волны". * noiseWarpAmount: 1 — величина искажения шума. * Следующие четыре аргумента (0, 0, width, height) задают позицию и размеры генерируемой области. * Метод .setRenderToTexture('noise-normal') рендерит сгенерированную нормальную карту в текстуру с ключом 'noise-normal'. Эту текстуру мы будем использовать позже.

Также создается простая белая текстура 'blank' с помощью this.textures.addFlatColor, которая будет служить нашим холстом для эффекта.

create()
{
    const { width, height } = this.scale;

    this.noise = this.add.noisesimplex2d({
        noiseCells: [ 4, 32 ],
        noiseNormalMap: true,
        noiseNormalScale: 0.3,
        noiseWarpAmount: 1
    }, 0, 0, width, height).setRenderToTexture('noise-normal');

    // Create a blank texture.
    this.textures.addFlatColor('blank', width, height, 0xffffff, 1);
}

Наложение фильтра отражения

Создаем изображение bg, используя нашу белую текстуру 'blank', и размещаем его по центру. Метод .setFlipY(true) переворачивает изображение по вертикали — это важно для корректного отображения отражения.

Затем мы включаем фильтры для этого изображения (enableFilters()) и добавляем конкретный фильтр — internal.addImageLight. Этот фильтр создает эффект отражения (рельефного освещения) на основе двух карт: * environmentMap: 'bg' — текстура, которая будет "отражаться". Мы используем загруженное изображение неба. * normalMap: 'noise-normal' — нормальная карта, которая задает форму поверхности. Именно наша сгенерированная карта шума создает иллюзию волн и неровностей. * bulge: 1 — параметр, усиливающий эффект выпуклости/вогнутости.

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

    bg.enableFilters().filters.internal.addImageLight({
        environmentMap: 'bg',
        normalMap: 'noise-normal',
        bulge: 1
    });

Анимация шума

Чтобы поверхность не была статичной, мы анимируем исходный шум в методе update. Мы изменяем свойство noiseFlow объекта this.noise, передавая в него значение, зависящее от времени.

time / 800 — это скорость течения. Деление на 800 замедляет анимацию, делая ее плавной. t % (Math.PI * 2) обеспечивает циклическое изменение значения в диапазоне от 0 до 2π, создавая бесшовную, зацикленную анимацию потока шума. Изменение noiseFlow приводит к смещению паттерна шума, что, в свою очередь, динамически меняет нормальную карту и, как следствие, отражение на основном изображении.

update (time)
{
    const t = time / 800;

    this.noise.noiseFlow = t % (Math.PI * 2);
}

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

Мы создали динамичный, "живой" фон, используя всего одну фоновую текстуру, генератор шума и фильтр отражения. Сила этого подхода — в его производительности и гибкости. Для экспериментов попробуйте изменить параметры noiseCells для получения более крупной или мелкой ряби, увеличьте noiseNormalScale для более агрессивных волн или подставьте другую текстуру в environmentMap, например, текстуру лавы или звездного неба, чтобы получить совершенно иной визуальный эффект.