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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    background;
    backgroundLight;
    environmentSky;
    environmentTrees;
    environmentCompositeTexture;
    environmentCompositeImage;
    redbubble;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/textures/alien-metal.jpg');
        this.load.image('bg_n', 'assets/textures/alien-metal-n.jpg');
        this.load.image('spider', 'assets/normal-maps/spider.png');
        this.load.image('spider_n', 'assets/normal-maps/spider_n.png');
        this.load.image('ms3-sky', 'assets/skies/ms3-sky.png');
        this.load.image('ms3-trees', 'assets/skies/ms3-trees.png');
        this.load.image('redbubble', 'assets/particles/redbubble.png');
    }

    create ()
    {
        this.environmentSky = this.add.tileSprite(0, 0, 1280, 444, 'ms3-sky').setTileScale(1.5).setOrigin(0).setVisible(false);
        this.environmentTrees = this.add.tileSprite(0, 400, 1280, 320, 'ms3-trees').setOrigin(0).setVisible(false);
        this.redbubble = this.add.image(640, 128, 'redbubble').setScale(2);

        this.environmentCompositeTexture = this.textures.addDynamicTexture('environmentComposite', 1280, 720);
        this.environmentCompositeImage = this.add.image(0, 0, 'environmentComposite').setOrigin(0);

        this.spider = this.add.image(640, 360, 'spider').enableFilters();
        this.spiderLight = this.spider.filters.internal.addImageLight({
            environmentMap: 'environmentComposite',
            normalMap: 'spider_n',
            colorFactor: [ 2, 2, 2 ],
            modelRotationSource: this.spider
        });
    }

    update (time, delta)
    {
        this.environmentSky.tilePositionX = time / 60;
        this.environmentTrees.tilePositionX = time / 20;
        this.redbubble.setPosition(512 * Math.cos(time / 1000) + 640, 256 * Math.sin(time / 1000) + 360);
        this.environmentCompositeTexture.draw(this.environmentSky).draw(this.environmentTrees).draw(this.redbubble).render();

        this.spider.setRotation(0.2 * Math.sin(time/3000));
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов: текстуры и карты нормалей

Для работы фильтра ImageLight необходимы две группы ресурсов: обычные текстуры и их карты нормалей (normal maps). Карта нормалей — это специальное изображение, где цвет каждого пикселя кодирует информацию о направлении поверхности (её «выпуклостях» и «впадинах»). Фильтр использует эти данные для расчёта отражения света.

this.load.image('bg', 'assets/textures/alien-metal.jpg');
this.load.image('bg_n', 'assets/textures/alien-metal-n.jpg'); // _n суффикс для карты нормалей
this.load.image('spider', 'assets/normal-maps/spider.png');
this.load.image('spider_n', 'assets/normal-maps/spider_n.png');

Также загружаются фоновые слои (ms3-sky, ms3-trees) и текстура частицы (redbubble), которые позже станут источником света в нашей сцене.

Создание динамического окружения

Окружение, которое будет отражаться на объекте, собирается из нескольких слоёв в одну динамическую текстуру. Это позволяет объединить независимо анимированные элементы (небо, деревья, частицы) в единую "карту окружения" (environment map).

// Создаём невидимые плиточные спрайты для фона
this.environmentSky = this.add.tileSprite(0, 0, 1280, 444, 'ms3-sky').setTileScale(1.5).setOrigin(0).setVisible(false);
this.environmentTrees = this.add.tileSprite(0, 400, 1280, 320, 'ms3-trees').setOrigin(0).setVisible(false);
// Создаём видимую частицу
this.redbubble = this.add.image(640, 128, 'redbubble').setScale(2);

// Создаём пустую динамическую текстуру
this.environmentCompositeTexture = this.textures.addDynamicTexture('environmentComposite', 1280, 720);
// Создаём изображение, которое будет отображать эту текстуру (для отладки)
this.environmentCompositeImage = this.add.image(0, 0, 'environmentComposite').setOrigin(0);

Ключевой объект — environmentCompositeTexture. Это холст в памяти, на который мы будем рисовать все слои окружения каждый кадр.

Применение фильтра Image Light к объекту

Фильтр добавляется к игровому объекту, который должен отражать окружение. В данном случае это спрайт паука. Важно сначала вызвать .enableFilters() на целевом изображении.

this.spider = this.add.image(640, 360, 'spider').enableFilters();
this.spiderLight = this.spider.filters.internal.addImageLight({
    environmentMap: 'environmentComposite', // Имя нашей динамической текстуры
    normalMap: 'spider_n',                 // Карта нормалей для паука
    colorFactor: [ 2, 2, 2 ],              // Множитель интенсивности отражения (RGB)
    modelRotationSource: this.spider       // Объект, чьё вращение влияет на расчёт
});

Параметр modelRotationSource связывает вращение модели (паука) с расчётом отражения. Когда паук поворачивается, "свет" скользит по его поверхности естественным образом, создавая иллюзию объёма.

Анимация в методе Update

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

// Анимация прокрутки фона для иллюзии движения
this.environmentSky.tilePositionX = time / 60;
this.environmentTrees.tilePositionX = time / 20;

// Круговое движение частицы по синусоиде и косинусоиде
this.redbubble.setPosition(512 * Math.cos(time / 1000) + 640, 256 * Math.sin(time / 1000) + 360);

// Отрисовка всех слоёв в динамическую текстуру и её принудительный рендер
this.environmentCompositeTexture.draw(this.environmentSky).draw(this.environmentTrees).draw(this.redbubble).render();

// Плавное вращение паука
this.spider.setRotation(0.2 * Math.sin(time/3000));

Цепочка вызовов .draw().draw().render() перерисовывает динамическую текстуру каждый кадр. Поскольку фильтр ImageLight использует эту текстуру как environmentMap, отражение на пауке мгновенно обновляется, реагируя на движение пузыря и прокрутку неба.

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

Фильтр ImageLight — мощный инструмент для добавления динамического освещения на основе окружения (Image-Based Lighting). Он превращает статичные 2D-спрайты в визуально сложные объекты, взаимодействующие с миром. Для экспериментов попробуйте: изменить colorFactor на [0, 4, 0] для зловещего зелёного свечения; использовать другую карту нормалей для грубой или гладкой поверхности; добавить больше светящихся частиц в составную текстуру или сделать источником света интерфейс игры.