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

Хотите, чтобы ваша 2D-игра на Phaser выглядела объёмнее и атмосфернее? Ключ к этому — динамическое освещение и карты нормалей (normal maps). Вместо статичных плоских спрайтов вы получите объекты, которые реагируют на свет и тень, создавая иллюзию глубины. В этой статье мы на практике разберём пример, который покажет, как загрузить текстурный атлас с нормал-мапами и интегрировать его в систему освещения движка.

Версия 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.setPath('assets/loader-tests/');

        this.load.atlas('megaset', [ 'texture-packer-atlas-with-normals-0.png', 'texture-packer-atlas-with-normals-0_n.png' ], 'texture-packer-atlas-with-normals.json');
    }

    create ()
    {
        const light = this.lights.addLight(0, 0, 200);

        this.lights.enable().setAmbientColor(0x555555);

        this.input.on('pointermove', pointer =>
        {

            light.x = pointer.x;
            light.y = pointer.y;

        });

        const atlasTexture = this.textures.get('megaset');

        const frames = atlasTexture.getFrameNames();

        Phaser.Utils.Array.Shuffle(frames);

        for (let i = 0; i < frames.length; i++)
        {
            const x = Phaser.Math.Between(100, 700);
            const y = Phaser.Math.Between(100, 500);

            this.add.image(x, y, 'megaset', frames[i]).setLighting(true);
        }
    }
}

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

const game = new Phaser.Game(config);

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

Всё начинается с этапа предзагрузки (preload). В Phaser для загрузки атласов, созданных, например, в Texture Packer, используется метод this.load.atlas(). Особенность нашего примера в том, что мы загружаем не только цветовую текстуру, но и соответствующую ей карту нормалей.

Карта нормалей — это специальное изображение, где цвет каждого пикселя кодирует информацию о направлении поверхности (нормали). Это позволяет рассчитать, как свет должен падать на плоский 2D-спрайт, создавая эффект объёма и рельефа.

В примере мы видим, что методу передаётся массив из двух путей к изображениям.

this.load.atlas('megaset', [ 'texture-packer-atlas-with-normals-0.png', 'texture-packer-atlas-with-normals-0_n.png' ], 'texture-packer-atlas-with-normals.json');

Первый файл (...-0.png) — это обычный цветовой атлас. Второй файл с суффиксом _n (...-0_n.png) — это карта нормалей. Третий аргумент — JSON-файл, описывающий структуру атласа (рамки и их координаты). Phaser автоматически связывает основной атлас и карту нормалей по их ключу 'megaset'.

Настройка системы освещения на сцене

Чтобы использовать освещение в Phaser, необходимо активировать систему Lights и настроить её. Это делается в методе create.

Сначала создаётся источник света — точечная лампа. Метод this.lights.addLight() принимает начальные координаты X, Y и радиус освещения.

const light = this.lights.addLight(0, 0, 200);

Далее система освещения явно включается вызовом this.lights.enable(). Без этого шага свет не будет работать. Также устанавливается фоновый (ambient) свет. Он добавляет постоянную, равномерную подсветку ко всем объектам, чтобы области в тени не становились completamente чёрными.

this.lights.enable().setAmbientColor(0x555555);

Затем мы привязываем источник света к движению указателя мыши. При каждом перемещении мыши (pointermove) координаты лампы обновляются.

this.input.on('pointermove', pointer => {
    light.x = pointer.x;
    light.y = pointer.y;
});

Создание спрайтов с поддержкой освещения

Теперь нужно создать объекты, которые будут реагировать на наш источник света. Для этого мы получаем доступ к загруженной текстуре по её ключу.

const atlasTexture = this.textures.get('megaset');

Из текстуры можно получить массив имён всех отдельных кадров (frames), которые в неё входят.

const frames = atlasTexture.getFrameNames();

Чтобы спрайты на сцене располагались в случайном порядке, имена кадров перемешиваются с помощью утилиты Phaser.Utils.Array.Shuffle().

Далее в цикле для каждого имени кадра создаётся изображение (this.add.image). Ключевой момент — вызов метода .setLighting(true) для этого изображения. Именно этот флаг указывает Phaser, что данный спрайт должен взаимодействовать с системой освещения и использовать привязанную к его текстуре карту нормалей.

this.add.image(x, y, 'megaset', frames[i]).setLighting(true);

Координаты X и Y для каждого спрайта генерируются случайным образом в заданных пределах с помощью Phaser.Math.Between().

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

Как видите, добавить продвинутое динамическое освещение с нормал-мапами в Phaser довольно просто. Основные шаги: корректно загрузить связанные текстуры, включить систему света и не забыть вызвать .setLighting(true) для нужных спрайтов. Для экспериментов попробуйте изменить цвет и интенсивность окружающего света (setAmbientColor), добавить несколько статических или движущихся источников света разного цвета и радиуса, или применить освещение только к определённым слоям объектов, создавая более глубокую композицию сцены.