О чем этот пример
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), но и другие свойства фильтра, например, смещение или масштаб. Можно привязать изменения к вводу игрока (движение мыши) или физике (освещение вспыхивает при столкновении), создавая глубоко интерактивные визуальные эффекты с относительно небольшими вычислительными затратами.
