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

Эффективное освещение — ключ к созданию атмосферных и профессионально выглядящих игр. В этом руководстве мы разберем, как использовать фильтр Image Light в Phaser для симуляции реалистичного отражения света на 3D-объектах, используя 2D-спрайты и панорамные карты окружения. Вы научитесь подготавливать окружение, создавать несколько уровней размытия для имитации шероховатости поверхности и анимировать отражения, что позволит добавить в вашу игру эффекты металлических или стеклянных поверхностей без использования настоящего 3D.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('orb-n', 'assets/normal-maps/orb-256.png');

        // For viewing at any angle, a 360 degree panorama with 180 degree height is ideal.
        // You can acquire many such assets from Adobe Stock or other sources.
        this.load.image('environment', 'assets/panorama-360/KPNO-Drone-360-2-CC2-by-NOIRLab.jpg');
    }

    create ()
    {
        const background = this.add.gradient({
            start: { x: 0, y: 1 },
            shape: { x: 0, y: -1 },
            bands: {
                colorStart: 0x2d3440,
                colorEnd: 0x204070
            },
        }, 640, 360, 1280, 720);

        // Create an object as the base for a PanoramaBlur.
        const envBlurSource = this.add.image(0, 0, 'environment').setOrigin(0);
        const envW = envBlurSource.width;
        const envH = envBlurSource.height;
        const panoramaBlur = envBlurSource.enableFilters().filters.internal.addPanoramaBlur({
            samplesX: envW / 16, // Be careful: more samples are much more costly!
            samplexY: envH / 16,
            power: 2 // Emphasise sun over darker areas.
        });

        // Render panorama at blur 1...
        const envTexture1 = this.textures.addDynamicTexture('environment-blur-1', envW, envH);
        envTexture1.draw(envBlurSource).render();

        // ... and at blur 0.5...
        const envTexture2 = this.textures.addDynamicTexture('environment-blur-0.5', envW, envH);
        panoramaBlur.power = 1.5;
        panoramaBlur.radius = 0.5;
        envTexture2.draw(envBlurSource).render();

        // ... and at blur 0.25...
        const envTexture3 = this.textures.addDynamicTexture('environment-blur-0.25', envW, envH);
        panoramaBlur.power = 1.25;
        panoramaBlur.radius = 0.25;
        envTexture3.draw(envBlurSource).render();

        // ... and at blur 0.125...
        const envTexture4 = this.textures.addDynamicTexture('environment-blur-0.125', envW, envH);
        panoramaBlur.power = 1.125;
        panoramaBlur.radius = 0.125;
        envTexture4.draw(envBlurSource).render();

        // ... and at blur 0.0625...
        const envTexture5 = this.textures.addDynamicTexture('environment-blur-0.0625', envW, envH);
        panoramaBlur.power = 1.0625;
        panoramaBlur.radius = 0.0625;
        envTexture5.draw(envBlurSource).render();

        // Remove blur source because blurs are costly per-frame.
        envBlurSource.destroy();

        // Draw an orb for reuse.
        const orb = this.add.rectangle(0, 0, 256, 256, 0xffffff);
        orb.enableFilters().filters.internal.addMask('orb-n');
        const orbTexture = this.textures.addDynamicTexture('orb', 256, 256);
        orbTexture.draw(orb, 128, 128).render();
        orb.destroy();

        // Light a series of orbs with the panorama at different roughness levels.
        const orb1 = this.add.image(200, 200, 'orb');
        this.orb1LightImage = orb1.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment-blur-1',
            normalMap: 'orb-n'
        });

        const orb2 = this.add.image(640, 200, 'orb');
        this.orb2LightImage = orb2.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment-blur-0.5',
            normalMap: 'orb-n'
        });

        const orb3 = this.add.image(1080, 200, 'orb');
        this.orb3LightImage = orb3.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment-blur-0.25',
            normalMap: 'orb-n'
        });

        const orb4 = this.add.image(200, 520, 'orb');
        this.orb4LightImage = orb4.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment-blur-0.125',
            normalMap: 'orb-n'
        });

        const orb5 = this.add.image(640, 520, 'orb');
        this.orb5LightImage = orb5.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment-blur-0.0625',
            normalMap: 'orb-n'
        });

        const orb6 = this.add.image(1080, 520, 'orb');
        this.orb6LightImage = orb6.enableFilters().filters.internal.addImageLight({
            environmentMap: 'environment', // Unfiltered environment.
            normalMap: 'orb-n'
        });
    }

    update (time, delta)
    {
        const filters = [
            this.orb1LightImage,
            this.orb2LightImage,
            this.orb3LightImage,
            this.orb4LightImage,
            this.orb5LightImage,
            this.orb6LightImage
        ];
        for (const filter of filters)
        {
            filter.viewMatrix
            .identity() // Reset matrix
            .rotateY(time / 5000) // Look left/right
            .rotateX(0.5 * Math.sin(time / 1765)); // Look up/down
        }
    }
}

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

const game = new Phaser.Game(config);

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

Основа эффекта — панорамная текстура окружения (environment) и карта нормалей (orb-n). Карта нормалей кодирует информацию о поверхности объекта, что позволяет фильтру рассчитать отражение света.

Первым делом загружаем изображения в методе preload. Также создаем градиентный фон для красивого контраста.

this.load.image('orb-n', 'assets/normal-maps/orb-256.png');
this.load.image('environment', 'assets/panorama-360/KPNO-Drone-360-2-CC2-by-NOIRLab.jpg');
const background = this.add.gradient({
    start: { x: 0, y: 1 },
    shape: { x: 0, y: -1 },
    bands: {
        colorStart: 0x2d3440,
        colorEnd: 0x204070
    },
}, 640, 360, 1280, 720);

Создание карт окружения с разной шероховатостью

Чем более шероховата поверхность, тем более размытым выглядит отражение на ней. Мы создадим несколько версий панорамы с разной степенью размытия, которые будут имитировать разную шероховатость материала.

Для этого используется фильтр PanoramaBlur. Важно: параметр samplesX напрямую влияет на производительность. Мы применяем фильтр к исходному изображению, а затем рендерим результат в динамические текстуры.

const envBlurSource = this.add.image(0, 0, 'environment').setOrigin(0);
const panoramaBlur = envBlurSource.enableFilters().filters.internal.addPanoramaBlur({
    samplesX: envW / 16,
    samplexY: envH / 16,
    power: 2
});

После настройки фильтра мы последовательно меняем его свойства power и radius и рендерим изображение в новую текстуру. Исходный спрайт envBlurSource удаляется после создания всех текстур для экономии ресурсов.

const envTexture1 = this.textures.addDynamicTexture('environment-blur-1', envW, envH);
envTexture1.draw(envBlurSource).render();
panoramaBlur.power = 1.5;
panoramaBlur.radius = 0.5;

Подготовка переиспользуемого спрайта сферы

Фильтр ImageLight применяется к объекту на сцене. Чтобы создать несколько одинаковых сфер, мы сначала рисуем одну белую сферу как Phaser.GameObjects.Rectangle, применяем к ней маску в виде карты нормалей и рендерим результат в отдельную текстуру. Эта текстура 'orb' будет использоваться для всех шести сфер на сцене, что эффективно с точки зрения памяти.

Ключевой момент: фильтр Mask из filters.internal использует карту нормалей для формирования базовой формы объекта, к которой затем будет применено освещение.

const orb = this.add.rectangle(0, 0, 256, 256, 0xffffff);
orb.enableFilters().filters.internal.addMask('orb-n');
const orbTexture = this.textures.addDynamicTexture('orb', 256, 256);
orbTexture.draw(orb, 128, 128).render();
orb.destroy();

Добавление освещения к сферам

Теперь создаем шесть спрайтов сфер, используя подготовленную текстуру 'orb'. К каждому из них применяем фильтр addImageLight. Этот фильтр связывает спрайт с картой нормалей (normalMap) и одной из текстур окружения (environmentMap), созданных на предыдущем шаге.

Разные текстуры окружения (от самой размытой 'environment-blur-1' до оригинальной 'environment') создают иллюзию разной шероховатости материала сферы. Ссылки на фильтры сохраняются в свойствах сцены (например, this.orb1LightImage), чтобы анимировать их в методе update.

const orb1 = this.add.image(200, 200, 'orb');
this.orb1LightImage = orb1.enableFilters().filters.internal.addImageLight({
    environmentMap: 'environment-blur-1',
    normalMap: 'orb-n'
});

Анимация отражения (вращение окружения)

Чтобы отражение на сферах динамически менялось, как если бы наблюдатель или источник света двигались, мы анимируем свойство viewMatrix каждого фильтра ImageLight в методе update.

Матрица сбрасывается методом .identity(), а затем к ней применяются повороты по осям Y и X. Значение time (время с начала работы сцены) обеспечивает плавную и непрерывную анимацию.

for (const filter of filters) {
    filter.viewMatrix
    .identity()
    .rotateY(time / 5000)
    .rotateX(0.5 * Math.sin(time / 1765));
}

Этот подход имитирует изменение угла обзора на панорамное окружение, заставляя блики и отражения "плыть" по поверхности сфер.

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

Фильтр ImageLight в Phaser открывает мощные возможности для создания псевдо-3D освещения, используя 2D-спрайты. Основной принцип — связка карты нормалей объекта и текстуры панорамного окружения. Для экспериментов попробуйте: использовать другие панорамные изображения (например, интерьеры комнат), анимировать параметры фильтра PanoramaBlur для динамического изменения шероховатости объекта или применить эффект к спрайтам сложной формы, создав для них соответствующие карты нормалей.