Общая схема работы движка

Oimo.js — это трёхмерный физический движок, реализующий жёсткотельную динамику в среде JavaScript. Его архитектура построена по классической схеме пошаговой симуляции с дискретным временем. В основе лежит цикл обновления, в котором происходит интеграция движения тел, обнаружение столкновений и разрешение контактов.

Общая схема работы движка включает следующие ключевые подсистемы:

  1. Мир (World)
  2. Тела (Rigid Bodies)
  3. Формы (Shapes)
  4. Детекция столкновений
  5. Контактный менеджер
  6. Решатель ограничений
  7. Интегратор движения

Каждая из этих подсистем участвует в пошаговой симуляции и взаимодействует с остальными через строго определённый порядок вызовов.


Мир как центральный объект симуляции

Основной управляющий объект — World. Он:

  • хранит список всех физических тел;
  • управляет гравитацией;
  • контролирует временной шаг;
  • запускает процесс симуляции через метод step().

Пример создания мира:

const world = new OIMO.World({
    gravity: [0, -9.8, 0]
});

Гравитация задаётся вектором, влияющим на все динамические тела.

Временной шаг

Движок использует дискретную интеграцию. Каждый вызов world.step(dt) выполняет полный цикл симуляции на интервал времени dt.

Типичная схема в игровом цикле:

function update() {
    world.step(1/60);
    requestAnimationFrame(update);
}

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


Жёсткие тела (Rigid Bodies)

Жёсткое тело — базовый физический объект. Оно содержит:

  • позицию;
  • ориентацию (кватернион);
  • линейную и угловую скорость;
  • массу и инерцию;
  • набор форм (shapes).

Тела бывают двух типов:

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

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

Динамические тела

  • имеют ненулевую массу;
  • подвержены действию сил и столкновениям;
  • участвуют в полной симуляции.

Пример создания динамического тела:

const body = world.add({
    type: 'box',
    size: [1, 1, 1],
    pos: [0, 10, 0],
    move: true
});

Формы (Shapes) и коллизионная геометрия

Форма определяет геометрию столкновения. Oimo.js поддерживает:

  • Box
  • Sphere
  • Cylinder
  • Capsule
  • Convex

Форма прикрепляется к телу и участвует только в расчётах столкновений. Визуализация осуществляется сторонними библиотеками, например, Three.js.

Важно понимать разделение:

  • Shape — физическая геометрия
  • Mesh — визуальное представление

Это разделение позволяет оптимизировать вычисления.


Этапы симуляции внутри world.step()

Каждый вызов step() выполняет строгую последовательность действий.

1. Применение сил

На этом этапе:

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

Формула линейного ускорения:

a = F / m

Где:

  • F — сумма сил
  • m — масса тела

2. Широкофазная детекция столкновений (Broad Phase)

Задача — быстро определить потенциально пересекающиеся пары тел.

Используется алгоритм пространственного разбиения (чаще всего Sweep and Prune). Он:

  • сортирует объекты по осям;
  • проверяет перекрытие проекций AABB;
  • формирует список возможных пар.

На этом этапе точная геометрия не проверяется.


3. Узкофазная детекция столкновений (Narrow Phase)

Для каждой потенциальной пары:

  • выполняется точный геометрический тест;
  • вычисляется точка контакта;
  • определяется нормаль столкновения;
  • рассчитывается глубина проникновения.

В зависимости от формы используются различные алгоритмы:

  • SAT (Separating Axis Theorem)
  • GJK (для выпуклых тел)

Результатом является объект контакта.


Контакты и ограничивающие уравнения

Контакт описывается как ограничение, предотвращающее проникновение тел друг в друга.

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

  • точку соприкосновения;
  • нормаль;
  • коэффициент трения;
  • коэффициент восстановления (restitution).

Математическая модель

Контакт формирует импульсное ограничение:

Jv + b = 0

Где:

  • J — якобиан
  • v — скорости тел
  • b — коррекционный член

Эти ограничения передаются в решатель.


Решатель ограничений (Constraint Solver)

Oimo.js использует итерационный импульсный решатель.

Этапы работы:

  1. Предварительная подготовка (pre-step)
  2. Итеративное вычисление импульсов
  3. Применение корректирующих импульсов к телам

Решатель работает несколько итераций за шаг (обычно 8–20), что повышает стабильность.

Импульсы корректируют:

  • линейные скорости
  • угловые скорости

Это устраняет проникновение и моделирует трение.


Интеграция движения

После разрешения всех ограничений выполняется интеграция движения.

Используется полуявный метод Эйлера:

v += a * dt
x += v * dt

Для вращения применяется интеграция кватерниона.

Этап интеграции:

  • обновляет позиции;
  • обновляет ориентацию;
  • подготавливает тела к следующему шагу.

Обновление трансформаций

После расчётов:

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

Типичный цикл взаимодействия с визуализацией:

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

Таким образом физика и графика остаются независимыми.


Система сна (Sleeping)

Для оптимизации Oimo.js использует механизм “сна”.

Тело засыпает, если:

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

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

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


Обработка ограничений (Joints)

Помимо контактов существуют механические ограничения:

  • шарнирные соединения
  • пружины
  • фиксации

Они формируют дополнительные уравнения, обрабатываемые тем же решателем.

Каждое ограничение:

  • вычисляет якобиан;
  • формирует импульс;
  • участвует в общей системе итераций.

Полный цикл работы движка

Суммарно один шаг симуляции включает:

  1. Применение сил
  2. Broad Phase
  3. Narrow Phase
  4. Формирование контактов
  5. Решение ограничений
  6. Интеграцию движения
  7. Обновление состояния тел
  8. Проверку сна

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


Взаимодействие с игровым циклом

Движок не содержит встроенного рендеринга. Он интегрируется в цикл приложения:

function loop() {
    world.step(1/60);
    render();
    requestAnimationFrame(loop);
}

Часто используется совместно с Three.js, где:

  • Oimo.js отвечает за физику;
  • Three.js — за графику.

Такое разделение соответствует классической архитектуре игрового движка: физика — логика — визуализация.


Детализация порядка вычислений

Внутренний порядок вызовов можно представить упрощённой схемой:

World.step()
 ├── applyForces()
 ├── broadPhase()
 ├── narrowPhase()
 ├── solveConstraints()
 │    ├── preSolve()
 │    ├── iterate()
 │    └── postSolve()
 ├── integrate()
 └── updateSleepState()

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

  • нестабильности;
  • накоплению ошибок;
  • дрожанию объектов;
  • “взрывам” симуляции.

Численная устойчивость

Стабильность обеспечивается:

  • фиксированным временем шага;
  • ограничением максимальной скорости;
  • ограничением глубины проникновения;
  • итерационным решателем.

При слишком большом dt:

  • возрастают ошибки интеграции;
  • ухудшается работа решателя;
  • появляются артефакты.

Рекомендуемое значение: 1/60 или 1/120.


Логическая модель данных

Каждый объект в мире хранит:

  • трансформацию
  • скорости
  • силы
  • ссылки на контакты
  • ссылки на ограничения

Контакты образуют граф взаимодействий. Решатель проходит по этому графу при вычислении импульсов.

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


Обобщённая схема работы

Общая схема работы Oimo.js может быть представлена как:

Силы → Столкновения → Ограничения → Импульсы → Новые скорости → Новые позиции

Этот цикл повторяется дискретно, создавая иллюзию непрерывного движения.

Вся архитектура движка построена вокруг строгой пошаговой обработки, что обеспечивает предсказуемость, расширяемость и высокую производительность при моделировании трёхмерной физики в среде JavaScript.