Библиотека Ammo.js представляет собой порт физического движка Bullet
Physics, скомпилированный в WebAssembly или asm.js. Внутренняя модель
вычислений основана на пошаговой симуляции мира
(btDiscreteDynamicsWorld), где каждое физическое тело
(btRigidBody) может находиться в одном из двух
состояний:
Ключ к оптимизации — обновление только активных тел и исключение спящих объектов из лишних вычислений и синхронизаций с графическим движком.
Система сна реализована на уровне ядра физического движка. Тело переводится в спящее состояние, если:
body.setSleepingThresholds(linearThreshold, angularThreshold);
linearThreshold — минимальная линейная скоростьangularThreshold — минимальная угловая скоростьЕсли значения скорости остаются ниже порогов, объект засыпает.
Для определения состояния используется метод:
body.isActive();
Возвращает true, если тело активно и участвует в
вычислениях текущего шага симуляции.
Пример использования при синхронизации с рендерингом:
if (body.isActive()) {
const transform = new Ammo.btTransform();
body.getMotionState().getWorldTransform(transform);
const origin = transform.getOrigin();
const rotation = transform.getRotation();
mesh.position.set(origin.x(), origin.y(), origin.z());
mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
Ammo.destroy(transform);
}
Спящие объекты пропускаются, что уменьшает нагрузку на CPU и сокращает количество операций с WebAssembly-памятью.
Стандартный шаг симуляции:
world.stepSimulation(deltaTime, maxSubSteps);
deltaTime — прошедшее времяmaxSubSteps — максимальное количество подшаговОптимизация достигается:
maxSubStepsdeltaTimeПример эффективного цикла:
function updatePhysics(deltaTime) {
world.stepSimulation(deltaTime, 2);
for (let i = 0; i < rigidBodies.length; i++) {
const obj = rigidBodies[i];
const body = obj.physicsBody;
if (!body.isActive()) continue;
updateMeshFromBody(obj, body);
}
}
При большом количестве объектов (1000+) линейный перебор может стать узким местом. Улучшение достигается хранением списка активных тел.
activeBodiesВ Ammo.js прямых событий нет, но можно реализовать проверку изменения состояния:
if (body.isActive() && !body.__wasActive) {
activeBodies.push(body);
}
if (!body.isActive() && body.__wasActive) {
removeFromActive(body);
}
body.__wasActive = body.isActive();
Это позволяет обновлять только действительно движущиеся объекты.
В некоторых сценариях необходимо разбудить тело вручную:
body.activate();
Или при применении силы:
body.applyCentralImpulse(new Ammo.btVector3(0, 10, 0));
При этом объект автоматически переходит в активное состояние.
Для динамических объектов, которые должны обновляться постоянно:
body.setActivationState(4); // DISABLE_DEACTIVATION
Константа 4 соответствует DISABLE_DEACTIVATION.
Использовать осторожно: постоянная активность увеличивает нагрузку.
Поскольку каждое обращение к getWorldTransform()
вызывает переход JS → WASM, экономия становится критичной.
Частая ошибка — создание нового btTransform для каждого
тела. Это вызывает:
Оптимизированный вариант:
const tmpTransform = new Ammo.btTransform();
function updateMeshFromBody(obj, body) {
body.getMotionState().getWorldTransform(tmpTransform);
const origin = tmpTransform.getOrigin();
const rotation = tmpTransform.getRotation();
obj.mesh.position.set(origin.x(), origin.y(), origin.z());
obj.mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
Один общий btTransform используется для всех тел.
Для них проверка isActive() не всегда достаточна —
необходимо учитывать тип тела:
if (body.isStaticObject()) return;
if (body.isKinematicObject()) {
syncKinematic(body);
return;
}
При симуляции сотен или тысяч тел применяются дополнительные техники:
Использование broadphase алгоритмов (DbvtBroadphase по умолчанию).
Не все объекты должны находиться в одном физическом мире.
Полное удаление из dynamicsWorld, если объект более не
участвует в игре.
world.removeRigidBody(body);
Ammo.destroy(body);
При использовании Three.js синхронизация должна происходить только для активных тел.
Типичный паттерн:
for (const obj of rigidBodies) {
if (!obj.physicsBody.isActive()) continue;
updateMeshFromBody(obj, obj.physicsBody);
}
Это снижает:
Vector3Неправильно подобранный deltaTime может приводить к:
Рекомендуемый фиксированный шаг:
const fixedTimeStep = 1 / 60;
world.stepSimulation(deltaTime, 3, fixedTimeStep);
Фиксированный шаг стабилизирует алгоритм сна.
Приводит к падению FPS при масштабировании сцены.
Уничтожает главное преимущество Bullet — адаптивность.
Особенно btVector3, btTransform.
const tmpTransform = new Ammo.btTransform();
function stepPhysics(deltaTime) {
world.stepSimulation(deltaTime, 2);
for (let i = 0; i < rigidBodies.length; i++) {
const obj = rigidBodies[i];
const body = obj.physicsBody;
if (!body.isActive()) continue;
body.getMotionState().getWorldTransform(tmpTransform);
const p = tmpTransform.getOrigin();
const q = tmpTransform.getRotation();
obj.mesh.position.set(p.x(), p.y(), p.z());
obj.mesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
}
}
isActive() перед синхронизациейОбновление только активных тел является одной из самых эффективных стратегий масштабирования физической сцены в Ammo.js и критически важно при работе с WebAssembly-версией движка в браузере.