Фильтрация столкновений через группы

В основе фильтрации столкновений в Ammo.js лежит механизм групп и масок, унаследованный от физического движка Bullet. Каждое физическое тело (rigid body) может принадлежать к определённой группе столкновений и одновременно иметь маску — набор групп, с которыми ему разрешено взаимодействовать.

Фильтрация происходит до расчёта контактов, что позволяет:

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

Принцип работы: группы и маски

Collision Group (группа)

Группа — это битовая маска, обозначающая принадлежность тела к определённой категории.

Каждая группа задаётся степенью двойки:

Назначение Значение Бит
Группа 1 1 0001
Группа 2 2 0010
Группа 3 4 0100
Группа 4 8 1000

Использование степеней двойки позволяет комбинировать группы через побитовое ИЛИ (|).


Collision Mask (маска)

Маска определяет, с какими группами объект может сталкиваться.

Если:

(groupA & maskB) !== 0

и

(groupB & maskA) !== 0

то объекты взаимодействуют.

Иначе столкновение игнорируется полностью — даже широкая фаза (broadphase) не будет учитывать такую пару.


Стандартные группы Bullet

В Bullet (и, соответственно, в Ammo.js) определены базовые группы:

  • btBroadphaseProxy.DefaultFilter
  • btBroadphaseProxy.StaticFilter
  • btBroadphaseProxy.KinematicFilter
  • btBroadphaseProxy.DebrisFilter
  • btBroadphaseProxy.SensorTrigger
  • btBroadphaseProxy.CharacterFilter
  • btBroadphaseProxy.AllFilter

В Ammo.js эти значения обычно задаются вручную как числа.

Пример соответствия:

DefaultFilter     = 1
StaticFilter      = 2
KinematicFilter   = 4
DebrisFilter      = 8
SensorTrigger     = 16
CharacterFilter   = 32
AllFilter         = -1

Добавление rigid body с указанием группы и маски

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

physicsWorld.addRigidBody(body, group, mask);

Если параметры group и mask не переданы, используется DefaultFilter и AllFilter.

Пример: статическая поверхность и динамический объект

const GROUP_STATIC = 1;
const GROUP_DYNAMIC = 2;

const MASK_STATIC = GROUP_DYNAMIC;
const MASK_DYNAMIC = GROUP_STATIC;

// Статическая платформа
physicsWorld.addRigidBody(staticBody, GROUP_STATIC, MASK_STATIC);

// Динамический куб
physicsWorld.addRigidBody(dynamicBody, GROUP_DYNAMIC, MASK_DYNAMIC);

В этом случае:

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

Логическая сегментация сцены

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

Пример структуры игры

Группа Назначение
1 Игрок
2 Враги
4 Пули
8 Статика
16 Триггеры

Возможная настройка масок

const GROUP_PLAYER   = 1;
const GROUP_ENEMY    = 2;
const GROUP_BULLET   = 4;
const GROUP_STATIC   = 8;
const GROUP_TRIGGER  = 16;

const MASK_PLAYER  = GROUP_ENEMY | GROUP_STATIC | GROUP_TRIGGER;
const MASK_ENEMY   = GROUP_PLAYER | GROUP_STATIC;
const MASK_BULLET  = GROUP_ENEMY | GROUP_STATIC;
const MASK_STATIC  = GROUP_PLAYER | GROUP_ENEMY | GROUP_BULLET;
const MASK_TRIGGER = GROUP_PLAYER;

Такая конфигурация обеспечивает:

  • игрок сталкивается с врагами и статикой;
  • пули не сталкиваются с игроком;
  • триггеры реагируют только на игрока;
  • враги игнорируют пули союзников (при необходимости можно расширить).

Использование битовых операций

Битовые операции — ключ к гибкости системы.

Побитовое ИЛИ

const mask = GROUP_PLAYER | GROUP_ENEMY;

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

if (mask & GROUP_PLAYER) {
    // группа входит в маску
}

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

Фильтрация через группы снижает нагрузку на:

  • broadphase (поиск потенциальных пар),
  • narrowphase (точный расчёт контакта),
  • solver (разрешение столкновений).

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

Типичная ошибка

Использование AllFilter для всех тел:

physicsWorld.addRigidBody(body);

Это приводит к проверке каждого тела со всеми остальными.


Динамическое изменение группы и маски

После добавления в мир изменить фильтрацию напрямую нельзя. Требуется:

  1. Удалить тело из мира.
  2. Добавить его снова с новой группой и маской.
physicsWorld.removeRigidBody(body);
physicsWorld.addRigidBody(body, newGroup, newMask);

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

  • временной неуязвимости,
  • фазирования,
  • изменения состояния объекта.

Фильтрация для триггеров (Sensor)

Триггеры создаются как объекты без физического отклика, но с проверкой пересечений.

Необходимо:

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

И добавить в мир с нужной группой:

physicsWorld.addRigidBody(body, GROUP_TRIGGER, MASK_TRIGGER);

Такие тела:

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

Пересечение с kinematic-объектами

Kinematic-тела обычно:

  • входят в отдельную группу,
  • имеют маску только для динамических тел.

Пример:

const GROUP_KINEMATIC = 32;

physicsWorld.addRigidBody(
    kinematicBody,
    GROUP_KINEMATIC,
    GROUP_DYNAMIC
);

Это исключает их столкновение со статикой и друг с другом.


Пример комплексной конфигурации сцены

const GROUP_PLAYER   = 1;
const GROUP_ENEMY    = 2;
const GROUP_BULLET   = 4;
const GROUP_STATIC   = 8;
const GROUP_TRIGGER  = 16;
const GROUP_KINEMATIC = 32;

physicsWorld.addRigidBody(playerBody, GROUP_PLAYER, GROUP_ENEMY | GROUP_STATIC | GROUP_TRIGGER);
physicsWorld.addRigidBody(enemyBody, GROUP_ENEMY, GROUP_PLAYER | GROUP_STATIC | GROUP_BULLET);
physicsWorld.addRigidBody(bulletBody, GROUP_BULLET, GROUP_ENEMY | GROUP_STATIC);
physicsWorld.addRigidBody(staticBody, GROUP_STATIC, GROUP_PLAYER | GROUP_ENEMY | GROUP_BULLET);
physicsWorld.addRigidBody(triggerBody, GROUP_TRIGGER, GROUP_PLAYER);
physicsWorld.addRigidBody(kinematicBody, GROUP_KINEMATIC, GROUP_PLAYER | GROUP_ENEMY);

Такая структура обеспечивает:

  • чёткое разделение логики,
  • минимальное число лишних проверок,
  • удобство масштабирования проекта.

Внутренний механизм фильтрации в Bullet

Внутри Bullet проверка происходит в broadphase через btBroadphaseProxy.

Каждый proxy содержит:

  • m_collisionFilterGroup
  • m_collisionFilterMask

Перед созданием пары проверяется:

if ((proxy0->m_collisionFilterGroup & proxy1->m_collisionFilterMask) != 0 &&
    (proxy1->m_collisionFilterGroup & proxy0->m_collisionFilterMask) != 0)
{
    // создаётся потенциальная пара
}

Ammo.js полностью повторяет эту логику, так как является Emscripten-портом Bullet.


Проектирование архитектуры групп

При проектировании рекомендуется:

  1. Зарезервировать диапазоны битов заранее.
  2. Документировать назначение каждой группы.
  3. Избегать использования отрицательных масок.
  4. Не превышать 16–32 групп (ограничение битовой маски int).

Подход «слоёв»

  • 0–7 бит — основные типы объектов.
  • 8–15 бит — служебные категории.
  • 16+ — временные состояния.

Отладка фильтрации

Признаки некорректной конфигурации:

  • объект «проваливается» сквозь другой,
  • контакт не регистрируется,
  • чрезмерная нагрузка на CPU.

Методы проверки:

  • временно установить маску AllFilter,
  • вывести значения групп в консоль,
  • проверить флаги collision.

Фильтрация через группы является фундаментальным инструментом оптимизации и логической организации физической сцены в Ammo.js. Правильная архитектура collision-групп позволяет контролировать взаимодействия на уровне движка, не перегружая код дополнительными проверками.