О чем этот пример
Нормальные карты — мощный инструмент для симуляции объёма и освещения на плоских спрайтах. Однако их статичность часто ограничивает выразительность. Пример демонстрирует, как в Phaser можно программно искажать нормали карты в реальном времени, создавая иллюзию "дышащей" или меняющейся поверхности. Это открывает двери для визуальных эффектов, таких как пульсирующая органика, волнующаяся вода или деформирующиеся под магией объекты, без необходимости перерисовывать текстуры.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
stonesN;
normalTools;
normalTexture;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('stones', 'assets/normal-maps/stones.png');
this.load.image('stones_n', 'assets/normal-maps/stones_n.png');
this.load.image('environment', 'assets/skies/grass.jpg');
}
create ()
{
this.stonesN = this.add.image(0, 0, 'stones_n').setOrigin(0).setVisible(false).enableFilters();
this.normalTools = this.stonesN.filters.internal.addNormalTools();
this.normalTexture = this.textures.addDynamicTexture('stones_n_tooled', this.stonesN.width, this.stonesN.height);
const stones = this.add.image(640, 360, 'stones').enableFilters();
Phaser.Actions.FitToRegion(stones, 1);
stones.filters.internal.addImageLight({
environmentMap: 'environment',
normalMap: 'stones_n_tooled'
});
}
update (time, delta)
{
// Bend the surface normals to or away from the camera.
this.normalTools.facingPower = 1.5 + Math.sin(time / 200);
this.normalTexture.draw(this.stonesN).render();
}
}
const config = {
type: Phaser.WEBGL,
width: 1280,
height: 720,
backgroundColor: '#2d3440',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и ресурсов
В методе preload загружаются три изображения: цветная текстура (stones), соответствующая ей нормальная карта (stones_n) и текстура окружения (environment) для симуляции отражённого света.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('stones', 'assets/normal-maps/stones.png');
this.load.image('stones_n', 'assets/normal-maps/stones_n.png');
this.load.image('environment', 'assets/skies/grass.jpg');
}
Создание динамической нормальной карты
В create происходит основная настройка. Изображение с нормалями (stones_n) добавляется на сцену, но скрывается (setVisible(false)), так как нам нужна не сама картинка, а её пиксельные данные. Для этого изображения включается система фильтров (enableFilters()).
Ключевой момент — добавление на это изображение специального фильтра addNormalTools(). Этот фильтр предоставляет инструменты для программного управления нормалями. Результат его работы мы будем перерисовывать в динамическую текстуру normalTexture.
create ()
{
this.stonesN = this.add.image(0, 0, 'stones_n').setOrigin(0).setVisible(false).enableFilters();
this.normalTools = this.stonesN.filters.internal.addNormalTools();
this.normalTexture = this.textures.addDynamicTexture('stones_n_tooled', this.stonesN.width, this.stonesN.height);
}
Применение освещения к основному спрайту
Далее создаётся основной видимый спрайт stones с цветной текстурой. Он масштабируется под размер региона (в данном случае, под сцену). На него добавляется фильтр освещения addImageLight(). Этот фильтр использует нашу динамическую текстуру stones_n_tooled как нормальную карту и текстуру environment для создания эффекта окружения.
const stones = this.add.image(640, 360, 'stones').enableFilters();
Phaser.Actions.FitToRegion(stones, 1);
stones.filters.internal.addImageLight({
environmentMap: 'environment',
normalMap: 'stones_n_tooled'
});
}
Анимация нормалей в реальном времени
Магия происходит в методе update. Каждый кадр мы меняем свойство facingPower объекта normalTools. Это свойство управляет тем, насколько нормали поверхности "смотрят" на камеру. Значение больше 1 заставляет их сильнее отклоняться к камере, создавая эффект выпуклости. Значение меньше 1, наоборот, отклоняет их, создавая вогнутость.
Здесь значение плавно колеблется от 0.5 до 2.5 с помощью синуса от времени, создавая пульсацию. После изменения параметра, мы перерисовываем (draw) исходную нормальную карту с применённым фильтром normalTools в нашу динамическую текстуру и принудительно рендерим изменения (render()). Обновлённая текстура тут же подхватывается фильтром освещения на основном спрайте.
update (time, delta)
{
// Bend the surface normals to or away from the camera.
this.normalTools.facingPower = 1.5 + Math.sin(time / 200);
this.normalTexture.draw(this.stonesN).render();
}
Что попробовать дальше
Техника динамического изменения нормалей через addNormalTools() и DynamicTexture позволяет оживить статичные объекты, добавив им выразительности и интерактивности. Для экспериментов попробуйте: привязать изменение facingPower к положению мыши для создания эффекта "вмятины" под курсором; использовать другие параметры normalTools, например, для вращения нормалей; анимировать не только камни, но и воду, металл или кожу монстра, меняя характер движения синусоиды на более сложную математику.
