Производительность при большом числе тел

Физический движок Cannon.js построен вокруг дискретного численного интегрирования и итерационного решателя ограничений. При работе с десятками объектов нагрузка остается умеренной, однако при сотнях и тысячах тел начинают проявляться узкие места:

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

Оптимизация требует понимания того, какие этапы занимают наибольшее время процессора и как их контролировать.


Широкофазная проверка столкновений (Broadphase)

Назначение

Broadphase уменьшает количество пар тел, передаваемых в точную фазу столкновений. Без него сложность проверки растёт как O(n²), что становится критично при большом числе объектов.

Доступные реализации

В Cannon.js доступны несколько стратегий:

  • NaiveBroadphase — проверяет все пары тел. Подходит только для малого количества объектов.
  • SAPBroadphase (Sweep and Prune) — сортировка по осям и проверка пересечений интервалов.
  • GridBroadphase (в некоторых сборках/форках) — пространственное разбиение.

Для сцен с сотнями динамических тел предпочтителен SAPBroadphase:

world.broadphase = new CANNON.SAPBroadphase(world);
world.broadphase.axisIndex = 0; // оптимальная ось сортировки

Выбор оси сортировки

Если большая часть движения происходит вдоль одной оси (например, гоночная трасса вдоль X), установка axisIndex в соответствующее значение снижает количество пересортировок.

Отключение лишних столкновений

Фильтрация через collision groups:

body.collisionFilterGroup = 1;
body.collisionFilterMask = 2;

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


Узкофазная проверка (Narrowphase)

Влияние сложности формы

Столкновения примитивов (Sphere, Box, Plane) вычисляются быстрее, чем сложных форм (ConvexPolyhedron, Trimesh). При большом количестве тел следует:

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

Пример упрощения:

const compound = new CANNON.Body({ mass: 5 });
compound.addShape(new CANNON.Box(new CANNON.Vec3(1, 1, 1)));
compound.addShape(new CANNON.Sphere(0.5), new CANNON.Vec3(1, 0, 0));

Составное тело часто работает быстрее, чем одна сложная выпуклая оболочка.


Итерационный решатель (Solver)

Количество итераций

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

world.solver.iterations = 10;

Чем больше итераций, тем стабильнее решение, но выше нагрузка. При большом числе тел:

  • уменьшение итераций до 5–7 снижает нагрузку,
  • увеличение до 15–20 повышает точность для стеков объектов.

Баланс выбирается экспериментально.

Tolerance

world.solver.tolerance = 0.01;

Увеличение tolerance позволяет завершать итерации раньше, снижая нагрузку.


Шаг симуляции и временной интервал

Фиксированный шаг

Рекомендуемый способ:

world.step(1 / 60, deltaTime, 3);

Параметры:

  • 1/60 — фиксированный шаг,
  • deltaTime — реальное время между кадрами,
  • 3 — максимум подшагов.

Ограничение числа подшагов предотвращает лавинообразный рост вычислений при падении FPS.

Снижение частоты обновления

При большом числе объектов допустимо:

  • уменьшить частоту до 1/30,
  • использовать интерполяцию для визуальной плавности.

Спящие тела (Sleep)

Автоматический режим сна

Один из самых эффективных способов оптимизации:

world.allowSleep = true;

body.sleepSpeedLimit = 0.1;
body.sleepTimeLimit = 1;

Тело переходит в состояние сна, если:

  • скорость ниже sleepSpeedLimit,
  • условие выполняется дольше sleepTimeLimit.

Спящие тела:

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

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


Ограничение динамических тел

Статические и кинематические объекты

Для неподвижных объектов:

mass: 0

Статические тела:

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

Если объект двигается по заданной траектории, предпочтительнее кинематический режим (в форках, таких как cannon-es):

body.type = CANNON.Body.KINEMATIC;

Это уменьшает вычислительную нагрузку по сравнению с динамическим телом.


Оптимизация больших стеков объектов

Проблема накопления ошибок

При большом числе контактирующих тел:

  • возрастает количество ограничений,
  • увеличивается нагрузка на solver,
  • появляются нестабильности.

Методы оптимизации:

  1. Уменьшение числа итераций.
  2. Повышение contactEquationStiffness.
  3. Использование меньшего timeStep.
world.defaultContactMaterial.contactEquationStiffness = 1e7;
world.defaultContactMaterial.contactEquationRelaxation = 3;

Слишком высокие значения увеличивают нагрузку, поэтому требуется баланс.


Пул объектов и управление памятью

Частое создание и удаление тел вызывает:

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

Рекомендуется:

  • переиспользовать тела,
  • хранить неактивные объекты в пуле,
  • отключать тело вместо удаления.
body.velocity.setZero();
body.angularVelocity.setZero();
body.position.set(0, -1000, 0);
body.sleep();

Снижение количества проверок столкновений

Ограничение пар

Если известно, что объекты взаимодействуют только в пределах зоны:

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

Пример кластеризации

Создание нескольких миров физики вместо одного большого:

const worldA = new CANNON.World();
const worldB = new CANNON.World();

Разделение сцены снижает квадратичный рост проверок.


Оптимизация совместно с графическим движком

Часто Cannon.js используется вместе с Three.js.

Разделение частоты рендера и физики

Физика может обновляться реже, чем рендер:

  • физика — 30 Гц,
  • рендер — 60 Гц,
  • позиция интерполируется.

Это уменьшает нагрузку почти вдвое без существенной потери плавности.

Избежание лишних синхронизаций

Синхронизация должна происходить один раз за кадр:

mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);

Множественные обращения к физическим данным внутри кадра увеличивают накладные расходы.


Ограничения и масштабирование

Практические пределы

На типичном клиентском устройстве:

  • 100–200 динамических тел — безопасный диапазон,
  • 500 — требует агрессивной оптимизации,
  • 1000+ — возможно только при сильном упрощении форм и активном использовании сна.

Стратегии масштабирования

  1. Упрощение геометрии.
  2. Разделение сцен.
  3. Использование sleep.
  4. Минимизация количества активных контактов.
  5. Контроль частоты обновления.

Профилирование

Анализ производительности проводится через:

  • вкладку Performance в браузере,
  • измерение времени world.step,
  • логирование количества контактных пар.

Пример замера:

const start = performance.now();
world.step(1/60);
const end = performance.now();
console.log("Physics step:", end - start);

Рост времени шага при увеличении числа тел показывает предел масштабируемости текущей конфигурации.


Типовые ошибки, приводящие к падению производительности

  • Использование Trimesh для динамических объектов.
  • Отключённый режим сна.
  • Слишком высокая частота шага.
  • Отсутствие broadphase оптимизации.
  • Чрезмерное количество контактных материалов.
  • Создание тел каждый кадр.

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