Использование Web Workers позволяет выполнять вычисления физики в отдельном потоке, освобождая основной поток для рендеринга и пользовательского интерфейса. В контексте Ammo.js, который является портом библиотеки Bullet Physics на JavaScript через Emscripten, это критически важно для поддержания высокой частоты кадров в сложных сценах.
Главная идея заключается в том, что вся симуляция физики происходит
внутри воркера, а основной поток занимается отображением. Коммуникация
между потоками осуществляется через postMessage и
onmessage, что требует сериализации данных, передаваемых
между потоками.
Для запуска Ammo.js в воркере создается отдельный скрипт, например,
physicsWorker.js. Внутри него выполняются следующие
шаги:
importScripts('ammo.js');
let physicsWorld;
let rigidBodies = [];
let tmpTransform = new Ammo.btTransform();
self.onmess age = function(e) {
const data = e.data;
switch (data.cmd) {
case 'init':
initPhysics();
break;
case 'addRigidBody':
addRigidBody(data.body);
break;
case 'stepSimulation':
stepSimulation(data.deltaTime);
break;
}
};
function initPhysics() {
const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
const broadphase = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();
physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0));
}
Ключевые моменты:
Ammo.btDiscreteDynamicsWorld отвечает за саму
физическую симуляцию.rigidBodies.stepSimulation.Rigid body описываются через массу, форму и начальное положение. Пример функции добавления тела:
function addRigidBody(bodyData) {
const shape = new Ammo.btBoxShape(new Ammo.btVector3(bodyData.size.x/2, bodyData.size.y/2, bodyData.size.z/2));
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(bodyData.position.x, bodyData.position.y, bodyData.position.z));
const motionState = new Ammo.btDefaultMotionState(transform);
const localInertia = new Ammo.btVector3(0, 0, 0);
if (bodyData.mass > 0) shape.calculateLocalInertia(bodyData.mass, localInertia);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(bodyData.mass, motionState, shape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);
rigidBodies.push(body);
}
Особенности:
calculateLocalInertia) обязателен для
подвижных объектов.btTransform задает начальное положение и ориентацию
тела.Для поддержания синхронизации с визуализацией выполняется шаг симуляции с фиксированным шагом времени. После шага обновляются позиции и ориентации объектов:
function stepSimulation(deltaTime) {
physicsWorld.stepSimulation(deltaTime, 10);
const bodiesData = rigidBodies.map(body => {
const motionState = body.getMotionState();
if (motionState) {
motionState.getWorldTransform(tmpTransform);
const origin = tmpTransform.getOrigin();
const rotation = tmpTransform.getRotation();
return {
position: {x: origin.x(), y: origin.y(), z: origin.z()},
rotation: {x: rotation.x(), y: rotation.y(), z: rotation.z(), w: rotation.w()}
};
}
return null;
});
self.postMessage({cmd: 'upd ate', bodies: bodiesData});
}
Особенности передачи данных:
postMessage автоматически копирует объекты. Для больших
массивов можно использовать Transferable Objects для
оптимизации.В основном потоке создается сцена (например, через Three.js) и подписывается на обновления от воркера:
const worker = new Worker('physicsWorker.js');
worker.postMessage({cmd: 'init'});
worker.onmess age = function(e) {
const data = e.data;
if (data.cmd === 'upd ate') {
data.bodies.forEach((b, i) => {
meshArray[i].position.se t(b.position.x, b.position.y, b.position.z);
meshArray[i].quaternion.se t(b.rotation.x, b.rotation.y, b.rotation.z, b.rotation.w);
});
}
};
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
worker.postMessage({cmd: 'stepSimulation', deltaTime: deltaTime});
renderer.render(scene, camera);
}
animate();
Особенности:
requestAnimationFrame синхронизирует визуализацию с
частотой обновления экрана.Фиксированный vs адаптивный шаг: Для
стабильности лучше использовать фиксированный шаг (например, 1/60
секунды) и несколько итераций (maxSubSteps) при
необходимости.
Сериализация объектов: Избегать передачи сложных структур; использовать компактные объекты с числами для позиции и ориентации.
Память и очистка: Ammo.js требует явного
освобождения ресурсов (Ammo.destroy) для форм и тел, чтобы
избежать утечек памяти в долгоживущих сценах.
Transferable Objects: Для массивов с большим
количеством тел возможно использование Float32Array и
передачи через transfer, что минимизирует копирование и ускоряет обмен
данными.
Коллизии и события: Можно отдельно передавать события столкновений воркер → основной поток, но для сложных сцен лучше фильтровать только необходимые столкновения.
Применение Web Workers с Ammo.js позволяет строить высокопроизводительные физические симуляции прямо в браузере, отделяя вычисления от рендеринга и обеспечивая плавный пользовательский опыт в динамичных сценах.