Цикл симуляции: шаги, итерации, время

Физический движок Oimo.js реализует дискретную пошаговую симуляцию твёрдых тел на основе интеграции уравнений движения и решения ограничений. Центральным элементом является объект мира — World, который управляет обновлением тел, столкновениями и решением ограничений в рамках одного шага времени.

Цикл симуляции состоит из последовательных этапов:

  1. Обновление временного шага.
  2. Обнаружение столкновений (broadphase и narrowphase).
  3. Формирование контактных ограничений.
  4. Решение ограничений (итерационный солвер).
  5. Интеграция скоростей и положений.
  6. Обновление состояний тел (сон, пробуждение).

Каждый вызов world.step() запускает полный цикл обновления сцены.


Временной шаг (Time Step)

Физическая симуляция в Oimo.js основана на фиксированном временном шаге dt, который определяет, насколько «вперёд» во времени продвигается система за один вызов шага.

Базовый вызов

world.step();

По умолчанию используется внутреннее значение dt, заданное при создании мира:

const world = new OIMO.World({
    timestep: 1 / 60,
    iterations: 8,
    broadphase: 2,
    worldscale: 1,
    random: true,
    info: false,
    gravity: [0, -9.8, 0]
});

Параметр timestep определяет фиксированный шаг времени.

Фиксированный vs переменный шаг

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

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

Поэтому рекомендуется использовать фиксированный временной шаг и аккумулировать прошедшее время.

Правильная схема с аккумулятором

let accumulator = 0;
const fixedTimeStep = 1 / 60;

function update(deltaTime) {
    accumulator += deltaTime;

    while (accumulator >= fixedTimeStep) {
        world.step(fixedTimeStep);
        accumulator -= fixedTimeStep;
    }
}

Такая схема обеспечивает:

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

Этапы одного шага симуляции

1. Broadphase

На первом этапе определяется список потенциально пересекающихся тел. Используются грубые объёмы (AABB), чтобы сократить количество проверок.

Broadphase:

  • не определяет точные точки контакта,
  • только выявляет возможные пары.

Это критично для производительности, особенно при большом числе объектов.


2. Narrowphase

На этом этапе выполняется точная проверка столкновения:

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

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


3. Построение контактных ограничений

Каждое столкновение преобразуется в набор ограничений:

  • нормальное ограничение (предотвращает проникновение),
  • фрикционное ограничение (трение).

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


4. Итерационный солвер

Oimo.js использует итерационный метод (Sequential Impulse Solver), основанный на методах Гаусса–Зейделя.

Основная идея:

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

Параметр iterations определяет количество проходов:

const world = new OIMO.World({
    timestep: 1 / 60,
    iterations: 10
});

Влияние количества итераций

Итерации Результат
4–6 Быстро, но менее стабильно
8–10 Баланс качества и скорости
12+ Более жёсткие соединения, выше нагрузка

Малое число итераций приводит к:

  • мягким соединениям,
  • «проваливанию» объектов,
  • дрожанию стеков.

Большое число итераций увеличивает нагрузку на CPU.


5. Интеграция скоростей и положений

После решения ограничений происходит интеграция:

  • линейной скорости,
  • угловой скорости,
  • положения,
  • ориентации.

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

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

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

Гравитация добавляется как постоянное ускорение:

gravity: [0, -9.8, 0]

6. Управление состоянием сна

Для оптимизации производительности тела могут «засыпать».

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

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

Преимущества:

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

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

  • столкновении,
  • приложении силы,
  • изменении положения.

Управление временем в реальном приложении

Связь с requestAnimationFrame

Обычно физика обновляется внутри игрового цикла:

let lastTime = performance.now();

function loop() {
    const now = performance.now();
    const deltaTime = (now - lastTime) / 1000;
    lastTime = now;

    update(deltaTime);

    requestAnimationFrame(loop);
}

loop();

Физика обновляется независимо от рендеринга, но синхронизируется с ним.


Подшаги (Substeps)

Если сцена содержит:

  • быстрые объекты,
  • тонкие геометрии,
  • сложные стеки,

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

for (let i = 0; i < 3; i++) {
    world.step(1 / 180);
}

Преимущества:

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

Недостаток — рост нагрузки.


Влияние шага времени на стабильность

Слишком большой шаг

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

Слишком маленький шаг

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

Оптимальным считается диапазон:

1/60 – 1/120 секунды

Детализация: решение ограничений

Солвер работает с импульсами, а не напрямую с силами.

Импульс:

J = F * dt

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

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

Псевдокод итерационного решения:

for iteration:
    for each constraint:
        compute corrective impulse
        apply impulse to bodies

Сходимость достигается постепенно. Чем больше итераций — тем ближе результат к точному решению.


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

Физические движки чувствительны к:

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

Рекомендуемый масштаб:

  • объекты 0.1 – 10 единиц,
  • массы 0.1 – 100,
  • гравитация порядка 9.8.

Нарушение масштабов приводит к:

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

Интерполяция для рендеринга

При фиксированном шаге физики и переменном FPS возможна визуальная интерполяция:

alpha = accumulator / fixedTimeStep
renderPosition = current * alpha + previous * (1 - alpha)

Это устраняет визуальные рывки при низком FPS.


Оптимизация цикла

1. Уменьшение количества активных тел

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

2. Снижение итераций

Если сцена проста — можно уменьшить iterations.

3. Упрощение геометрии

Меньше сложных коллайдеров — быстрее narrowphase.

4. Контроль подшагов

Не выполнять лишние подшаги без необходимости.


Детализация порядка операций внутри world.step()

Упрощённая последовательность:

  1. Обновление AABB.
  2. Broadphase.
  3. Narrowphase.
  4. Создание контактов.
  5. Подготовка ограничений.
  6. Итерационный солвер.
  7. Интеграция скоростей.
  8. Обновление позиций.
  9. Обновление состояния сна.

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

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

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

Силы применяются перед интеграцией:

body.applyForce([0, 10, 0]);

Во время шага:

  1. Силы преобразуются в ускорения.
  2. Ускорения изменяют скорости.
  3. Скорости изменяют положение.

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


Практические параметры для разных задач

Игровая сцена с десятками объектов

  • timestep: 1/60
  • iterations: 8
  • сон включён

Стек из множества коробок

  • iterations: 10–12
  • возможно 2 подшага

Высокоскоростные объекты

  • уменьшить timestep
  • увеличить подшаги

Детерминированность

При фиксированном:

  • шаге времени,
  • количестве итераций,
  • порядке обработки,

симуляция будет детерминированной на одной платформе. Это важно для:

  • сетевых игр,
  • повторов,
  • синхронизации состояния.

Переменный шаг разрушает детерминированность.


Основные принципы стабильного цикла симуляции

  1. Использовать фиксированный временной шаг.
  2. Контролировать количество итераций.
  3. Поддерживать разумный масштаб сцены.
  4. Избегать экстремальных масс и размеров.
  5. Использовать подшаги только при необходимости.
  6. Включать сон для оптимизации.

Цикл симуляции в Oimo.js представляет собой сбалансированную систему численных методов, оптимизаций и итерационных алгоритмов, обеспечивающих устойчивое и предсказуемое моделирование твёрдых тел в реальном времени.