Минимизация вызовов Ammo.destroy()

Ammo.js — это JavaScript-порт физического движка Bullet Physics. Он использует WebAssembly, что позволяет создавать высокопроизводительные симуляции. Одним из ключевых аспектов работы с этим движком является управление памятью объектов, созданных через API. В отличие от обычных объектов JavaScript, объекты Ammo.js не очищаются автоматически сборщиком мусора, поэтому требуется явный вызов Ammo.destroy() для освобождения памяти.

Каждый вызов Ammo.destroy() имеет накладные расходы, особенно если уничтожается большое количество объектов за кадр. Избыточные вызовы могут значительно замедлить производительность симуляции. Минимизация этих вызовов позволяет ускорить рендеринг и уменьшить нагрузку на основной поток.

Использование пулов объектов

Наиболее эффективная стратегия — создание пулов объектов. Пул объектов представляет собой заранее выделенный набор экземпляров для повторного использования. Вместо создания и уничтожения нового объекта при каждом кадре, используется уже существующий объект из пула.

Пример пула для btVector3:

const vectorPool = [];
function getVector(x = 0, y = 0, z = 0) {
    let vec;
    if (vectorPool.length) {
        vec = vectorPool.pop();
        vec.setValue(x, y, z);
    } else {
        vec = new Ammo.btVector3(x, y, z);
    }
    return vec;
}

function releaseVector(vec) {
    vectorPool.push(vec);
}

Ключевой момент: объект не уничтожается, а возвращается в пул. Это позволяет многократно использовать одну и ту же память.

Минимизация временных объектов

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

  • Использовать один временный объект на кадр, который перезаписывается при каждой операции.
  • Избегать создания новых объектов внутри циклов с высокой частотой (например, для каждого столкновения).

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

const tempVec = new Ammo.btVector3(0, 0, 0);

function applyImpulse(body, impulse, relPos) {
    tempVec.setValue(impulse.x, impulse.y, impulse.z);
    body.applyImpulse(tempVec, relPos);
}

После завершения симуляции можно уничтожить tempVec один раз, а не при каждом вызове applyImpulse.

Упорядочивание освобождения ресурсов

Для больших симуляций освобождение памяти должно быть структурированным. Рекомендуется:

  1. Создавать контейнеры для всех объектов одного типа (bodies, shapes, constraints).
  2. В конце симуляции или при смене сцены проходить по контейнерам и вызывать Ammo.destroy() для каждого объекта.
  3. После вызова Ammo.destroy() очищать ссылки на объекты, чтобы избежать “висячих” ссылок.

Пример организации:

const rigidBodies = [];
const collisionShapes = [];

// Создание объектов
const boxShape = new Ammo.btBoxShape(new Ammo.btVector3(1,1,1));
collisionShapes.push(boxShape);

const body = createRigidBody(boxShape, mass, position);
rigidBodies.push(body);

// Очистка
function cleanupPhysics() {
    for (const body of rigidBodies) {
        physicsWorld.removeRigidBody(body);
        Ammo.destroy(body);
    }
    rigidBodies.length = 0;

    for (const shape of collisionShapes) {
        Ammo.destroy(shape);
    }
    collisionShapes.length = 0;
}

Такой подход гарантирует, что вызовы Ammo.destroy() минимизированы и централизованы, что снижает риск утечек памяти.

Работа с btTransform и матрицами

btTransform и аналогичные структуры часто создаются для позиционирования объектов. Чтобы минимизировать вызовы Ammo.destroy():

  • Создать один глобальный экземпляр btTransform, который используется для всех операций на кадр.
  • Внутри функции обновления сцены просто перезаписывать значение трансформации.
const tempTransform = new Ammo.btTransform();

function updateBodyPosition(body, position, rotation) {
    tempTransform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z));
    tempTransform.setRotation(new Ammo.btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w));
    body.setWorldTransform(tempTransform);
}

Здесь важно не создавать новые btVector3 и btQuaternion каждый кадр, а использовать либо временные объекты из пула, либо переиспользовать один глобальный объект.

Профилирование и отладка утечек

Минимизация вызовов Ammo.destroy() требует контроля использования памяти:

  • Включить счетчики для всех созданных объектов (btVector3, btTransform, btRigidBody).
  • Периодически проверять разницу между количеством созданных и уничтоженных объектов.
  • Использовать инструментальные средства профилирования JavaScript и WebAssembly (например, Chrome DevTools → Memory).

Пример счетчика:

let vectorCount = 0;

function createVector(x, y, z) {
    vectorCount++;
    return new Ammo.btVector3(x, y, z);
}

function destroyVector(vec) {
    Ammo.destroy(vec);
    vectorCount--;
}

Это позволяет точно видеть, где объекты не возвращаются в пул или не уничтожаются.

Рекомендации по архитектуре

  • Разделять создание объектов и их использование в симуляции.
  • Использовать пулы для всех часто создаваемых структур: векторов, кватернионов, трансформов.
  • Минимизировать вызовы Ammo.destroy() внутри высокочастотных функций (коллизии, импульсы, позиции объектов).
  • Центрально освобождать ресурсы при смене сцены или завершении симуляции.

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