О чем этот пример
Хотите добавить в свою 2D-игру на Phaser эффектную трёхмерную графику без тяжёлых движков? Этот пример демонстрирует, как можно программно парсить модели из формата OBJ, управлять ими в 3D-пространстве и отрисовывать с помощью встроенного графического контекста. Вы научитесь загружать геометрические данные, создавать простейший 3D-конвейер для вращения и анимации объектов, что откроет двери для создания визуально сложных элементов интерфейса, вращающихся логотипов или стилизованных 3D-предметов в вашем проекте.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class DemoD extends Phaser.Scene
{
constructor ()
{
super({ key: 'DemoD', active: true });
this.graphics;
this.t = {
x: -0.03490658503988659,
y: 0.03490658503988659,
z: -0.03490658503988659
};
this.modelData = {};
this.objects = [];
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.text('bevelledcube', 'assets/text/bevelledcube.obj');
this.load.text('computer', 'assets/text/computer.obj');
this.load.text('geosphere', 'assets/text/geosphere.obj');
this.load.text('spike', 'assets/text/spike.obj');
this.load.text('torus', 'assets/text/torus.obj');
}
create ()
{
this.parseObj('bevelledcube');
this.parseObj('computer');
this.parseObj('geosphere');
this.parseObj('spike');
this.parseObj('torus');
this.graphics = this.add.graphics();
this.camera = {
x: 400,
y: 340
};
var b = this.addObject('bevelledcube', -200, -200, 0);
var g = this.addObject('geosphere', 200, -200, 0);
g.color = 0x00ffff;
var t = this.addObject('torus', -200, 100, 0);
t.color = 0xff00ff;
t.scale = 200;
var c = this.addObject('computer', 200, 100, 0);
c.color = 0xffff00;
this.tweens.add({
targets: t,
duration: 2000,
scale: 10,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true
});
this.tweens.add({
targets: c,
duration: 4000,
scale: 10,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true
});
var cam = this.cameras.main;
cam.x = 800;
cam.y = 600;
}
update ()
{
this.graphics.clear();
for (var i = 0; i < this.objects.length; i++)
{
this.objects[i].rotateX(0.01);
this.objects[i].rotateY(0.03);
this.objects[i].rotateZ(0.01);
this.objects[i].render(this.graphics);
}
}
addObject (key, x, y, z)
{
var model = new Obj3D(this.camera, this.getModel(key), x, y, z);
this.objects.push(model);
return model;
}
getModel (key)
{
var data = Phaser.Utils.Objects.Extend(true, this.modelData[key], {});
return data;
}
// Parses out tris and quads from the obj file
parseObj (key)
{
var text = this.cache.text.get(key);
var verts = [];
var faces = [];
// split the text into lines
var lines = text.replace('\r', '').split('\n');
var count = lines.length;
for (var i = 0; i < count; i++)
{
var line = lines[i];
if (line[0] === 'v')
{
// lines that start with 'v' are vertices
var tokens = line.split(' ');
verts.push({
x: parseFloat(tokens[1]),
y: parseFloat(tokens[2]),
z: parseFloat(tokens[3])
});
}
else if (line[0] === 'f')
{
// lines that start with 'f' are faces
var tokens = line.split(' ');
var face = [
parseInt(tokens[1], 10),
parseInt(tokens[2], 10),
parseInt(tokens[3], 10),
parseInt(tokens[4], 10)
];
faces.push(face);
if (face[0] < 0)
{
face[0] = verts.length + face[0];
}
if (face[1] < 0)
{
face[1] = verts.length + face[1];
}
if (face[2] < 0)
{
face[2] = verts.length + face[2];
}
if (!face[3])
{
face[3] = face[2];
}
else if (face[3] < 0)
{
face[3] = verts.length + face[3];
}
}
}
this.modelData[key] = {
verts: verts,
faces: faces
};
return this.modelData[key];
}
}
Загрузка и парсинг 3D-моделей
Пример начинает работу с загрузки текстовых файлов с расширением .obj. Это простой формат для хранения 3D-геометрии, где каждая строка определяет вершину (`v) или грань (f`).
В методе preload мы загружаем несколько файлов как обычный текст, используя this.load.text.
this.load.text('bevelledcube', 'assets/text/bevelledcube.obj');
После загрузки в методе create для каждого ключа вызывается parseObj. Этот метод читает текст из кэша, разбивает его на строки и анализирует.
var text = this.cache.text.get(key);
var lines = text.replace('\r', '').split('\n');
Для строк, начинающихся с 'v', извлекаются координаты `x,y,zи сохраняются в массивverts. Для строк с'f'извлекаются индексы вершин, образующих грань (квадрат или треугольник), и сохраняются в массивfaces. Полученные данные сохраняются в объектеthis.modelData` по ключу загрузки для последующего использования.
Создание и управление 3D-объектами
Для работы с распарсенной геометрией в примере используется класс Obj3D (его код в примере не показан, но логику можно понять по контексту). Он, вероятно, хранит вершины, грани, позицию, масштаб, цвет и методы для трансформаций.
Метод addObject создаёт новый экземпляр Obj3D, передавая ему позицию камеры, данные модели и координаты размещения в 3D-пространстве сцены.
var model = new Obj3D(this.camera, this.getModel(key), x, y, z);
this.objects.push(model);
В create создаётся несколько объектов: куб, геосфера, тор и модель компьютера. Каждому можно задать индивидуальный цвет и начальный масштаб. Объекты добавляются в массив this.objects для централизованного обновления.
var t = this.addObject('torus', -200, 100, 0);
t.color = 0xff00ff;
t.scale = 200;
Анимация и твининг
Phaser предоставляет мощную систему твининга для плавной анимации свойств. В примере она используется для пульсирующего изменения масштаба (scale) объектов тора и компьютера.
this.tweens.add({
targets: t,
duration: 2000,
scale: 10,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true
});
Здесь targets — это объект `t(тор). Его свойствоscaleбудет изменяться от исходного значения до 10 за 2000 миллисекунд с плавностьюSine.easeInOut. Параметрыrepeat: -1иyoyo: true` заставляют анимацию повторяться бесконечно, чередуя движение вперёд и назад, создавая эффект "дыхания". Аналогичный твин, но с большей длительностью, применяется к модели компьютера.
Непрерывное вращение и отрисовка
Основная логика обновления и отрисовки находится в методе update, который вызывается каждый кадр.
Сначала графический контекст this.graphics очищается, чтобы удалить изображение предыдущего кадра.
this.graphics.clear();
Затем в цикле для каждого объекта в массиве this.objects вызываются методы поворота вокруг осей rotateX, rotateY, rotateZ. Эти методы, принадлежащие классу Obj3D, изменяют внутреннюю матрицу или углы вращения объекта.
this.objects[i].rotateX(0.01);
this.objects[i].rotateY(0.03);
this.objects[i].rotateZ(0.01);
После применения вращения вызывается метод render, который, используя текущее состояние объекта (позицию, поворот, масштаб, цвет) и данные о вершинах/гранях, выполняет проекцию 3D-координат на 2D-экран и отрисовывает линии или полигоны через this.graphics. Таким образом, мы видим постоянно вращающиеся 3D-модели.
Работа с камерой
Интересный момент в коде — это смещение основной камеры сцены. Хотя вся отрисовка происходит программно через Graphics в координатах мира, камера всё равно влияет на итоговую позицию отрисовки.
var cam = this.cameras.main;
cam.x = 800;
cam.y = 600;
Это смещает область просмотра (viewport) камеры. В данном контексте это может использоваться для того, чтобы разместить область отрисовки 3D-объектов в определённой части экрана, в то время как другие элементы интерфейса или игрового мира отрисовываются в стандартных координатах. Также в коде определён объект this.camera, который, судя по передаче в конструктор Obj3D, используется для математики проекции (например, для учёта смещения точки обзора).
Что попробовать дальше
Этот пример — отличная отправная точка для интеграции простой, но эффективной программной 3D-графики в ваши проекты на Phaser. Вы можете экспериментировать: загружать собственные OBJ-модели, изменять логику освещения и заливки в методе render, комбинировать 3D-объекты со спрайтами и частицами, а также управлять камерой для более сложных сцен. Попробуйте добавить интерактивность — вращение объектов по клику или их перемещение по сцене.
