Накопление времени и интерполяция состояния

Oimo.js использует фиксированный шаг симуляции для интеграции физических тел в мире. Основная идея заключается в том, что физический движок должен получать равные интервалы времени для расчёта, чтобы избежать нестабильности и неравномерного поведения объектов при изменении частоты кадров. Поскольку реальные игры и анимации могут иметь переменную частоту обновления (deltaTime), необходимо аккумулировать время между кадрами.

В Oimo.js применяется концепция накопления времени (time accumulator). Принцип работы следующий:

  1. На каждый кадр вычисляется прошедшее время deltaTime между текущим и предыдущим кадром.
  2. Это время добавляется к аккумулятору.
  3. Если аккумулятор превышает фиксированный шаг симуляции timeStep, физический мир обновляется на один шаг, а аккумулятор уменьшается на timeStep.
  4. Процесс повторяется, пока аккумулятор больше timeStep.

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

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

const timeStep = 1 / 60; // фиксированный шаг 60 FPS
let accumulator = 0;
let lastTime = performance.now() / 1000;

function update() {
    const currentTime = performance.now() / 1000;
    let deltaTime = currentTime - lastTime;
    lastTime = currentTime;

    accumulator += deltaTime;

    while (accumulator >= timeStep) {
        world.step(timeStep); // шаг симуляции Oimo.js
        accumulator -= timeStep;
    }

    requestAnimationFrame(update);
}

update();

Интерполяция состояния объектов

Проблема прямого использования фиксированного шага симуляции заключается в том, что визуальное представление объектов может “дёргаться”, если частота рендеринга выше, чем частота обновления физики. Для плавной анимации используется интерполяция состояния (state interpolation).

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

  • Рассчитать дробное смещение между предыдущим и текущим состоянием объекта.
  • Применить линейную интерполяцию (LERP) для позиции и SLERP для ориентации (quaternion) на момент рендера.

Формулы для линейной интерполяции позиции:

renderPos = prevPos.clone().lerp(currentPos, alpha);

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

alpha = accumulator / timeStep;

Для ориентации объектов, заданной кватернионами:

renderQuat = prevQuat.clone().slerp(currentQuat, alpha);

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

Практическая структура кода с накоплением времени и интерполяцией

Полный рабочий цикл рендеринга и физики может выглядеть следующим образом:

const timeStep = 1 / 60;
let accumulator = 0;
let lastTime = performance.now() / 1000;

function update() {
    const currentTime = performance.now() / 1000;
    let deltaTime = currentTime - lastTime;
    lastTime = currentTime;

    accumulator += deltaTime;

    while (accumulator >= timeStep) {
        // Сохраняем текущее состояние для интерполяции
        world.bodies.forEach(body => {
            body.prevPosition.copy(body.position);
            body.prevQuaternion.copy(body.quaternion);
        });

        world.step(timeStep); 
        accumulator -= timeStep;
    }

    const alpha = accumulator / timeStep;

    // Интерполируем состояние для рендеринга
    world.bodies.forEach(body => {
        const renderPos = body.prevPosition.clone().lerp(body.position, alpha);
        const renderQuat = body.prevQuaternion.clone().slerp(body.quaternion, alpha);

        meshMap.get(body).position.copy(renderPos);
        meshMap.get(body).quaternion.copy(renderQuat);
    });

    renderer.render(scene, camera);
    requestAnimationFrame(update);
}

update();

Важные моменты

  • Фиксированный шаг симуляции: обеспечивает предсказуемость и стабильность физики.
  • Аккумулятор времени: предотвращает потерю симуляции при изменении частоты кадров.
  • Интерполяция состояния: плавность визуального движения при различной частоте рендеринга.
  • Сохранение предыдущего состояния: необходимое условие для корректной интерполяции позиции и ориентации.

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