Данные контакта: нормаль, глубина проникновения, позиция

В физическом движке на базе Bullet, реализованном в виде WebAssembly-библиотеки Ammo.js, столкновения тел описываются набором структур, содержащих параметры контакта. Ключевыми величинами являются:

  • нормаль контакта
  • глубина проникновения
  • позиция точки контакта

Эти данные формируются на этапе узкофазной проверки столкновений (narrowphase) и используются солвером для расчёта импульсов, трения и корректировки позиций тел.


Общая структура контактных данных

После выполнения шага симуляции (world.stepSimulation) физический мир формирует набор диспетчеров столкновений. Получение контактных данных осуществляется через:

const dispatcher = physicsWorld.getDispatcher();
const numManifolds = dispatcher.getNumManifolds();

Каждый btPersistentManifold содержит контактную информацию для пары тел. Для доступа:

for (let i = 0; i < numManifolds; i++) {
    const manifold = dispatcher.getManifoldByIndexInternal(i);
    const body0 = manifold.getBody0();
    const body1 = manifold.getBody1();
    
    const numContacts = manifold.getNumContacts();
    
    for (let j = 0; j < numContacts; j++) {
        const contactPoint = manifold.getContactPoint(j);
    }
}

Объект btManifoldPoint содержит нормаль, глубину проникновения и позиции контакта.


Нормаль контакта

Определение

Нормаль контакта — это вектор, направленный перпендикулярно поверхности соприкосновения тел. В Ammo.js она определяется в пространстве мира.

Получение нормали:

const normal = contactPoint.get_m_normalWorldOnB();

Возвращается объект btVector3.

Направление

Нормаль всегда направлена:

  • от первого тела к второму,
  • либо в соответствии с внутренним порядком тел в манифолде.

Это принципиально важно при вычислении импульсов:

[ J = -(1 + e) ]

где

  • ( n ) — нормаль контакта
  • ( v_{rel} ) — относительная скорость
  • ( e ) — коэффициент реституции

Нормаль используется для:

  • расчёта отражения,
  • вычисления импульса упругости,
  • определения направления силы реакции.

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

const nx = normal.x();
const ny = normal.y();
const nz = normal.z();

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


Глубина проникновения

Физический смысл

Глубина проникновения (penetration depth) — это величина перекрытия тел вдоль нормали контакта.

Получение:

const distance = contactPoint.getDistance();

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

  • Если distance < 0, тела пересекаются.
  • Если distance = 0, тела касаются.
  • Если distance > 0, точки находятся на расстоянии.

В Bullet/Ammo отрицательное значение означает проникновение.

Геометрическая интерпретация

Глубина рассчитывается как:

[ penetration = - (p_B - p_A) n]

где

  • ( p_A ), ( p_B ) — контактные точки на телах
  • ( n ) — нормаль

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

Глубина влияет на:

  • корректировку позиций (position correction)
  • расчёт стабилизационного импульса
  • предотвращение «залипания» объектов

Bullet применяет метод Баумгарте (Baumgarte stabilization):

[ bias = -]

где ( ) — коэффициент стабилизации.

Пример чтения

if (distance < 0) {
    const penetrationDepth = -distance;
}

Позиция точки контакта

Каждый btManifoldPoint хранит две позиции:

  • точка контакта на первом теле
  • точка контакта на втором теле

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

const positionWorldOnA = contactPoint.getPositionWorldOnA();
const positionWorldOnB = contactPoint.getPositionWorldOnB();

Обе точки заданы в мировых координатах.

Зачем хранятся две точки

Несмотря на совпадение по нормали, точки могут отличаться из-за:

  • численных ошибок,
  • особенностей алгоритма GJK/EPA,
  • аппроксимации контактной поверхности.

Солвер использует среднюю точку либо одну из них в зависимости от внутренней логики.

Практическое применение

Позиция контакта необходима для:

  • расчёта крутящего момента:

[ = r F]

где

  • ( r ) — радиус-вектор от центра массы до точки контакта

  • ( F ) — контактная сила

  • визуализации точек столкновения,

  • создания триггеров,

  • построения пользовательской логики столкновений.

Пример получения координат:

const px = positionWorldOnA.x();
const py = positionWorldOnA.y();
const pz = positionWorldOnA.z();

Связь нормали, глубины и позиции

Контактная структура образует целостную систему:

Параметр Назначение
Нормаль направление реакции
Глубина величина коррекции
Позиция точка приложения силы

Алгоритм работы солвера:

  1. Определяется нормаль.
  2. Вычисляется относительная скорость вдоль нормали.
  3. Учитывается глубина проникновения.
  4. Рассчитывается импульс.
  5. Импульс применяется в точке контакта.

Особенности хранения контактов

Персистентные манифолды

btPersistentManifold сохраняет контактные точки между кадрами для:

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

Каждый манифолд может содержать до 4 контактных точек.

Обновление контактов

При каждом шаге:

  • старые точки пересчитываются,
  • удаляются устаревшие,
  • добавляются новые.

Взаимодействие с Three.js

При интеграции с Three.js контактные точки часто используются для:

  • отрисовки маркеров столкновений,
  • создания эффектов частиц,
  • синхронизации физики и графики.

Пример преобразования:

const contactVector = new THREE.Vector3(
    positionWorldOnA.x(),
    positionWorldOnA.y(),
    positionWorldOnA.z()
);

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

Получение контактных данных — операция относительно дорогая, особенно при большом количестве тел.

Рекомендации:

  • избегать перебора всех манифолдов каждый кадр без необходимости;
  • фильтровать тела по пользовательским флагам;
  • использовать callback-механизмы Bullet при необходимости;
  • минимизировать создание временных объектов btVector3.

После использования временные объекты следует освобождать:

Ammo.destroy(positionWorldOnA);

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

1. Неверная интерпретация знака distance

Положительное значение не означает проникновение.

2. Игнорирование направления нормали

Неверное направление приводит к неправильному расчёту отражения.

3. Использование локальных координат вместо мировых

Методы getPositionWorldOnA/B возвращают координаты в world-space.

4. Отсутствие освобождения памяти

Ammo.js не имеет автоматического сборщика мусора для C++-структур.


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

for (let i = 0; i < dispatcher.getNumManifolds(); i++) {

    const manifold = dispatcher.getManifoldByIndexInternal(i);
    const numContacts = manifold.getNumContacts();

    for (let j = 0; j < numContacts; j++) {

        const pt = manifold.getContactPoint(j);

        const normal = pt.get_m_normalWorldOnB();
        const distance = pt.getDistance();
        const posA = pt.getPositionWorldOnA();

        if (distance < 0) {
            console.log(
                "Contact:",
                "Normal:", normal.x(), normal.y(), normal.z(),
                "Penetration:", -distance,
                "Position:", posA.x(), posA.y(), posA.z()
            );
        }
    }
}

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