О чем этот пример
Создание игр часто требует выхода за рамки 2D. Интеграция мощного 3D-движка Three.js с Phaser открывает новые возможности: от сложных визуальных эффектов и анимаций до полноценных гибридных сцен. Этот подход позволяет использовать проверенную простоту Phaser для игровой логики и интерфейса, одновременно задействуя богатый инструментарий Three.js для трёхмерной графики, не покидая контекст одного приложения. Пример демонстрирует ключевой механизм — `Extern` Game Object. Этот объект позволяет передать управление рендерингом внешней библиотеке, в данном случае Three.js, и синхронно отображать 2D-спрайты Phaser поверх 3D-сцены. Это практичный способ добавить 3D-элементы в меню, карты или специальные эффекты, не переписывая проект с нуля.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.script('threejs', 'js/three145.js');
this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
this.load.image('bg', 'assets/skies/gradient3.png');
}
create ()
{
this.add.image(400, 300, 'bg');
this.init3D();
this.add.image(0, 0, 'logo').setOrigin(0, 0);
}
// This is all mostly ThreeJS code
init3D ()
{
const camera = new THREE.PerspectiveCamera(70, 800 / 600, 1, 10000);
camera.position.z = 300;
const scene = new THREE.Scene();
const texture1 = new THREE.TextureLoader().load('assets/normal-maps/brick.jpg');
const texture2 = new THREE.TextureLoader().load('assets/textures/gold.png');
const texture3 = new THREE.TextureLoader().load('assets/textures/grass.png');
const material1 = new THREE.MeshBasicMaterial({ map: texture1 });
const material2 = new THREE.MeshBasicMaterial({ map: texture2 });
const material3 = new THREE.MeshBasicMaterial({ map: texture3 });
const geometry1 = new THREE.BoxGeometry(100, 100, 100);
const mesh1 = new THREE.Mesh(geometry1, material1);
const geometry2 = new THREE.SphereGeometry(64, 32, 16);
const mesh2 = new THREE.Mesh(geometry2, material2);
const geometry3 = new THREE.CylinderGeometry(35, 35, 80, 32);
const mesh3 = new THREE.Mesh(geometry3, material3);
mesh2.position.x = -200;
mesh3.position.x = 200;
scene.add(mesh1);
scene.add(mesh2);
scene.add(mesh3);
// Tell three to use the Phaser canvas
// Also: Notice we're using the WebGL1 Renderer here
const renderer = new THREE.WebGL1Renderer({
canvas: this.sys.game.canvas,
context: this.sys.game.context,
antialias: true,
});
// Create the Phaser Extern, tells Phaser to hand-off rendering to ThreeJS
const view = this.add.extern();
renderer.setPixelRatio(1);
renderer.setSize(800, 600);
// You can skip this if threeJS is providing the _background_
// and all Phaser objects are on-top of it
renderer.autoClear = false;
// The Extern render function
view.render = (webGLRenderer, drawingContext, calcMatrix) => {
// This is essential to get ThreeJS to reset the GL state
renderer.resetState();
// Ensure the DrawingContext framebuffer is bound.
webGLRenderer.glWrapper.updateBindingsFramebuffer({
bindings: {
framebuffer: drawingContext.framebuffer
}
}, true);
mesh1.rotation.x += 0.005;
mesh1.rotation.y += 0.01;
mesh2.rotation.x -= 0.005;
mesh2.rotation.y += 0.02;
mesh3.rotation.x += 0.03;
mesh3.rotation.y += 0.02;
renderer.render(scene, camera);
// Call it again, after rendering, if you get graphical corruption
// renderer.resetState();
};
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ресурсов
Как и в любом проекте Phaser, работа начинается с подготовки сцены. В методе preload мы загружаем не только стандартные изображения для фона и логотипа, но и скрипт самой библиотеки Three.js.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.script('threejs', 'js/three145.js');
this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
this.load.image('bg', 'assets/skies/gradient3.png');
}
Метод load.script добавляет скрипт Three.js в DOM, делая глобальный объект THREE доступным для использования в нашем коде. После загрузки в create мы добавляем фоновое изображение стандартным для Phaser способом через this.add.image и инициируем создание 3D-сцены.
create ()
{
this.add.image(400, 300, 'bg'); // 2D фон от Phaser
this.init3D(); // Инициализация 3D мира
this.add.image(0, 0, 'logo').setOrigin(0, 0); // 2D спрайт Phaser поверх всего
}
Обратите внимание на порядок: сначала фон, затем 3D-сцена, и только потом — логотип. Это гарантирует, что 2D-логотип будет отрисован поверх 3D-объектов.
Создание 3D-мира с помощью Three.js
Метод init3D содержит чистый код инициализации Three.js. Здесь создаётся камера, сцена, геометрия, материалы и меши (объекты).
const camera = new THREE.PerspectiveCamera(70, 800 / 600, 1, 10000);
camera.position.z = 300;
const scene = new THREE.Scene();
Загружаются текстуры и создаются материалы типа MeshBasicMaterial. Затем определяются три примитивные геометрии: куб (BoxGeometry), сфера (SphereGeometry) и цилиндр (CylinderGeometry). Для каждой геометрии создаётся меш с соответствующим материалом, и они позиционируются в пространстве.
const mesh1 = new THREE.Mesh(geometry1, material1); // Куб
const mesh2 = new THREE.Mesh(geometry2, material2); // Сфера
const mesh3 = new THREE.Mesh(geometry3, material3); // Цилиндр
mesh2.position.x = -200;
mesh3.position.x = 200;
scene.add(mesh1);
scene.add(mesh2);
scene.add(mesh3);
На этом этапе у нас есть полностью функциональная, но изолированная 3D-сцена Three.js. Ключевой шаг — связать её рендерер с канвасом, который контролирует Phaser.
Мост между движками: Рендерер Three.js и Extern
Сердце интеграции — создание рендерера Three.js, который будет использовать существующий WebGL-контекст игры Phaser. Это предотвращает создание второго канваса на странице.
const renderer = new THREE.WebGL1Renderer({
canvas: this.sys.game.canvas,
context: this.sys.game.context,
antialias: true,
});
renderer.setPixelRatio(1);
renderer.setSize(800, 600);
renderer.autoClear = false; // Критически важно!
Параметр autoClear: false указывает Three.js не очищать буферы цвета и глубины перед своим рендером. Если бы он это делал, он стирал бы фон и другие объекты, отрисованные Phaser ранее (например, наш 'bg').
Затем создаётся специальный объект Phaser — Extern.
const view = this.add.extern();
Этот объект служит хуком. Его метод render будет вызываться движком Phaser на каждом кадре в процессе отрисовки. Внутри этого метода мы и выполняем рендеринг нашей сцены Three.js.
Функция рендеринга и синхронизация состояния
Функция view.render — это место, где происходит магия синхронизации. Phaser вызывает её, передавая свои внутренние WebGL-утилиты.
view.render = (webGLRenderer, drawingContext, calcMatrix) => {
renderer.resetState(); // Сброс состояния GL для Three.js
webGLRenderer.glWrapper.updateBindingsFramebuffer({
bindings: {
framebuffer: drawingContext.framebuffer
}
}, true); // Привязка фреймбуфера Phaser
// Анимация вращения объектов
mesh1.rotation.x += 0.005;
mesh1.rotation.y += 0.01;
mesh2.rotation.x -= 0.005;
mesh2.rotation.y += 0.02;
mesh3.rotation.x += 0.03;
mesh3.rotation.y += 0.02;
renderer.render(scene, camera); // Отрисовка сцены Three.js
};
Вызов renderer.resetState() критически важен. Three.js управляет состоянием WebGL (шейдеры, буферы, блендинг) по-своему. Phaser, отрисовывая свои 2D-объекты до вызова этой функции, оставляет контекст в своём состоянии. resetState() сбрасывает его к нейтральному, понятному для Three.js, предотвращая графические артефакты или падения.
Строка с updateBindingsFramebuffer гарантирует, что Three.js будет рисовать в тот же фреймбуфер, который в данный момент активен у Phaser, обеспечивая корректное совмещение 2D и 3D-слоёв. После этого выполняется стандартный рендеринг Three.js renderer.render(scene, camera). Анимация вращения объектов обновляется прямо здесь, на каждом кадре.
Что попробовать дальше
Интеграция Phaser и Three.js через Extern объект — это мощный и элегантный способ комбинировать сильные стороны обоих движков. Вы получаете простоту 2D-игростроения от Phaser и практически безграничные возможности 3D-визуализации от Three.js в одном проекте.
Для экспериментов попробуйте:
1. Добавить физику от Phaser (this.physics.add.sprite) и расположить 2D-спрайты, которые будут "парить" перед вращающимися 3D-фигурами.
2. Использовать THREE.Raycaster для реализации взаимодействия (кликов, наведения) с 3D-объектами и передавать эти события в логику Phaser.
3. Заменить статичный фон Phaser на динамическое небо или частицы, созданные в Three.js, переключив renderer.autoClear на true и удалив фоновую картинку.
