О чем этот пример

Пример демонстрирует, как загрузить 3D-модель из OBJ-файла, наложить на неё текстуру и создать интерактивную сцену с вращением, панорамированием и зумом с помощью мыши и клавиатуры. Это полезно для разработчиков, которые хотят добавить простые, но эффектные 3D-элементы в свою 2D-игру на Phaser, не прибегая к тяжеловесным движкам. Вы научитесь управлять мешем, включать визуальную отладку его вершин и синхронизировать эти операции с камерой.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


var config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

var game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
  this.load.image('powerups', 'assets/obj/powerups.png');
  this.load.obj('skull', 'assets/obj/skull.obj');
}

function create ()
{
    const mesh = this.add.mesh(400, 300, 'powerups');

    mesh.addVerticesFromObj('skull', 0.1);

    mesh.panZ(7);
    mesh.modelRotation.y += 0.5;

    this.debug = this.add.graphics().setScrollFactor(0);

    this.input.keyboard.on('keydown-D', () => {

        if (mesh.debugCallback)
        {
            mesh.setDebug();
        }
        else
        {
            mesh.setDebug(this.debug);
        }

    });

    const rotateRate = 1;
    const panRate = 1;
    const zoomRate = 4;

    this.input.on('pointermove', pointer => {

        if (!pointer.isDown)
        {
            return;
        }

        if (!pointer.event.shiftKey)
        {
            mesh.modelRotation.y += pointer.velocity.x * (rotateRate / 800);
            mesh.modelRotation.x += pointer.velocity.y * (rotateRate / 600);
        }
        else
        {
            mesh.panX(pointer.velocity.x * (panRate / 800));
            mesh.panY(pointer.velocity.y * (panRate / 600));
        }

    });

    this.input.on('wheel', (pointer, over, deltaX, deltaY, deltaZ) => {

        mesh.panZ(deltaY * (zoomRate / 600));

    });

    this.mesh = mesh;

    const cursors = this.input.keyboard.createCursorKeys();

    const controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
        zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
        acceleration: 0.06,
        drag: 0.0005,
        maxSpeed: 1.0
    };

    this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

    this.t = this.add.text(10, 10).setScrollFactor(0);

    window.cam = this.cameras.main;
}

function update (time, delta)
{
    this.controls.update(delta);

    this.debug.clear();
    this.debug.lineStyle(1, 0x00ff00);

    this.t.text = this.mesh.totalRendered;
}

Загрузка данных: текстура и 3D-модель

В методе preload происходит загрузка необходимых ресурсов. Ключевой момент — использование двух разных методов: для текстуры и для геометрии.

this.load.image('powerups', 'assets/obj/powerups.png');
this.load.obj('skull', 'assets/obj/skull.obj');

Метод this.load.image загружает спрайт-лист powerups.png, который будет использован как текстура для меша. Метод this.load.obj загружает 3D-геометрию из файла skull.obj (формат Wavefront OBJ). Важно, что модель и текстура загружаются отдельно, что позволяет комбинировать их произвольным образом.

Создание меша и добавление геометрии

В методе create создаётся основной объект — меш. Меш — это контейнер, который объединяет текстуру и 3D-геометрию.

const mesh = this.add.mesh(400, 300, 'powerups');
mesh.addVerticesFromObj('skull', 0.1);
mesh.panZ(7);
mesh.modelRotation.y += 0.5;

Сначала создаётся меш в центре экрана (400x300) с текстурой 'powerups'. Затем метод mesh.addVerticesFromObj добавляет к этому мешу вершины, загруженные из OBJ-файла 'skull'. Второй аргумент 0.1 — масштаб, который уменьшает модель, чтобы она уместилась в поле зрения. Далее меш отдаляется от камеры с помощью panZ(7) и сразу немного поворачивается по оси Y (modelRotation.y += 0.5), чтобы продемонстрировать его объём.

Интерактивное управление: мышь и клавиши

Код настраивает управление мешем с помощью мыши и переключение режима отладки по клавише.

this.input.on('pointermove', pointer => {
    if (!pointer.isDown) return;
    if (!pointer.event.shiftKey) {
        mesh.modelRotation.y += pointer.velocity.x * (rotateRate / 800);
        mesh.modelRotation.x += pointer.velocity.y * (rotateRate / 600);
    } else {
        mesh.panX(pointer.velocity.x * (panRate / 800));
        mesh.panY(pointer.velocity.y * (panRate / 600));
    }
});

При движении зажатой мыши (без Shift) меш вращается. Скорость вращения зависит от pointer.velocity. Если зажат Shift — меш сдвигается (panX, panY). Колесико мыши управляет приближением (panZ).

this.input.keyboard.on('keydown-D', () => {
    if (mesh.debugCallback) {
        mesh.setDebug();
    } else {
        mesh.setDebug(this.debug);
    }
});

Клавиша `Dвключает или выключает режим отладки. При включении в графический контекстthis.debug` рисуется сетка вершин меша, что помогает визуализировать его структуру.

Управление камерой и отображение статистики

Помимо управления самим мешем, в сцене есть плавное управление камерой с клавиатуры.

const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
    zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
    acceleration: 0.06,
    drag: 0.0005,
    maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

Создаётся экземпляр SmoothedKeyControl, который обеспечивает инерционное движение и зум камеры по стрелкам и клавишам `Q`/`E. Это позволяет независимо исследовать сцену. В методеupdateобновляется состояние этих контролов и отображается количество отрендеренных вершин (mesh.totalRendered`) в текстовом объекте, что полезно для оценки производительности.

Что попробовать дальше

Этот пример показывает гибкий подход к работе с 3D в Phaser: геометрия и текстура загружаются независимо, а меш выступает их универсальным контейнером. Для экспериментов попробуйте: заменить модель skull.obj на свою, используя тот же метод addVerticesFromObj; анимировать modelRotation или положение меша в update для создания автоматического вращения; применить разные текстуры к одной модели, чтобы изменить её внешний вид; или использовать режим отладки (setDebug), чтобы понять структуру вершин загруженной модели.