Библиотека Ammo.js представляет собой порт физического движка Bullet Physics на JavaScript и WebAssembly. В основе обработки столкновений лежит подсистема collision detection, формирующая данные о пересечениях тел в виде контактных точек (contact points). Итерация по этим точкам необходима для:
В Ammo.js данные о контактах доступны через объекты:
btDispatcherbtCollisionWorldbtPersistentManifoldbtManifoldPointИтерация строится вокруг получения списка манипулов контакта (manifolds) и обхода содержащихся в них точек.
Манифолд — это структура, описывающая контакт двух тел. Один манифолд соответствует одной паре объектов, находящихся во взаимодействии.
Внутри манифолда хранится:
Каждая контактная точка содержит:
После выполнения шага симуляции:
physicsWorld.stepSimulation(deltaTime, 10);
можно получить диспетчер столкновений:
const dispatcher = physicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();
getNumManifolds() возвращает количество активных пар
объектов, находящихся в контакте.
Основной цикл:
for (let i = 0; i < numManifolds; i++) {
const contactManifold = dispatcher.getManifoldByIndexInternal(i);
const body0 = Ammo.castObject(
contactManifold.getBody0(),
Ammo.btRigidBody
);
const body1 = Ammo.castObject(
contactManifold.getBody1(),
Ammo.btRigidBody
);
const numContacts = contactManifold.getNumContacts();
for (let j = 0; j < numContacts; j++) {
const contactPoint = contactManifold.getContactPoint(j);
// обработка точки
}
}
getManifoldByIndexInternal() возвращает внутреннюю
структуру Bullet.Ammo.castObject требуется для корректного приведения
типов.Каждый btManifoldPoint содержит подробную
информацию.
const distance = contactPoint.getDistance();
В большинстве случаев проверка выглядит так:
if (distance < 0) {
// реальное столкновение
}
const ptA = contactPoint.getPositionWorldOnA();
const ptB = contactPoint.getPositionWorldOnB();
Возвращаются btVector3.
Доступ к координатам:
const x = ptA.x();
const y = ptA.y();
const z = ptA.z();
Разница между ptA и ptB позволяет вычислить
глубину и направление смещения.
const normal = contactPoint.get_m_normalWorldOnB();
Нормаль направлена от объекта B к объекту A.
Компоненты:
const nx = normal.x();
const ny = normal.y();
const nz = normal.z();
Нормаль используется для:
function processCollisions(physicsWorld) {
const dispatcher = physicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();
for (let i = 0; i < numManifolds; i++) {
const manifold = dispatcher.getManifoldByIndexInternal(i);
const body0 = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody);
const body1 = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody);
const numContacts = manifold.getNumContacts();
for (let j = 0; j < numContacts; j++) {
const contactPoint = manifold.getContactPoint(j);
if (contactPoint.getDistance() < 0) {
const position = contactPoint.getPositionWorldOnB();
const normal = contactPoint.get_m_normalWorldOnB();
console.log(
"Collision at:",
position.x(),
position.y(),
position.z()
);
console.log(
"Normal:",
normal.x(),
normal.y(),
normal.z()
);
}
}
}
}
Ammo.js не хранит пользовательские данные напрямую в
btRigidBody. Обычно используется
setUserPointer():
body.setUserPointer(userData);
При обработке:
const user0 = body0.getUserPointer();
const user1 = body1.getUserPointer();
Это позволяет сопоставлять физическое тело с:
Часто требуется обрабатывать столкновения только определённых типов тел. Возможные подходы:
if (user0.type === "player" && user1.type === "enemy") {
// логика урона
}
При создании тела:
physicsWorld.addRigidBody(
body,
collisionGroup,
collisionMask
);
Группы позволяют отсекать ненужные контакты ещё на уровне движка.
Итерация по контактным точкам выполняется каждый кадр, поэтому:
Контактные точки в Bullet:
Это означает:
enter, stay, exit.Обычно хранится набор активных пар ID:
const activeCollisions = new Set();
И при каждой итерации обновляется состояние.
btManifoldPoint предоставляет информацию о силе
удара:
const impulse = contactPoint.getAppliedImpulse();
Использование:
Большой импульс означает сильное столкновение.
В Bullet существует механизм callback’ов, однако в Ammo.js он ограничен. На практике применяется ручная итерация через dispatcher, поскольку:
1. Отсутствие проверки distance < 0
Приводит к ложным срабатываниям.
2. Создание новых btVector3 внутри цикла Увеличивает нагрузку на GC.
3. Неправильное направление нормали Следует учитывать, что нормаль направлена от B к A.
4. Многократная обработка одного столкновения Без
системы отслеживания enter/stay события будут вызываться
каждый кадр.
При использовании связки Ammo.js и Three.js:
Позиция эффекта берётся из getPositionWorldOnB(), а
ориентация — из нормали.
Создание тела с флагом CF_NO_CONTACT_RESPONSE:
body.setCollisionFlags(
body.getCollisionFlags() | 4
);
Такие тела:
Можно вычислить относительную скорость:
const vel0 = body0.getLinearVelocity();
const vel1 = body1.getLinearVelocity();
И затем:
const relativeVelocity = vel0.op_sub(vel1);
Скалярное произведение с нормалью позволяет понять, происходит ли сближение.
Основные методы:
getDistance()getPositionWorldOnA()getPositionWorldOnB()get_m_normalWorldOnB()getAppliedImpulse()Каждый вызов — переход в WebAssembly, поэтому число обращений минимизируется.
stepSimulation.distance.Итерация по контактным точкам в Ammo.js является фундаментальным инструментом для построения продвинутой физической логики, систем урона, триггеров, аудио-реакций и визуальных эффектов. Глубокое понимание структуры манифолдов и особенностей жизненного цикла контактов позволяет строить масштабируемые и производительные системы взаимодействия объектов в реальном времени.