Оптимизация: обновление только активных тел

Библиотека Ammo.js представляет собой порт физического движка Bullet Physics, скомпилированный в WebAssembly или asm.js. Внутренняя модель вычислений основана на пошаговой симуляции мира (btDiscreteDynamicsWorld), где каждое физическое тело (btRigidBody) может находиться в одном из двух состояний:

  • Активное (active)
  • Спящее (sleeping / deactivated)

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


Механизм сна (Sleeping) в Bullet / Ammo.js

Система сна реализована на уровне ядра физического движка. Тело переводится в спящее состояние, если:

  • Линейная и угловая скорость остаются ниже заданного порога
  • На протяжении определённого времени не происходит столкновений или силовых воздействий
  • Не поступают внешние импульсы

Основные параметры сна:

body.setSleepingThresholds(linearThreshold, angularThreshold);
  • linearThreshold — минимальная линейная скорость
  • angularThreshold — минимальная угловая скорость

Если значения скорости остаются ниже порогов, объект засыпает.


Проверка активности тела

Для определения состояния используется метод:

body.isActive();

Возвращает true, если тело активно и участвует в вычислениях текущего шага симуляции.

Пример использования при синхронизации с рендерингом:

if (body.isActive()) {
    const transform = new Ammo.btTransform();
    body.getMotionState().getWorldTransform(transform);

    const origin = transform.getOrigin();
    const rotation = transform.getRotation();

    mesh.position.set(origin.x(), origin.y(), origin.z());
    mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());

    Ammo.destroy(transform);
}

Спящие объекты пропускаются, что уменьшает нагрузку на CPU и сокращает количество операций с WebAssembly-памятью.


Оптимизация цикла симуляции

Стандартный шаг симуляции:

world.stepSimulation(deltaTime, maxSubSteps);

Важные параметры:

  • deltaTime — прошедшее время
  • maxSubSteps — максимальное количество подшагов

Оптимизация достигается:

  • Уменьшением maxSubSteps
  • Ограничением deltaTime
  • Пропуском синхронизации спящих тел

Пример эффективного цикла:

function updatePhysics(deltaTime) {
    world.stepSimulation(deltaTime, 2);

    for (let i = 0; i < rigidBodies.length; i++) {
        const obj = rigidBodies[i];
        const body = obj.physicsBody;

        if (!body.isActive()) continue;

        updateMeshFromBody(obj, body);
    }
}

Групповая фильтрация активных тел

При большом количестве объектов (1000+) линейный перебор может стать узким местом. Улучшение достигается хранением списка активных тел.

Подход:

  1. Подписка на события активации
  2. Ведение массива activeBodies
  3. Удаление тела из массива при засыпании

В Ammo.js прямых событий нет, но можно реализовать проверку изменения состояния:

if (body.isActive() && !body.__wasActive) {
    activeBodies.push(body);
}

if (!body.isActive() && body.__wasActive) {
    removeFromActive(body);
}

body.__wasActive = body.isActive();

Это позволяет обновлять только действительно движущиеся объекты.


Принудительная активация тел

В некоторых сценариях необходимо разбудить тело вручную:

body.activate();

Или при применении силы:

body.applyCentralImpulse(new Ammo.btVector3(0, 10, 0));

При этом объект автоматически переходит в активное состояние.


Отключение деактивации

Для динамических объектов, которые должны обновляться постоянно:

body.setActivationState(4); // DISABLE_DEACTIVATION

Константа 4 соответствует DISABLE_DEACTIVATION.

Использовать осторожно: постоянная активность увеличивает нагрузку.


Влияние сна на производительность

Без оптимизации:

  • Обновление 1000 тел
  • 1000 операций чтения трансформаций
  • 1000 синхронизаций с графикой

С оптимизацией:

  • Обновление 50 активных тел
  • 50 чтений трансформаций
  • Существенное снижение обращений к WebAssembly

Поскольку каждое обращение к getWorldTransform() вызывает переход JS → WASM, экономия становится критичной.


Оптимизация памяти при работе с Transform

Частая ошибка — создание нового btTransform для каждого тела. Это вызывает:

  • Частые аллокации
  • Давление на сборщик мусора
  • Увеличение накладных расходов

Оптимизированный вариант:

const tmpTransform = new Ammo.btTransform();

function updateMeshFromBody(obj, body) {
    body.getMotionState().getWorldTransform(tmpTransform);

    const origin = tmpTransform.getOrigin();
    const rotation = tmpTransform.getRotation();

    obj.mesh.position.set(origin.x(), origin.y(), origin.z());
    obj.mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}

Один общий btTransform используется для всех тел.


Работа со статическими и кинематическими телами

Статические тела

  • Масса = 0
  • Не участвуют в динамике
  • Не обновляются каждый кадр

Кинематические тела

  • Управляются вручную
  • Не реагируют на силы
  • Требуют явной синхронизации

Для них проверка isActive() не всегда достаточна — необходимо учитывать тип тела:

if (body.isStaticObject()) return;
if (body.isKinematicObject()) {
    syncKinematic(body);
    return;
}

Массовые сцены: стратегия масштабирования

При симуляции сотен или тысяч тел применяются дополнительные техники:

1. Spatial partitioning

Использование broadphase алгоритмов (DbvtBroadphase по умолчанию).

2. Разделение логических слоёв

Не все объекты должны находиться в одном физическом мире.

3. Удаление неактивных объектов

Полное удаление из dynamicsWorld, если объект более не участвует в игре.

world.removeRigidBody(body);
Ammo.destroy(body);

Связь с рендерингом (например, Three.js)

При использовании Three.js синхронизация должна происходить только для активных тел.

Типичный паттерн:

for (const obj of rigidBodies) {
    if (!obj.physicsBody.isActive()) continue;
    updateMeshFromBody(obj, obj.physicsBody);
}

Это снижает:

  • количество операций записи в Vector3
  • обновление матриц
  • перерасчёт worldMatrix

Таймстеп и активность

Неправильно подобранный deltaTime может приводить к:

  • частому пробуждению тел
  • нестабильности сна
  • дрожанию объектов

Рекомендуемый фиксированный шаг:

const fixedTimeStep = 1 / 60;
world.stepSimulation(deltaTime, 3, fixedTimeStep);

Фиксированный шаг стабилизирует алгоритм сна.


Распространённые ошибки

Обновление всех тел без проверки активности

Приводит к падению FPS при масштабировании сцены.

Принудительное отключение сна для всех объектов

Уничтожает главное преимущество Bullet — адаптивность.

Частые аллокации Ammo-объектов

Особенно btVector3, btTransform.


Практический шаблон оптимизированного обновления

const tmpTransform = new Ammo.btTransform();

function stepPhysics(deltaTime) {
    world.stepSimulation(deltaTime, 2);

    for (let i = 0; i < rigidBodies.length; i++) {
        const obj = rigidBodies[i];
        const body = obj.physicsBody;

        if (!body.isActive()) continue;

        body.getMotionState().getWorldTransform(tmpTransform);

        const p = tmpTransform.getOrigin();
        const q = tmpTransform.getRotation();

        obj.mesh.position.set(p.x(), p.y(), p.z());
        obj.mesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
    }
}

Ключевые принципы оптимизации

  • Проверять isActive() перед синхронизацией
  • Использовать повторно создаваемые Ammo-объекты
  • Не отключать сон без необходимости
  • Минимизировать переходы JS ↔︎ WASM
  • Удалять неиспользуемые тела из мира

Обновление только активных тел является одной из самых эффективных стратегий масштабирования физической сцены в Ammo.js и критически важно при работе с WebAssembly-версией движка в браузере.