Ammo.js + OffscreenCanvas + Worker: полная схема

Использование Ammo.js в сочетании с OffscreenCanvas и Web Worker позволяет вынести тяжелые вычисления физики из основного потока, обеспечивая плавность интерфейса и высокую производительность. Основная схема строится на разделении задач:

  • Worker: вычисление физики, обработка коллизий, обновление позиций объектов.
  • OffscreenCanvas: рендеринг сцены в отдельном потоке.
  • Main thread: управление интерфейсом, передача данных между Worker и OffscreenCanvas.

Такая архитектура минимизирует блокировку UI и позволяет создавать сложные симуляции с большим количеством тел.


Инициализация Ammo.js в Worker

Подключение 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 рассчитывается распределение массы для динамических тел.

Организация Worker и OffscreenCanvas

Создание Worker и передачи OffscreenCanvas

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.
  • Передача Canvas через postMessage требует использования Transferable Objects.

Симуляция и рендеринг в Worker

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 } });
        }
    }
}

Оптимизация и масштабирование

  1. Использование динамических и статических тел: статические объекты не требуют вычислений, что экономит ресурсы.
  2. Поддержка нескольких Worker: тяжелые симуляции можно разделить на несколько потоков для разных областей сцены.
  3. Использование TypedArray для передачи данных между потоками, снижая накладные расходы на сериализацию.
  4. Step Simulation с фиксированным временем: предотвращает нестабильность при изменении FPS.
  5. Кэширование буферов WebGL: повторное использование вершинных данных уменьшает количество операций GPU.

Продвинутая интеграция

Можно создавать сложные сцены с 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).
  • Работает в Worker без блокировки UI.

Эта схема позволяет строить масштабируемые, отзывчивые физические симуляции в браузере, полностью изолируя тяжелые вычисления и рендеринг от основного потока, с высокой точностью и гибкостью настройки физического мира.