Oimo.js использует фиксированный шаг симуляции для интеграции
физических тел в мире. Основная идея заключается в том, что физический
движок должен получать равные интервалы времени для расчёта,
чтобы избежать нестабильности и неравномерного поведения объектов при
изменении частоты кадров. Поскольку реальные игры и анимации могут иметь
переменную частоту обновления (deltaTime), необходимо
аккумулировать время между кадрами.
В Oimo.js применяется концепция накопления времени (time accumulator). Принцип работы следующий:
deltaTime
между текущим и предыдущим кадром.timeStep, физический мир обновляется на один шаг, а
аккумулятор уменьшается на timeStep.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).
Идея интерполяции:
Формулы для линейной интерполяции позиции:
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.