Библиотека Oimo.js реализует физический движок на
JavaScript, обеспечивая обработку столкновений, динамику твердых тел и
констрейнтов. Для корректного моделирования физики важно отделять шаги
симуляции от частоты отрисовки, особенно когда используется
requestAnimationFrame, частота которого зависит от
производительности устройства и может быть нестабильной.
Важный принцип — фиксированный шаг симуляции. Независимо от того, как часто вызывается рендер, физика должна продвигаться равномерно, чтобы движение объектов оставалось предсказуемым и стабильным.
Физический мир Oimo.js обновляется методом
world.step(timeStep), где timeStep задает
интервал симуляции. Для стабильной работы часто используют шаг
1/60 секунды (≈16.6667 мс), что соответствует частоте
60 FPS.
const timeStep = 1 / 60; // 60 кадров в секунду
Использование переменного времени между кадрами может привести к:
Для интеграции Oimo.js с requestAnimationFrame
используется аккумулятор времени. Основная идея:
суммировать прошедшее время и выполнять шаги симуляции с фиксированным
timeStep, пока накопившееся время не станет меньше одного
шага.
let previousTime = performance.now();
let accumulator = 0;
function animate(currentTime) {
const deltaTime = (currentTime - previousTime) / 1000; // в секундах
previousTime = currentTime;
accumulator += deltaTime;
while (accumulator >= timeStep) {
world.step(timeStep); // фиксированный шаг
accumulator -= timeStep;
}
render(); // функция рендера
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Пояснения:
deltaTime измеряет реальное время между кадрами.accumulator хранит накопленное время, которое нужно
«проиграть» в симуляции.while позволяет выполнять несколько шагов физики,
если deltaTime оказался больше одного шага.Даже при фиксированном шаге физики визуализация может дергаться, если частота рендера не совпадает с частотой симуляции. Для сглаживания используют интерполяцию между последними состояниями тел:
const alpha = accumulator / timeStep;
for (const body of world.bodies) {
const interpolatedPosition = body.previousPosition.clone().lerp(body.position, alpha);
const interpolatedQuaternion = body.previousQuaternion.clone().slerp(body.quaternion, alpha);
drawMesh(body.mesh, interpolatedPosition, interpolatedQuaternion);
}
body.previousPosition и
body.previousQuaternion сохраняются после каждого шага
симуляции.lerp и slerp позволяют плавно
интерполировать позицию и ориентацию.alpha — доля прошедшего времени текущего кадра
относительно шага физики.При сложных сценах фиксированный шаг может вызвать накопление «лишних» шагов, если рендер не успевает за физикой. Для контроля вводят максимальное количество шагов за кадр:
const maxSteps = 5;
let steps = 0;
while (accumulator >= timeStep && steps < maxSteps) {
world.step(timeStep);
accumulator -= timeStep;
steps++;
}
Если накопление времени слишком большое, физика будет продвигаться не
более maxSteps шагов, чтобы избежать резких «рывков»
сцены.
const world = new OIMO.World({
timestep: 1/60,
iterations: 8,
broadphase: 2, // 1: brute-force, 2: sweep & prune
worldscale: 1,
random: true,
info: false
});
let previousTime = performance.now();
let accumulator = 0;
function animate(currentTime) {
const deltaTime = (currentTime - previousTime) / 1000;
previousTime = currentTime;
accumulator += deltaTime;
const maxSteps = 5;
let steps = 0;
while (accumulator >= timeStep && steps < maxSteps) {
world.step(timeStep);
accumulator -= timeStep;
steps++;
}
const alpha = accumulator / timeStep;
for (const body of world.bodies) {
const pos = body.previousPosition.clone().lerp(body.position, alpha);
const quat = body.previousQuaternion.clone().slerp(body.quaternion, alpha);
drawMesh(body.mesh, pos, quat);
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
performance.now() вместо
Date.now() повышает точность измерений времени.Эта организация цикла позволяет эффективно использовать Oimo.js в реальном времени, сохраняя физическую точность и визуальную плавность, независимо от нестабильности частоты кадров.