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

Normal mapping (карты нормалей) — мощный приём для создания иллюзии рельефа на плоских поверхностях. В Phaser 3 с ними можно работать не только статично. Этот пример демонстрирует, как динамически модифицировать карту нормалей в реальном времени, используя фильтр `NormalTools`. Это открывает двери для создания «живого», движущегося освещения на спрайтах без анимации самой текстуры — например, для имитации вращающегося источника света, пульсирующей энергии или текущей воды.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    imageN;
    normalTools;
    normalTexture;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('phaser', 'assets/sprites/phaser-large.png');
        this.load.image('phaser_n', 'assets/normal-maps/phaser-large_n.png');
        this.load.image('chrome', 'assets/skies/chrome.png');
    }

    create ()
    {
        this.add.gradient({
            start: { x: 0.5, y: 0.5 },
            shape: { x: 0.5, y: 0.5 },
            shapeMode: 2,
            bands: {
                colorStart: 0xffff44,
                colorEnd: 0xaa4422,
                colorSpace: 1
            }
        }, 640, 360, 1280, 720);

        // Get a texture with a modified normal map.
        this.imageN = this.add.image(0, 0, 'phaser_n').setOrigin(0).setVisible(false);
        this.normalTools = this.imageN.enableFilters().filters.internal.addNormalTools({});
        this.normalTexture = this.textures.addDynamicTexture('phaser_n_tooled', this.imageN.width, this.imageN.height);

        // Use the modified normal map.
        const image = this.add.image(640, 360, 'phaser');
        image.enableFilters().filters.internal.addImageLight({
            environmentMap: 'chrome',
            normalMap: 'phaser_n_tooled'
        });

        // Display the normal map, original and altered.
        this.add.image(320, 560, 'phaser_n').setScale(0.5);
        this.add.image(960, 560, 'phaser_n_tooled').setScale(0.5);
    }

    update (time, delta)
    {
        this.normalTools.setRotation(time / 1000);
        this.normalTexture.draw(this.imageN).render();
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload загружаются необходимые изображения. Ключевые из них — основной спрайт ('phaser') и его соответствующая карта нормалей ('phaser_n'). Карта нормалей — это специальное изображение, где цвет каждого пикселя кодирует направление нормали (перпендикуляра к поверхности) в этой точке. Также загружается текстура окружения ('chrome'), которая будет использоваться как источник света (environment map).

this.load.image('phaser', 'assets/sprites/phaser-large.png');
this.load.image('phaser_n', 'assets/normal-maps/phaser-large_n.png');
this.load.image('chrome', 'assets/skies/chrome.png');

Создание фона и модификация карты нормалей

В create сначала создаётся градиентный фон для наглядности. Затем начинается основная работа: 1. Спрайт с оригинальной картой нормалей (phaser_n) добавляется в сцену, но скрывается (setVisible(false)). Он нужен не для отображения, а как источник данных. 2. На этот спрайт добавляется фильтр NormalTools через цепочку вызовов enableFilters().filters.internal.addNormalTools({}). Этот фильтр позволит программно менять параметры нормалей. 3. Создаётся динамическая текстура (addDynamicTexture) с именем 'phaser_n_tooled'. В неё мы будем «рисовать» модифицированную карту нормалей.

this.imageN = this.add.image(0, 0, 'phaser_n').setOrigin(0).setVisible(false);
this.normalTools = this.imageN.enableFilters().filters.internal.addNormalTools({});
this.normalTexture = this.textures.addDynamicTexture('phaser_n_tooled', this.imageN.width, this.imageN.height);

Применение модифицированной карты к основному спрайту

Далее создаётся основной видимый спрайт с логотипом Phaser. На него добавляется фильтр ImageLight, который отвечает за наложение освещения. Ключевой момент: в параметрах normalMap указывается не оригинальная текстура, а имя нашей динамической текстуры 'phaser_n_tooled'. Теперь освещение на спрайте будет рассчитываться на основе данных из этой изменяемой текстуры. Для сравнения внизу сцены отображаются оригинальная и изменённая карты нормалей.

const image = this.add.image(640, 360, 'phaser');
image.enableFilters().filters.internal.addImageLight({
    environmentMap: 'chrome',
    normalMap: 'phaser_n_tooled'
});

Анимация в реальном времени

Вся магия происходит в методе update, который вызывается каждый кадр. 1. У объекта normalTools меняется свойство вращения (setRotation), которое зависит от времени. Это заставляет «виртуальный источник света» вращаться вокруг спрайта. 2. Изменённые данные из скрытого спрайта this.imageN (к которому применён фильтр) переносятся в динамическую текстуру с помощью метода draw. 3. Вызов render() для динамической текстуры фиксирует эти изменения. Так как основной спрайт использует эту текстуру как карту нормалей, освещение на нём мгновенно обновляется.

this.normalTools.setRotation(time / 1000);
this.normalTexture.draw(this.imageN).render();

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

Фильтр NormalTools в связке с динамическими текстурами позволяет оживить статичное освещение. Вместо предрасчитанных кадров анимации вы управляете параметрами нормалей кодом. Для экспериментов попробуйте менять не только вращение (setRotation), но и другие свойства фильтра, например, смещение или масштаб. Можно привязать изменения к вводу игрока (движение мыши) или физике (освещение вспыхивает при столкновении), создавая глубоко интерактивные визуальные эффекты с относительно небольшими вычислительными затратами.