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

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

Версия 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('robot', [ 'assets/pics/equality-by-ragnarok.png', 'assets/normal-maps/equality-by-ragnarok_n.png' ]);
    }

    create ()
    {
        const robot = this.add.image(-300, 0, 'robot').setOrigin(0);

        //  The following just displays the normal map on-screen, so you can see that it loaded properly

        const canvasTexture = this.textures.createCanvas('normalMap', 400, 600);

        canvasTexture.context.drawImage(robot.texture.getDataSourceImage(), -300, 0);

        canvasTexture.refresh();

        const robotMap = this.add.image(400, 0, 'normalMap').setOrigin(0);
    }
}

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

const game = new Phaser.Game(config);

Загрузка связки текстур: диффузная + Normal Map

Ключевой метод для загрузки — this.load.image(). Обычно ему передают ключ и путь к одному изображению. Однако, чтобы загрузить Normal Map вместе с основной текстурой, вторым аргументом нужно передать массив из двух строк.

Первым элементом массива должен быть путь к основному (диффузному) изображению, вторым — путь к карте нормалей. Phaser автоматически свяжет эти две текстуры под одним ключом.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('robot', [ 
    'assets/pics/equality-by-ragnarok.png',
    'assets/normal-maps/equality-by-ragnarok_n.png'
]);

После такой загрузки ключ 'robot' будет ссылаться на текстуру, у которой есть как основное изображение, так и Normal Map. Это необходимо для последующего использования со светом (Lights2D).

Создание спрайта и проверка загрузки

После загрузки в методе create() мы можем создать изображение обычным способом, используя тот же ключ.

const robot = this.add.image(-300, 0, 'robot').setOrigin(0);

В данном примере спрайт размещён за левой границей экрана (x = -300), потому что основная цель кода — не показать его в игре, а продемонстрировать техническую сторону загрузки. Следующий блок кода служит исключительно для визуальной проверки: он рисует загруженную Normal Map на отдельном холсте (CanvasTexture) и отображает этот холст на сцене.

Это полезный приём для отладки, чтобы убедиться, что Normal Map загрузилась корректно и соответствует ожиданиям по размерам и содержимому.

Создание CanvasTexture для отладки Normal Map

Phaser позволяет создавать текстуры на лету. Класс TextureManager (доступный через this.textures) предоставляет метод createCanvas().

Сначала мы создаём пустую текстуру на основе холста с заданным ключом и размерами.

const canvasTexture = this.textures.createCanvas('normalMap', 400, 600);

Затем, используя контекст рисования этого холста (canvasTexture.context), мы вручную отрисовываем на нём изображение из источника данных текстуры нашего спрайта. Важно: метод getDataSourceImage() возвращает HTMLImageElement, связанный с текстурой.

canvasTexture.context.drawImage(robot.texture.getDataSourceImage(), -300, 0);

Обратите внимание на координаты (-300, 0). Мы сдвигаем изображение, потому что исходный спрайт robot был создан с координатой x = -300. Чтобы нарисовать его в начале холста, мы компенсируем этот сдвиг отрицательной координатой.

После внесения изменений в холст текстуру необходимо обновить, чтобы Phaser пересчитал её внутренний WebGL-текстурный объект.

canvasTexture.refresh();

И наконец, создаём изображение на сцене, используя эту новую, только что созданную текстуру.

const robotMap = this.add.image(400, 0, 'normalMap').setOrigin(0);

В результате рядом мы увидим саму Normal Map — обычно это синеватое изображение, где цвет кодирует направление нормалей поверхности.

Конфигурация игры и запуск

Конфигурация игры стандартна. Важно, чтобы размеры игрового холста (width и height) были достаточными для отображения всех элементов, которые мы создаём в примере (основное изображение и его Normal Map).

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

const game = new Phaser.Game(config);

При запуске этой конфигурации вы увидите два изображения: слева — обычная диффузная текстура, справа — её Normal Map, извлечённая и отрисованная на холсте. Это подтверждает успешную загрузку.

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

Загрузка Normal Map в Phaser 3 — это простой процесс, сводящийся к передаче массива путей в this.load.image(). После загрузки текстура готова к использованию с системой освещения Lights2D. Для экспериментов попробуйте

  1. Добавить источник света (this.lights.addLight()) и включить рендеринг света на сцене, чтобы увидеть, как Normal Map создаёт иллюзию объёма
  2. Использовать другие типы карт (например, Specular Map), загружая их третьим элементом массива
  3. Автоматизировать процесс отладки, написав функцию, которая для любого ключа текстуры создаёт и отображает её Normal Map в углу экрана