Портирование проектов с Ammo.js на Oimo.js

Портирование проекта с Ammo.js на Oimo.js требует понимания фундаментальных различий между движками. Ammo.js представляет собой Emscripten-порт C++-движка Bullet Physics и повторяет его архитектуру: указатели, ручное управление памятью, сложная иерархия классов, низкоуровневые структуры данных.

Oimo.js — нативная JavaScript-реализация физического движка, ориентированная на простоту API, меньший объём кода и более удобную интеграцию с WebGL-сценами.

Ключевые отличия

Характеристика Ammo.js Oimo.js
Происхождение Порт Bullet Нативный JS
Управление памятью Ручное, через Ammo.destroy Автоматическое (GC)
API Низкоуровневое Более компактное
Производительность Высокая при сложной физике Оптимизировано для веб-сцен
Настройка Гибкая, но сложная Быстрая конфигурация

Переход требует изменения подхода к созданию мира, тел, форм и шагу симуляции.


Создание физического мира

Ammo.js

В Ammo.js инициализация мира включает несколько обязательных компонентов:

const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
const broadphase = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();

const physicsWorld = new Ammo.btDiscreteDynamicsWorld(
  dispatcher,
  broadphase,
  solver,
  collisionConfiguration
);

physicsWorld.setGravity(new Ammo.btVector3(0, -9.8, 0));

Oimo.js

В Oimo.js инициализация значительно проще:

const world = new OIMO.World({
  gravity: [0, -9.8, 0]
});

Отличие: отсутствуют отдельные конфигурации диспетчера, broadphase и solver. В Oimo эти механизмы скрыты внутри World.


Создание тел: сопоставление API

Статические и динамические тела

Ammo.js

const shape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));

const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(0, 5, 0));

const motionState = new Ammo.btDefaultMotionState(transform);

const mass = 1;
const localInertia = new Ammo.btVector3(0, 0, 0);
shape.calculateLocalInertia(mass, localInertia);

const rbInfo = new Ammo.btRigidBodyConstructionInfo(
  mass,
  motionState,
  shape,
  localInertia
);

const body = new Ammo.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);

Oimo.js

const body = world.add({
  type: 'box',
  size: [2, 2, 2],
  pos: [0, 5, 0],
  move: true,
  density: 1
});

Основные различия

  • В Ammo требуется ручной расчёт инерции.
  • В Oimo расчёт массы выполняется автоматически на основе плотности.
  • В Ammo используется btRigidBodyConstructionInfo.
  • В Oimo всё передаётся через конфигурационный объект.

Формы столкновений

Сопоставление типов

Ammo.js Oimo.js
btBoxShape type: 'box'
btSphereShape type: 'sphere'
btCylinderShape type: 'cylinder'
btConvexHullShape type: 'convex'

Пример сферы

Ammo.js:

const shape = new Ammo.btSphereShape(1);

Oimo.js:

world.add({
  type: 'sphere',
  size: [2],
  pos: [0, 10, 0],
  move: true
});

Размеры в Oimo задаются диаметром, тогда как в Ammo радиус передаётся напрямую.


Обновление симуляции

Ammo.js

physicsWorld.stepSimulation(deltaTime, 10);

Oimo.js

world.step();

Oimo обычно использует фиксированный шаг времени (по умолчанию 1/60). Для изменения:

const world = new OIMO.World({
  timestep: 1/120,
  gravity: [0, -9.8, 0]
});

Получение позиции и вращения

Ammo.js

const transform = new Ammo.btTransform();
body.getMotionState().getWorldTransform(transform);

const origin = transform.getOrigin();
const rotation = transform.getRotation();

Oimo.js

const position = body.getPosition();
const quaternion = body.getQuaternion();

В Oimo отсутствует необходимость работать с временными объектами btTransform.


Управление памятью

Ammo.js

Критически важно уничтожать объекты:

Ammo.destroy(rbInfo);
Ammo.destroy(motionState);
Ammo.destroy(transform);

Отсутствие освобождения памяти приводит к утечкам, поскольку объекты находятся в WebAssembly-памяти.

Oimo.js

Используется сборщик мусора JavaScript. Явное уничтожение требуется только при удалении тела:

world.remove(body);

Ограничения и шарниры

В Ammo.js доступны практически все типы шарниров Bullet:

  • btHingeConstraint
  • btSliderConstraint
  • btPoint2PointConstraint

Oimo поддерживает базовые типы соединений через конфигурацию:

world.add({
  type: 'jointHinge',
  body1: bodyA,
  body2: bodyB,
  pos1: [0, 0, 0],
  pos2: [0, 0, 0],
  axe1: [0, 1, 0],
  axe2: [0, 1, 0]
});

API отличается концептуально: в Ammo создаётся отдельный объект ограничения, в Oimo — добавляется конфигурационный объект через world.add.


Работа с Three.js

Часто портирование выполняется в проектах на базе Three.js.

Ammo.js синхронизация

mesh.position.set(origin.x(), origin.y(), origin.z());
mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());

Oimo.js синхронизация

mesh.position.copy(body.getPosition());
mesh.quaternion.copy(body.getQuaternion());

Oimo возвращает объекты, совместимые с THREE.Vector3 и THREE.Quaternion, что упрощает интеграцию.


Производительность и стабильность

Ammo.js лучше справляется с:

  • сложными стековыми структурами,
  • мягкими телами,
  • точными ограничениями.

Oimo.js демонстрирует преимущества:

  • быстрый старт проекта,
  • меньший размер библиотеки,
  • более простая сериализация сцен.

При портировании крупных сцен рекомендуется:

  1. Пересчитать плотности вместо явной массы.
  2. Проверить параметры трения и реституции.
  3. Настроить шаг симуляции.
  4. Проверить стабильность стека тел.

Перенос параметров материала

Ammo.js

body.setFriction(0.5);
body.setRestitution(0.2);

Oimo.js

world.add({
  type: 'box',
  size: [2,2,2],
  friction: 0.5,
  restitution: 0.2
});

Параметры задаются при создании тела.


Типичные проблемы при портировании

1. Некорректная масса

В Ammo используется явное значение mass. В Oimo масса рассчитывается из density и объёма.

Решение: вычислить плотность:

density = mass / volume

2. Различия в ориентации осей

Bullet и Oimo используют правую систему координат, но параметры цилиндров и шарниров могут отличаться по оси по умолчанию.


3. Различия в масштабировании

Ammo.js чувствителен к слишком маленьким или большим масштабам (оптимальный диапазон 0.1–10). Oimo более устойчив, но также требует разумного масштаба сцены.


Пошаговая стратегия миграции

  1. Удаление Ammo-инициализации и создание OIMO.World.
  2. Переписывание всех btRigidBody через world.add.
  3. Пересчёт плотностей.
  4. Перенос ограничений.
  5. Настройка шага симуляции.
  6. Проверка стабильности стеков и коллизий.
  7. Оптимизация количества тел.

Минимальный пример полной замены

Было (Ammo.js)

physicsWorld.stepSimulation(deltaTime);

body.getMotionState().getWorldTransform(tmpTrans);
mesh.position.set(
  tmpTrans.getOrigin().x(),
  tmpTrans.getOrigin().y(),
  tmpTrans.getOrigin().z()
);

Стало (Oimo.js)

world.step();

mesh.position.copy(body.getPosition());
mesh.quaternion.copy(body.getQuaternion());

Когда переход оправдан

Портирование целесообразно при:

  • использовании только твёрдых тел,
  • отсутствии сложной физики транспорта,
  • необходимости уменьшить размер сборки,
  • упрощении поддержки кода.

При проектах, завязанных на расширенные возможности Bullet, сохранение Ammo.js остаётся более рациональным решением.

Oimo.js обеспечивает более компактный и читаемый код, снижает порог входа и упрощает сопровождение веб-проектов с физикой.