Оффлодинг симуляции в Web Worker

Библиотека Oimo.js предоставляет мощный, но лёгкий движок физики для симуляций тел с твёрдой и мягкой структурой в браузере. Одним из ключевых аспектов при создании сложных сцен является оптимизация производительности, особенно при большом количестве объектов. Offloading симуляции в Web Worker позволяет переносить тяжёлые вычисления физики в отдельный поток, освобождая главный поток для рендеринга и UI.


Создание мира физики в Oimo.js

Мир физики формируется через объект OIMO.World. Он управляет всеми телами, связями и коллизиями. Основные параметры при создании:

const world = new OIMO.World({
    timestep: 1/60,
    iterations: 8,
    broadphase: 2, // 1 = brute force, 2 = sweep and prune
    worldscale: 1,
    random: true,
    info: true
});

Ключевые моменты:

  • timestep — фиксированный шаг симуляции. Меньшие значения повышают точность, но увеличивают нагрузку.
  • iterations — количество итераций решателя для столкновений. Большее число повышает стабильность, но увеличивает вычислительную стоимость.
  • broadphase — алгоритм предварительного отслеживания потенциальных столкновений. Sweep-and-prune (2) эффективен для большого числа объектов.

Создание тел осуществляется через world.add:

const box = world.add({
    type: 'box',
    size: [1, 1, 1],
    pos: [0, 5, 0],
    move: true
});

Параметры type, size, pos, move определяют форму, размеры, начальное положение и возможность движения тела.


Подготовка Web Worker для симуляции

Web Worker создаётся отдельным файлом:

worker.js:

importScripts('oimo.js');

let world;

self.onmess age = function(e) {
    const { type, data } = e.data;

    if (type === 'init') {
        world = new OIMO.World(data.worldOptions);
        data.bodies.forEach(b => {
            world.add(b);
        });
    } else if (type === 'step') {
        world.step();
        const positions = world.rigidBodies.map(b => ({
            id: b.id,
            position: b.getPosition(),
            rotation: b.getQuaternion()
        }));
        self.postMessage({ type: 'upd ate', data: positions });
    }
};

Особенности:

  • Импорт Oimo.js через importScripts — стандартный способ загрузки библиотек в Worker.
  • Инициализация мира происходит при сообщении с типом init.
  • Шаг симуляции вызывается при step. После каждого шага возвращается минимальный набор данных (position и rotation) для рендера.

Связь главного потока и Web Worker

Главный поток управляет визуализацией через Three.js, Babylon.js или Canvas, а Worker обрабатывает физику. Коммуникация осуществляется через postMessage:

const worker = new Worker('worker.js');

worker.postMessage({
    type: 'init',
    data: {
        worldOptions: { timestep: 1/60, iterations: 8, broadphase: 2 },
        bodies: [
            { type: 'box', size: [1,1,1], pos: [0,5,0], move: true },
            { type: 'sphere', size: [1], pos: [2,10,0], move: true }
        ]
    }
});

function animate() {
    requestAnimationFrame(animate);
    worker.postMessage({ type: 'step' });
}

worker.onmess age = function(e) {
    if (e.data.type === 'upd ate') {
        e.data.data.forEach(obj => {
            const mesh = scene.getObjectById(obj.id);
            mesh.position.se t(...obj.position);
            mesh.quaternion.se t(...obj.rotation);
        });
    }
};

animate();

Примечания:

  • Главный поток не выполняет вычислений физики, только синхронизирует визуализацию.
  • Передача данных минимизируется, чтобы снизить накладные расходы на сериализацию (postMessage копирует объекты, а не передаёт ссылки).

Оптимизация передачи данных

При большом количестве объектов сериализация и передача данных становятся узким местом. Рекомендации:

  1. Использовать Typed Arrays (Float32Array) для передачи координат всех тел за один массив.
  2. Передавать только изменившиеся объекты, если структура сцены статична.
  3. Адаптивный timestep: уменьшать частоту шага Worker при небольшой активности сцены.

Пример передачи данных через Float32Array:

const positions = new Float32Array(world.rigidBodies.length * 7); // x,y,z,qx,qy,qz,qw
world.rigidBodies.forEach((b, i) => {
    positions[i*7 + 0] = b.getPosition().x;
    positions[i*7 + 1] = b.getPosition().y;
    positions[i*7 + 2] = b.getPosition().z;
    const q = b.getQuaternion();
    positions[i*7 + 3] = q.x;
    positions[i*7 + 4] = q.y;
    positions[i*7 + 5] = q.z;
    positions[i*7 + 6] = q.w;
});
self.postMessage({ type: 'update', data: positions }, [positions.buffer]);

Использование transferable objects позволяет передавать массивы без копирования, что значительно снижает нагрузку.


Работа с коллизиями и связями в Worker

Oimo.js поддерживает различные типы соединений (joint), например distance, hinge, ball. Их можно создавать в Worker точно так же, как и тела:

const joint = world.add({
    type: 'joint',
    jointType: 'hinge',
    body1: box1,
    body2: box2,
    pos1: [0,1,0],
    pos2: [0,-1,0],
    axis1: [0,0,1],
    axis2: [0,0,1]
});

Всё управление происходит в Worker, а главный поток получает только финальные трансформации для визуализации.


Интеграция с UI и рендерингом

Чтобы избежать лагов в интерфейсе:

  • Web Worker выполняет шаги физики непрерывно, а главный поток обновляет сцену через requestAnimationFrame.
  • Частота обновления визуализации может быть меньше, чем частота шага симуляции. Например, 60fps рендер и 120fps физика — плавность при высокой точности.
  • Сложные сцены с сотнями объектов требуют разделения на группы тел, чтобы можно было прогонять физику параллельно через несколько Worker.

Применение Offloading в реальных проектах

  • Игры и симуляции с большим количеством динамических объектов.
  • VR/AR приложения, где главный поток отвечает за рендеринг, а физика может быть вычислена отдельно.
  • Научные и образовательные симуляции, требующие точной динамики, но без блокировки UI.

Использование Web Worker с Oimo.js повышает отзывчивость интерфейса и масштабируемость сцен, особенно в браузерных приложениях с ограниченными ресурсами.