О чем этот пример
Эффекты освещения и окружения могут кардинально изменить атмосферу вашей игры. В этом примере мы рассмотрим, как использовать фильтр `PanoramaBlur` и систему `ImageLight` в Phaser для создания реалистичного динамического освещения на основе HDRI-панорам. Вы научитесь генерировать несколько уровней размытия из одной панорамы и применять их для освещения 3D-объектов в реальном времени, что особенно полезно для создания сцен с изменяющимся временем суток или погодными условиями.
Версия 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('orb-n', 'assets/normal-maps/orb-256.png');
// For viewing at any angle, a 360 degree panorama with 180 degree height is ideal.
// You can acquire many such assets from Adobe Stock or other sources.
this.load.image('environment', 'assets/panorama-360/KPNO-Drone-360-2-CC2-by-NOIRLab.jpg');
}
create ()
{
const background = this.add.gradient({
start: { x: 0, y: 1 },
shape: { x: 0, y: -1 },
bands: {
colorStart: 0x2d3440,
colorEnd: 0x204070
},
}, 640, 360, 1280, 720);
// Create an object as the base for a PanoramaBlur.
const envBlurSource = this.add.image(0, 0, 'environment').setOrigin(0);
const envW = envBlurSource.width;
const envH = envBlurSource.height;
const panoramaBlur = envBlurSource.enableFilters().filters.internal.addPanoramaBlur({
samplesX: envW / 16, // Be careful: more samples are much more costly!
samplexY: envH / 16,
power: 2 // Emphasize sun over darker areas.
});
// Render panorama at blur 1...
const envTexture1 = this.textures.addDynamicTexture('environment-blur-1', envW, envH);
envTexture1.draw(envBlurSource).render();
// ... and at blur 0.5...
const envTexture2 = this.textures.addDynamicTexture('environment-blur-0.5', envW, envH);
panoramaBlur.power = 1.5;
panoramaBlur.radius = 0.5;
envTexture2.draw(envBlurSource).render();
// ... and at blur 0.25...
const envTexture3 = this.textures.addDynamicTexture('environment-blur-0.25', envW, envH);
panoramaBlur.power = 1.25;
panoramaBlur.radius = 0.25;
envTexture3.draw(envBlurSource).render();
// ... and at blur 0.125...
const envTexture4 = this.textures.addDynamicTexture('environment-blur-0.125', envW, envH);
panoramaBlur.power = 1.125;
panoramaBlur.radius = 0.125;
envTexture4.draw(envBlurSource).render();
// ... and at blur 0.0625...
const envTexture5 = this.textures.addDynamicTexture('environment-blur-0.0625', envW, envH);
panoramaBlur.power = 1.0625;
panoramaBlur.radius = 0.0625;
envTexture5.draw(envBlurSource).render();
// Remove blur source because blurs are costly per-frame.
envBlurSource.destroy();
// Display textures as clickable icons.
this.add.text(320, 32, 'Click a blur level').setOrigin(0.5);
const image0 = this.add.image(160, 200, 'environment').setScale(0.2);
const image1 = this.add.image(160, 400, 'environment-blur-0.125').setScale(0.2);
const image2 = this.add.image(160, 600, 'environment-blur-0.5').setScale(0.2);
const image3 = this.add.image(480, 200, 'environment-blur-0.0625').setScale(0.2);
const image4 = this.add.image(480, 400, 'environment-blur-0.25').setScale(0.2);
const image5 = this.add.image(480, 600, 'environment-blur-1').setScale(0.2);
// Draw an orb for reuse.
const rect = this.add.rectangle(0, 0, 512, 512, 0xffffff);
rect.enableFilters().filters.internal.addMask('orb-n');
const orbTexture = this.textures.addDynamicTexture('orb', 512, 512);
orbTexture.draw(rect, 256, 256).render();
rect.destroy();
// Light an orb with the selected texture.
const orb = this.add.image(960, 360, 'orb');
this.orbLightImage = orb.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment',
normalMap: 'orb-n'
});
// Allow texture swapping.
const setClick = (sprite, texture) => {
sprite.setInteractive().on('pointerdown', () => {
this.orbLightImage.setEnvironmentMap(texture);
});
}
setClick(image0, 'environment');
setClick(image1, 'environment-blur-0.125');
setClick(image2, 'environment-blur-0.5');
setClick(image3, 'environment-blur-0.0625');
setClick(image4, 'environment-blur-0.25');
setClick(image5, 'environment-blur-1');
}
update (time, delta)
{
this.orbLightImage.viewMatrix
.identity() // Reset matrix
.rotateY(time / 5000); // Look left/right
}
}
const config = {
type: Phaser.WEBGL,
width: 1280,
height: 720,
backgroundColor: '#2d3440',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка ресурсов и создание базовой сцены
В методе preload загружаются необходимые ресурсы: нормальная карта для объекта (орб) и HDRI-панорама окружения. Панорама на 360 градусов идеально подходит для создания реалистичного освещения с любого угла.
В create мы сначала создаем градиентный фон для визуального контраста. Затем загруженная панорама добавляется на сцену как источник для фильтра размытия. Важно отметить, что исходное изображение панорамы будет использовано только для предварительной обработки и затем удалено, чтобы не нагружать рендеринг в реальном времени.
const background = this.add.gradient({
start: { x: 0, y: 1 },
shape: { x: 0, y: -1 },
bands: {
colorStart: 0x2d3440,
colorEnd: 0x204070
},
}, 640, 360, 1280, 720);
const envBlurSource = this.add.image(0, 0, 'environment').setOrigin(0);
Генерация панорам с разным уровнем размытия
Ключевой этап — применение фильтра PanoramaBlur к исходной панораме для создания нескольких её версий с разной степенью детализации. Фильтр добавляется через envBlurSource.enableFilters().filters.internal.addPanoramaBlur(). Параметры samplesX и samplexY (опечатка в исходнике, должно быть samplesY) определяют количество выборок для размытия и напрямую влияют на производительность: больше выборок — выше качество, но ниже FPS. Параметр power усиливает яркие области (например, солнце) над тёмными.
Затем, меняя свойства фильтра radius и power, мы рендерим несколько версий панорамы в динамические текстуры с помощью this.textures.addDynamicTexture() и draw(). Это предварительный рендеринг (baking), который выполняется один раз при запуске сцены.
const panoramaBlur = envBlurSource.enableFilters().filters.internal.addPanoramaBlur({
samplesX: envW / 16,
samplexY: envH / 16,
power: 2
});
const envTexture1 = this.textures.addDynamicTexture('environment-blur-1', envW, envH);
envTexture1.draw(envBlurSource).render();
panoramaBlur.radius = 0.5;
envTexture2.draw(envBlurSource).render();
После генерации всех необходимых текстур исходный объект envBlurSource уничтожается (destroy()), так как сам фильтр PanoramaBlur ресурсоёмок для вычислений в каждом кадре.
Создание освещаемого объекта и источника света
Теперь создадим объект, который будет реагировать на освещение. В примере это орб (сфера). Сначала создаётся белый прямоугольник, к которому применяется фильтр маски (addMask) с использованием нормальной карты orb-n. Результат рендерится в отдельную текстуру orb, а временный прямоугольник удаляется.
Затем текстура орба добавляется на сцену, и к ней применяется фильтр ImageLight. Этот фильтр использует две карты: environmentMap (одна из наших панорам) в качестве источника света и normalMap для расчёта отражений на поверхности объекта.
const orb = this.add.image(960, 360, 'orb');
this.orbLightImage = orb.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment',
normalMap: 'orb-n'
});
Свойство this.orbLightImage сохраняется для последующего управления в методе update.
Интерактивность: переключение карт окружения
Чтобы продемонстрировать разницу между уровнями размытия, создаётся интерфейс. Несколько уменьшенных изображений (превью) сгенерированных панорам размещаются на сцене. Каждому превью назначается интерактивность через setInteractive().
При клике на превью вызывается функция, которая меняет карту окружения у фильтра ImageLight с помощью метода setEnvironmentMap(). Это позволяет в реальном времени увидеть, как разные уровни размытия панорамы (от чёткой до сильно размытой) влияют на освещение и отражения на поверхности орба.
const setClick = (sprite, texture) => {
sprite.setInteractive().on('pointerdown', () => {
this.orbLightImage.setEnvironmentMap(texture);
});
}
setClick(image0, 'environment');
Динамическое вращение обзора (камеры)
Для создания иллюзии, что мы осматриваем объект при вращающемся вокруг него источнике света (или наоборот), в методе update каждый кадр модифицируется видовая матрица (viewMatrix) фильтра ImageLight.
Сначала матрица сбрасывается в единичное состояние методом identity(), затем к ней применяется поворот вокруг оси Y на угол, зависящий от времени. Это создаёт плавное вращение освещения вокруг объекта, демонстрируя эффект с разных сторон.
update (time, delta)
{
this.orbLightImage.viewMatrix
.identity()
.rotateY(time / 5000);
}
Что попробовать дальше
Этот пример показывает мощь комбинации предварительно рассчитанных фильтров и динамического освещения в Phaser. Вы можете использовать эту технику для создания статичных "запечённых" (baked) карт освещения для уровней, динамической смены времени суток (подставляя разные панорамы) или симуляции различных материалов у объектов, меняя нормальные карты.
Поэкспериментируйте: попробуйте использовать панорамы с разным содержимым (город, лес, интерьер), измените параметры размытия samples и power для баланса качества и производительности или анимируйте другие параметры фильтра ImageLight, например, интенсивность.
