Web Workers: физика в отдельном потоке

Использование Web Workers позволяет выполнять вычисления физики в отдельном потоке, освобождая основной поток для рендеринга и пользовательского интерфейса. В контексте Ammo.js, который является портом библиотеки Bullet Physics на JavaScript через Emscripten, это критически важно для поддержания высокой частоты кадров в сложных сценах.

Главная идея заключается в том, что вся симуляция физики происходит внутри воркера, а основной поток занимается отображением. Коммуникация между потоками осуществляется через postMessage и onmessage, что требует сериализации данных, передаваемых между потоками.


Создание Web Worker для физики

Для запуска 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 синхронизирует визуализацию с частотой обновления экрана.
  • Передача deltaTime воркеру позволяет использовать адаптивный шаг симуляции.

Оптимизации и особенности работы

  1. Фиксированный vs адаптивный шаг: Для стабильности лучше использовать фиксированный шаг (например, 1/60 секунды) и несколько итераций (maxSubSteps) при необходимости.

  2. Сериализация объектов: Избегать передачи сложных структур; использовать компактные объекты с числами для позиции и ориентации.

  3. Память и очистка: Ammo.js требует явного освобождения ресурсов (Ammo.destroy) для форм и тел, чтобы избежать утечек памяти в долгоживущих сценах.

  4. Transferable Objects: Для массивов с большим количеством тел возможно использование Float32Array и передачи через transfer, что минимизирует копирование и ускоряет обмен данными.

  5. Коллизии и события: Можно отдельно передавать события столкновений воркер → основной поток, но для сложных сцен лучше фильтровать только необходимые столкновения.


Расширенные сценарии

  • Многопоточность: можно создавать несколько воркеров для отдельных областей сцены, чтобы увеличить производительность.
  • Совмещение с WebGL: позиции тел напрямую обновляют объекты Three.js или Babylon.js.
  • Сложные меши: Ammo.js поддерживает конвексные и триангулярные меши, их можно добавлять через btConvexHullShape и btBvhTriangleMeshShape, но они требуют больше ресурсов.

Применение Web Workers с Ammo.js позволяет строить высокопроизводительные физические симуляции прямо в браузере, отделяя вычисления от рендеринга и обеспечивая плавный пользовательский опыт в динамичных сценах.