О чем этот пример
Эффективное освещение — ключ к созданию атмосферных и профессионально выглядящих игр. В этом руководстве мы разберем, как использовать фильтр Image Light в Phaser для симуляции реалистичного отражения света на 3D-объектах, используя 2D-спрайты и панорамные карты окружения. Вы научитесь подготавливать окружение, создавать несколько уровней размытия для имитации шероховатости поверхности и анимировать отражения, что позволит добавить в вашу игру эффекты металлических или стеклянных поверхностей без использования настоящего 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 // Emphasise 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();
// Draw an orb for reuse.
const orb = this.add.rectangle(0, 0, 256, 256, 0xffffff);
orb.enableFilters().filters.internal.addMask('orb-n');
const orbTexture = this.textures.addDynamicTexture('orb', 256, 256);
orbTexture.draw(orb, 128, 128).render();
orb.destroy();
// Light a series of orbs with the panorama at different roughness levels.
const orb1 = this.add.image(200, 200, 'orb');
this.orb1LightImage = orb1.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-1',
normalMap: 'orb-n'
});
const orb2 = this.add.image(640, 200, 'orb');
this.orb2LightImage = orb2.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-0.5',
normalMap: 'orb-n'
});
const orb3 = this.add.image(1080, 200, 'orb');
this.orb3LightImage = orb3.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-0.25',
normalMap: 'orb-n'
});
const orb4 = this.add.image(200, 520, 'orb');
this.orb4LightImage = orb4.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-0.125',
normalMap: 'orb-n'
});
const orb5 = this.add.image(640, 520, 'orb');
this.orb5LightImage = orb5.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-0.0625',
normalMap: 'orb-n'
});
const orb6 = this.add.image(1080, 520, 'orb');
this.orb6LightImage = orb6.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment', // Unfiltered environment.
normalMap: 'orb-n'
});
}
update (time, delta)
{
const filters = [
this.orb1LightImage,
this.orb2LightImage,
this.orb3LightImage,
this.orb4LightImage,
this.orb5LightImage,
this.orb6LightImage
];
for (const filter of filters)
{
filter.viewMatrix
.identity() // Reset matrix
.rotateY(time / 5000) // Look left/right
.rotateX(0.5 * Math.sin(time / 1765)); // Look up/down
}
}
}
const config = {
type: Phaser.WEBGL,
width: 1280,
height: 720,
backgroundColor: '#2d3440',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка ресурсов и создание сцены
Основа эффекта — панорамная текстура окружения (environment) и карта нормалей (orb-n). Карта нормалей кодирует информацию о поверхности объекта, что позволяет фильтру рассчитать отражение света.
Первым делом загружаем изображения в методе preload. Также создаем градиентный фон для красивого контраста.
this.load.image('orb-n', 'assets/normal-maps/orb-256.png');
this.load.image('environment', 'assets/panorama-360/KPNO-Drone-360-2-CC2-by-NOIRLab.jpg');
const background = this.add.gradient({
start: { x: 0, y: 1 },
shape: { x: 0, y: -1 },
bands: {
colorStart: 0x2d3440,
colorEnd: 0x204070
},
}, 640, 360, 1280, 720);
Создание карт окружения с разной шероховатостью
Чем более шероховата поверхность, тем более размытым выглядит отражение на ней. Мы создадим несколько версий панорамы с разной степенью размытия, которые будут имитировать разную шероховатость материала.
Для этого используется фильтр PanoramaBlur. Важно: параметр samplesX напрямую влияет на производительность. Мы применяем фильтр к исходному изображению, а затем рендерим результат в динамические текстуры.
const envBlurSource = this.add.image(0, 0, 'environment').setOrigin(0);
const panoramaBlur = envBlurSource.enableFilters().filters.internal.addPanoramaBlur({
samplesX: envW / 16,
samplexY: envH / 16,
power: 2
});
После настройки фильтра мы последовательно меняем его свойства power и radius и рендерим изображение в новую текстуру. Исходный спрайт envBlurSource удаляется после создания всех текстур для экономии ресурсов.
const envTexture1 = this.textures.addDynamicTexture('environment-blur-1', envW, envH);
envTexture1.draw(envBlurSource).render();
panoramaBlur.power = 1.5;
panoramaBlur.radius = 0.5;
Подготовка переиспользуемого спрайта сферы
Фильтр ImageLight применяется к объекту на сцене. Чтобы создать несколько одинаковых сфер, мы сначала рисуем одну белую сферу как Phaser.GameObjects.Rectangle, применяем к ней маску в виде карты нормалей и рендерим результат в отдельную текстуру. Эта текстура 'orb' будет использоваться для всех шести сфер на сцене, что эффективно с точки зрения памяти.
Ключевой момент: фильтр Mask из filters.internal использует карту нормалей для формирования базовой формы объекта, к которой затем будет применено освещение.
const orb = this.add.rectangle(0, 0, 256, 256, 0xffffff);
orb.enableFilters().filters.internal.addMask('orb-n');
const orbTexture = this.textures.addDynamicTexture('orb', 256, 256);
orbTexture.draw(orb, 128, 128).render();
orb.destroy();
Добавление освещения к сферам
Теперь создаем шесть спрайтов сфер, используя подготовленную текстуру 'orb'. К каждому из них применяем фильтр addImageLight. Этот фильтр связывает спрайт с картой нормалей (normalMap) и одной из текстур окружения (environmentMap), созданных на предыдущем шаге.
Разные текстуры окружения (от самой размытой 'environment-blur-1' до оригинальной 'environment') создают иллюзию разной шероховатости материала сферы. Ссылки на фильтры сохраняются в свойствах сцены (например, this.orb1LightImage), чтобы анимировать их в методе update.
const orb1 = this.add.image(200, 200, 'orb');
this.orb1LightImage = orb1.enableFilters().filters.internal.addImageLight({
environmentMap: 'environment-blur-1',
normalMap: 'orb-n'
});
Анимация отражения (вращение окружения)
Чтобы отражение на сферах динамически менялось, как если бы наблюдатель или источник света двигались, мы анимируем свойство viewMatrix каждого фильтра ImageLight в методе update.
Матрица сбрасывается методом .identity(), а затем к ней применяются повороты по осям Y и X. Значение time (время с начала работы сцены) обеспечивает плавную и непрерывную анимацию.
for (const filter of filters) {
filter.viewMatrix
.identity()
.rotateY(time / 5000)
.rotateX(0.5 * Math.sin(time / 1765));
}
Этот подход имитирует изменение угла обзора на панорамное окружение, заставляя блики и отражения "плыть" по поверхности сфер.
Что попробовать дальше
Фильтр ImageLight в Phaser открывает мощные возможности для создания псевдо-3D освещения, используя 2D-спрайты. Основной принцип — связка карты нормалей объекта и текстуры панорамного окружения. Для экспериментов попробуйте: использовать другие панорамные изображения (например, интерьеры комнат), анимировать параметры фильтра PanoramaBlur для динамического изменения шероховатости объекта или применить эффект к спрайтам сложной формы, создав для них соответствующие карты нормалей.
