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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    background;
    backgroundLight;
    spider;
    spiderLight;
    environmentIndex;
    environmentPreview;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('stones', 'assets/normal-maps/stones.png');
        this.load.image('stones_n', 'assets/normal-maps/stones_n.png');
        this.load.image('spider', 'assets/normal-maps/spider.png');
        this.load.image('spider_n', 'assets/normal-maps/spider_n.png');

        // A series of images to provide light to the scene.
        // Only the NOIRLab image is a true panorama,
        // but the others all work great with a default view.
        this.load.image('environment1', 'assets/pics/town-wreck.jpg');
        this.load.image('environment2', 'assets/pics/turkey-1985086.jpg');
        this.load.image('environment3', 'assets/pics/undersea.jpg');
        this.load.image('environment4', 'assets/skies/chrome.png');
        this.load.image('environment5', 'assets/skies/fire.png');
        this.load.image('environment6', 'assets/skies/spookysky.jpg');
        this.load.image('environment7', 'assets/panorama-360/KPNO-Drone-360-2-CC2-by-NOIRLab.jpg');
    }

    create ()
    {
        this.background = this.add.image(640, 360, 'stones').enableFilters().setScale(1.5);
        this.backgroundLight = this.background.filters.internal.addImageLight({
            environmentMap: 'environment3',
            normalMap: 'stones_n',
            colorFactor: [ 1.5, 1.5, 1.5 ]
        });

        this.spider = this.add.image(320, 480, 'spider').enableFilters().setRotation(1).setScale(0.5);
        this.spiderLight = this.spider.filters.internal.addImageLight({
            environmentMap: 'environment3',
            normalMap: 'spider_n',
            colorFactor: [ 2, 2, 2 ],
            modelRotationSource: this.spider
        });

        this.add.text(640, 48, 'Click to change light image', { fontSize: 24 }).setOrigin(0.5);

        this.environmentPreview = this.add.image(1100, 600, 'environment3').setScale(0.2);

        this.environmentIndex = 3;

        this.input.on('pointerdown', () => {
            this.environmentIndex++;
            if (this.environmentIndex > 7) { this.environmentIndex = 1; }
            const texture = `environment${this.environmentIndex}`;
            this.backgroundLight.setEnvironmentMap(texture);
            this.spiderLight.setEnvironmentMap(texture);
            this.environmentPreview.setTexture(texture);
        });
    }

    update (time, delta)
    {
        const angle = time / 3000;
        this.spider.setPosition(128 * Math.cos(angle) + 300, 128 * Math.sin(angle) + 500)
        .setRotation(angle);
    }
}

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

const game = new Phaser.Game(config);

Подготовка активов: нормальные карты и HDRI

Основа эффекта — два типа текстур: обычное диффузное изображение и его нормальная карта (normal map). Нормальная карта кодирует информацию о 'направлении' поверхности каждого пикселя (его условную нормаль) в цветовых каналах (R, G, B). Это позволяет рассчитать, как свет должен падать на плоский спрайт, создавая иллюзию рельефа.

Второй ключевой элемент — изображения-окружения (environment maps), которые выступают в роли источника света. Это могут быть HDRI-изображения или любые другие картинки, чьи цвета и яркость будут 'проецироваться' на объект.

Код загрузки:

this.load.image('stones', 'assets/normal-maps/stones.png');
this.load.image('stones_n', 'assets/normal-maps/stones_n.png');
this.load.image('spider', 'assets/normal-maps/spider.png');
this.load.image('spider_n', 'assets/normal-maps/spider_n.png');

this.load.image('environment1', 'assets/pics/town-wreck.jpg');
// ... загрузка environment2, environment3 и т.д.

Создание и настройка источников света

Эффект применяется не ко всей сцене, а к конкретным игровым объектам. Для этого объект (например, this.background) сначала нужно подготовить, вызвав метод .enableFilters(). После этого в его внутренний менеджер фильтров (filters.internal) добавляется источник света типа addImageLight.

Конфигурационный объект этого источника — сердце эффекта. В нем указывается, какое изображение (environmentMap) будет светить, какая нормальная карта (normalMap) описывает рельеф объекта, и множитель интенсивности света по каналам RGB (colorFactor).

Код создания света для фона и паука:

// Создание света для фона (камней)
this.backgroundLight = this.background.filters.internal.addImageLight({
    environmentMap: 'environment3',
    normalMap: 'stones_n',
    colorFactor: [ 1.5, 1.5, 1.5 ]
});

// Создание света для паука
this.spiderLight = this.spider.filters.internal.addImageLight({
    environmentMap: 'environment3',
    normalMap: 'spider_n',
    colorFactor: [ 2, 2, 2 ],
    modelRotationSource: this.spider
});

Обратите внимание на параметр modelRotationSource у паука. Он привязывает расчет освещения к вращению самого спрайта (this.spider). Это означает, что свет будет корректно 'скользить' по поверхности паука при его повороте, как если бы он был объемной моделью.

Динамическая смена освещения

Одна из сильных сторон этого подхода — возможность менять освещение 'на лету'. В примере по клику мыши циклически меняется изображение-окружение. Для этого используется метод .setEnvironmentMap() у созданного ранее объекта света (this.backgroundLight и this.spiderLight).

Код обработки клика:

this.input.on('pointerdown', () => {
    this.environmentIndex++;
    if (this.environmentIndex > 7) { this.environmentIndex = 1; }
    const texture = `environment${this.environmentIndex}`;
    // Меняем карту окружения для обоих источников света
    this.backgroundLight.setEnvironmentMap(texture);
    this.spiderLight.setEnvironmentMap(texture);
    // Обновляем превью-картинку в углу экрана
    this.environmentPreview.setTexture(texture);
});

Это позволяет создавать драматические переходы: например, мгновенно перенести сцену из подводного мира (undersea.jpg) в огненную бездну (fire.png).

Анимация и связь с физикой

Чтобы продемонстрировать, как свет взаимодействует с движущимся и вращающимся объектом, паук анимируется в методе update. Его позиция и угол поворота меняются на основе времени. Благодаря параметру modelRotationSource, установленному при создании света, освещение паука корректно обновляется при каждом кадре.

Код анимации в update:

update (time, delta)
{
    const angle = time / 3000;
    this.spider.setPosition(128 * Math.cos(angle) + 300, 128 * Math.sin(angle) + 500)
    .setRotation(angle);
}

Этот же принцип можно использовать для связи освещения с физическим движением тела (Phaser.Physics.Arcade.Sprite), создавая правдоподобное освещение для прыгающих, катящихся или падающих объектов.

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

Фильтр ImageLight в Phaser 3 — мощный инструмент для добавления 'псевдо-3D' освещения в 2D-игры без сложной работы с шейдерами вручную. Он оживляет статичные спрайты, добавляет атмосферу и позволяет динамически управлять настроением сцены. **Идеи для экспериментов:** 1. Используйте разные нормальные карты для одного и того же спрайта, чтобы быстро менять его воспринимаемую текстуру (например, с гладкой на шершавую). 2. Анимируйте параметр colorFactor для создания эффекта пульсации света или цветовых переходов. 3. Привяжите смену environmentMap не к клику, а к игровым событиям (вход в пещеру, использование заклинания, смена времени суток). 4. Поэкспериментируйте с очень контрастными или монохромными HDRI-картами для стилизованных эффектов.