Итерация по контактным точкам

Библиотека Ammo.js представляет собой порт физического движка Bullet Physics на JavaScript и WebAssembly. В основе обработки столкновений лежит подсистема collision detection, формирующая данные о пересечениях тел в виде контактных точек (contact points). Итерация по этим точкам необходима для:

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

В Ammo.js данные о контактах доступны через объекты:

  • btDispatcher
  • btCollisionWorld
  • btPersistentManifold
  • btManifoldPoint

Итерация строится вокруг получения списка манипулов контакта (manifolds) и обхода содержащихся в них точек.


Контактный манифолд (btPersistentManifold)

Манифолд — это структура, описывающая контакт двух тел. Один манифолд соответствует одной паре объектов, находящихся во взаимодействии.

Внутри манифолда хранится:

  • указатель на тело A,
  • указатель на тело B,
  • массив контактных точек,
  • служебная информация для solver’а.

Каждая контактная точка содержит:

  • позицию контакта на теле A,
  • позицию контакта на теле B,
  • нормаль,
  • глубину проникновения,
  • импульсы.

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

После выполнения шага симуляции:

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 требуется для корректного приведения типов.
  • Контактных точек может быть несколько — обычно до 4.

Итерация по контактным точкам

Каждый btManifoldPoint содержит подробную информацию.

Получение глубины проникновения

const distance = contactPoint.getDistance();
  • Значение < 0 — тела пересекаются.
  • Значение > 0 — тела находятся на расстоянии (контакт может быть потенциальным).

В большинстве случаев проверка выглядит так:

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

Это позволяет сопоставлять физическое тело с:

  • 3D-объектом Three.js,
  • игровым компонентом,
  • сущностью ECS.

Фильтрация по конкретным объектам

Часто требуется обрабатывать столкновения только определённых типов тел. Возможные подходы:

1. Проверка пользовательских данных

if (user0.type === "player" && user1.type === "enemy") {
    // логика урона
}

2. Проверка collision group и mask

При создании тела:

physicsWorld.addRigidBody(
    body,
    collisionGroup,
    collisionMask
);

Группы позволяют отсекать ненужные контакты ещё на уровне движка.


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

Итерация по контактным точкам выполняется каждый кадр, поэтому:

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

Оптимизированная схема

  1. Проверка типа тел.
  2. Проверка глубины проникновения.
  3. Минимальный сбор данных.
  4. Передача события в игровую систему.

Особенности жизненного цикла контактной точки

Контактные точки в Bullet:

  • сохраняются между кадрами,
  • обновляются при продолжительном контакте,
  • удаляются при разрыве столкновения.

Это означает:

  • при непрерывном соприкосновении будет приходить несколько кадров подряд,
  • требуется собственная система отслеживания событий enter, stay, exit.

Обычно хранится набор активных пар ID:

const activeCollisions = new Set();

И при каждой итерации обновляется состояние.


Работа с импульсами

btManifoldPoint предоставляет информацию о силе удара:

const impulse = contactPoint.getAppliedImpulse();

Использование:

  • вычисление урона,
  • громкость звука,
  • сила вибрации,
  • триггер разрушения.

Большой импульс означает сильное столкновение.


Отличие contact callback от ручной итерации

В Bullet существует механизм callback’ов, однако в Ammo.js он ограничен. На практике применяется ручная итерация через dispatcher, поскольку:

  • проще контролировать порядок обработки,
  • удобнее интегрировать с ECS,
  • отсутствуют сложности с WebAssembly callback’ами.

Типичные ошибки

1. Отсутствие проверки distance < 0 Приводит к ложным срабатываниям.

2. Создание новых btVector3 внутри цикла Увеличивает нагрузку на GC.

3. Неправильное направление нормали Следует учитывать, что нормаль направлена от B к A.

4. Многократная обработка одного столкновения Без системы отслеживания enter/stay события будут вызываться каждый кадр.


Интеграция с рендерингом (пример с Three.js)

При использовании связки Ammo.js и Three.js:

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

Позиция эффекта берётся из getPositionWorldOnB(), а ориентация — из нормали.


Расширенные сценарии

1. Триггерные объекты

Создание тела с флагом CF_NO_CONTACT_RESPONSE:

body.setCollisionFlags(
    body.getCollisionFlags() | 4
);

Такие тела:

  • участвуют в детекции,
  • не участвуют в физическом отклике,
  • требуют ручной обработки через итерацию.

2. Определение направления удара

Можно вычислить относительную скорость:

const vel0 = body0.getLinearVelocity();
const vel1 = body1.getLinearVelocity();

И затем:

const relativeVelocity = vel0.op_sub(vel1);

Скалярное произведение с нормалью позволяет понять, происходит ли сближение.


Структура данных btManifoldPoint

Основные методы:

  • getDistance()
  • getPositionWorldOnA()
  • getPositionWorldOnB()
  • get_m_normalWorldOnB()
  • getAppliedImpulse()

Каждый вызов — переход в WebAssembly, поэтому число обращений минимизируется.


Практическая схема обработки

  1. Выполнить stepSimulation.
  2. Получить dispatcher.
  3. Пройти по манифолдам.
  4. Отфильтровать пары.
  5. Пройти по контактным точкам.
  6. Проверить distance.
  7. Извлечь данные.
  8. Передать в игровую систему.
  9. Обновить состояние enter/stay/exit.

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