О чем этот пример
Работа с 3D в Phaser часто ассоциируется с готовыми плагинами. Однако понимание основ позволяет создавать кастомные 3D-эффекты, оптимизировать рендеринг и глубже контролировать процесс. В этой статье разберем класс `Obj3D` из официальных примеров Phaser, который реализует программный рендеринг 3D-модели (каркасного куба) с помощью 2D Graphics API. Вы научитесь манипулировать вершинами, применять матрицы поворота и интегрировать такой объект в свою сцену, что откроет путь к созданию уникальной псевдо-3D графики для ваших игр.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Obj3D
{
constructor (camera, modelData, x, y, z)
{
this.camera = camera;
this.model = modelData;
this.x = x;
this.y = y;
this.z = z;
this.thickness = 2;
this.color = 0x00ff00;
this.alpha = 1;
this.scale = 100;
}
rotateX (theta)
{
var ts = Math.sin(theta);
var tc = Math.cos(theta);
var model = this.model;
for (var n = 0; n < model.verts.length; n++)
{
var vert = model.verts[n];
var y = vert.y;
var z = vert.z;
vert.y = y * tc - z * ts;
vert.z = z * tc + y * ts;
}
}
rotateY (theta)
{
var ts = Math.sin(theta);
var tc = Math.cos(theta);
var model = this.model;
for (var n = 0; n < model.verts.length; n++)
{
var vert = model.verts[n];
var x = vert.x;
var z = vert.z;
vert.x = x * tc - z * ts;
vert.z = z * tc + x * ts;
}
}
rotateZ (theta)
{
var ts = Math.sin(theta);
var tc = Math.cos(theta);
var model = this.model;
for (var n = 0; n < model.verts.length; n++)
{
var vert = model.verts[n];
var x = vert.x;
var y = vert.y;
vert.x = x * tc - y * ts;
vert.y = y * tc + x * ts;
}
}
render (graphics)
{
var model = this.model;
var x = this.camera.x + this.x;
var y = this.camera.y + this.y;
var z = this.z;
var scale = this.scale;
graphics.lineStyle(this.thickness, this.color, this.alpha);
graphics.beginPath();
for (var i = 0; i < model.faces.length; i++)
{
var face = model.faces[i];
var v0 = model.verts[face[0] - 1];
var v1 = model.verts[face[1] - 1];
var v2 = model.verts[face[2] - 1];
var v3 = model.verts[face[3] - 1];
// if (v0 && v1 && v2 && v3)
// {
this.drawLine(graphics, x + v0.x * scale, y - v0.y * scale, x + v1.x * scale, y - v1.y * scale);
this.drawLine(graphics, x + v1.x * scale, y - v1.y * scale, x + v2.x * scale, y - v2.y * scale);
this.drawLine(graphics, x + v2.x * scale, y - v2.y * scale, x + v3.x * scale, y - v3.y * scale);
this.drawLine(graphics, x + v3.x * scale, y - v3.y * scale, x + v0.x * scale, y - v0.y * scale);
// }
}
graphics.closePath();
graphics.strokePath();
}
drawLine (graphics, x0, y0, x1, y1)
{
graphics.moveTo(x0, y0);
graphics.lineTo(x1, y1);
}
}
Структура данных модели и инициализация
Класс Obj3D принимает на вход данные модели, позицию в пространстве и ссылку на камеру. Модель — это простой объект JavaScript, содержащий массивы вершин (verts) и граней (faces).
constructor (camera, modelData, x, y, z)
{
this.camera = camera;
this.model = modelData;
this.x = x;
this.y = y;
this.z = z;
this.thickness = 2;
this.color = 0x00ff00;
this.alpha = 1;
this.scale = 100;
}
Здесь `x,y,z— это мировые координаты объекта. Параметрscaleопределяет масштаб отрисовки. Важно отметить, чтоzв данном контексте не используется для перспективного преобразования, а служит для потенциальной логики сортировки. Камера (this.camera`) нужна для корректировки позиции отрисовки на экране, что позволяет объекту двигаться вместе с видом камеры.
Матрицы поворота: как вращать вершины
Класс реализует три базовые функции поворота вокруг осей X, Y и Z. Это классическое применение матриц поворота в упрощенном виде, примененное к каждой вершине модели.
rotateY (theta)
{
var ts = Math.sin(theta);
var tc = Math.cos(theta);
var model = this.model;
for (var n = 0; n < model.verts.length; n++)
{
var vert = model.verts[n];
var x = vert.x;
var z = vert.z;
vert.x = x * tc - z * ts;
vert.z = z * tc + x * ts;
}
}
Метод rotateY вычисляет синус и косинус угла theta. Затем в цикле для каждой вершины пересчитываются её координаты `xиzпо формулам поворота вокруг оси Y. Ключевой момент: модифицируются исходные данные вершины в массивеmodel.verts. Это эффективно, но требует аккуратности, так как изменяет "оригинальную" геометрию. Аналогично работают методыrotateXиrotateZ`.
Процесс рендеринга: от 3D координат к экранным
Сердце класса — метод render(graphics). Его задача взять преобразованные вершины, спроецировать их на 2D плоскость и нарисовать линии, используя API Phaser.GameObjects.Graphics.
render (graphics)
{
var model = this.model;
var x = this.camera.x + this.x;
var y = this.camera.y + this.y;
var z = this.z;
var scale = this.scale;
graphics.lineStyle(this.thickness, this.color, this.alpha);
graphics.beginPath();
for (var i = 0; i < model.faces.length; i++)
{
var face = model.faces[i];
var v0 = model.verts[face[0] - 1];
var v1 = model.verts[face[1] - 1];
var v2 = model.verts[face[2] - 1];
var v3 = model.verts[face[3] - 1];
this.drawLine(graphics, x + v0.x * scale, y - v0.y * scale, x + v1.x * scale, y - v1.y * scale);
// ... остальные линии грани
}
graphics.closePath();
graphics.strokePath();
}
Здесь происходит проекция. Финальные экранные координаты вычисляются как: x + v0.x * scale. Учитывается позиция камеры (this.camera.x), позиция объекта (this.x) и масштабированные координаты вершины. Обратите внимание на y - v0.y * scale: вычитание нужно потому, что в 2D Canvas и Phaser ось Y направлена вниз, в то время как в нашей 3D модели ось Y, вероятно, направлена вверх. Метод drawLine просто использует команды moveTo и lineTo объекта graphics.
Интеграция в сцену Phaser
Чтобы использовать Obj3D, вам нужно создать экземпляр модели, объект этого класса и вызывать его методы в цикле обновления сцены.
// 1. Определяем модель куба (вершины и грани)
const cubeModel = {
verts: [ /* массив вершин типа {x, y, z} */ ],
faces: [ /* массив граней, каждая - массив из 4 индексов вершин */ ]
};
// 2. В create() создаем Graphics объект и экземпляр Obj3D
this.graphics = this.add.graphics();
this.cube = new Obj3D(this.cameras.main, cubeModel, 400, 300, 0);
// 3. В update() вращаем и рисуем
function update(time, delta) {
this.graphics.clear(); // Очищаем предыдущий кадр
this.cube.rotateX(0.01);
this.cube.rotateY(0.01);
this.cube.render(this.graphics);
}
Критически важно очищать Graphics (this.graphics.clear()) каждый кадр, иначе предыдущие отрисованные линии останутся на экране, создавая "шлейф". Камера (this.cameras.main) передается для корректного позиционирования объекта относительно вида.
Что попробовать дальше
Класс Obj3D демонстрирует элегантный и учебный подход к программному рендерингу 3D. Хотя он не использует аппаратное ускорение и перспективу, он дает полный контроль над геометрией. Для экспериментов попробуйте
- Добавить перспективное преобразование, используя координату `z` вершины при расчете экранных координат
- Реализовать сортировку граней по глубине для устранения артефактов
- Добавить загрузку моделей из формата OBJ
- Реализовать буфер вершин, чтобы не модифицировать исходные данные, а хранить преобразованные копии. Это отличная основа для создания кастомных 3D-визуализаций, HUD-элементов или стилизованных эффектов в ваших играх на Phaser
