Использование Ammo.js в сочетании с OffscreenCanvas и Web Worker позволяет вынести тяжелые вычисления физики из основного потока, обеспечивая плавность интерфейса и высокую производительность. Основная схема строится на разделении задач:
Такая архитектура минимизирует блокировку UI и позволяет создавать сложные симуляции с большим количеством тел.
Подключение Ammo.js в Worker осуществляется через
importScripts или через модульную загрузку:
importScripts('ammo.wasm.js');
let physicsWorld;
Ammo().then((AmmoLib) => {
const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
const broadphase = new AmmoLib.btDbvtBroadphase();
const solver = new AmmoLib.btSequentialImpulseConstraintSolver();
physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
dispatcher,
broadphase,
solver,
collisionConfiguration
);
physicsWorld.setGravity(new AmmoLib.btVector3(0, -9.81, 0));
});
Ключевые моменты:
btDefaultCollisionConfiguration отвечает за обработку
коллизий.btSequentialImpulseConstraintSolver обеспечивает
стабильное решение для тел с ограничениями.btDbvtBroadphase ускоряет поиск потенциальных
столкновений через динамическое дерево.Все физические объекты создаются с использованием RigidBody и CollisionShape. Пример для куба и сферы:
function createBox(AmmoLib, position, size, mass) {
const shape = new AmmoLib.btBoxShape(new AmmoLib.btVector3(size.x/2, size.y/2, size.z/2));
const transform = new AmmoLib.btTransform();
transform.setIdentity();
transform.setOrigin(new AmmoLib.btVector3(position.x, position.y, position.z));
const motionState = new AmmoLib.btDefaultMotionState(transform);
const localInertia = new AmmoLib.btVector3(0, 0, 0);
if (mass > 0) shape.calculateLocalInertia(mass, localInertia);
const rbInfo = new AmmoLib.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
const body = new AmmoLib.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);
return body;
}
Особенности:
mass = 0 делает объект статическим.btDefaultMotionState синхронизирует трансформацию между
физикой и визуализацией.localInertia рассчитывается распределение массы для
динамических тел.const canvas = document.getElementById('myCanvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('physicsWorker.js');
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
Внутри Worker:
self.onmess age = (e) => {
if (e.data.type === 'init') {
const canvas = e.data.canvas;
const gl = canvas.getContext('webgl2');
// Здесь будет рендеринг сцены с использованием данных Ammo.js
}
};
Ключевые моменты:
transferControlToOffscreen() позволяет управлять Canvas
в Worker.postMessage требует использования
Transferable Objects.Worker обновляет физику и передает данные объектов для рендеринга:
function stepSimulation(deltaTime) {
physicsWorld.stepSimulation(deltaTime, 10);
const objectData = [];
for (let i = 0; i < rigidBodies.length; i++) {
const obj = rigidBodies[i];
const motionState = obj.getMotionState();
if (motionState) {
const transform = new Ammo.btTransform();
motionState.getWorldTransform(transform);
const origin = transform.getOrigin();
const rotation = transform.getRotation();
objectData.push({
position: { x: origin.x(), y: origin.y(), z: origin.z() },
rotation: { x: rotation.x(), y: rotation.y(), z: rotation.z(), w: rotation.w() }
});
}
}
renderScene(objectData);
}
function renderScene(objects) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Отрисовка объектов с использованием WebGL
objects.forEach(obj => {
// применить obj.position и obj.rotation к мешам
});
requestAnimationFrame(() => stepSimulation(1/60));
}
Особенности:
stepSimulation может принимать maxSubSteps
для повышения стабильности при нестабильном фреймрейте.getMotionState() и btTransform.Основной поток получает только минимальные данные: позиции, ротации или события коллизий. Это уменьшает нагрузку на канал сообщений. Пример:
worker.onmess age = (e) => {
if (e.data.type === 'collision') {
console.log('Столкновение объектов', e.data.info);
}
};
В Worker можно определить обработку столкновений:
function checkCollisions() {
const numManifolds = dispatcher.getNumManifolds();
for (let i = 0; i < numManifolds; i++) {
const contactManifold = dispatcher.getManifoldByIndexInternal(i);
const numContacts = contactManifold.getNumContacts();
if (numContacts > 0) {
const bodyA = Ammo.castObject(contactManifold.getBody0(), Ammo.btRigidBody);
const bodyB = Ammo.castObject(contactManifold.getBody1(), Ammo.btRigidBody);
postMessage({ type: 'collision', info: { a: bodyA.ptr, b: bodyB.ptr } });
}
}
}
Можно создавать сложные сцены с constraint-объектами:
const hinge = new Ammo.btHingeConstraint(
bodyA,
bodyB,
new Ammo.btVector3(0, 1, 0),
new Ammo.btVector3(0, 1, 0),
true
);
physicsWorld.addConstraint(hinge, true);
Особенности HingeConstraint:
setLimit(min, max).Эта схема позволяет строить масштабируемые, отзывчивые физические симуляции в браузере, полностью изолируя тяжелые вычисления и рендеринг от основного потока, с высокой точностью и гибкостью настройки физического мира.