В реальном времени физический движок и система рендеринга работают с
разной частотой обновления. В библиотеке Oimo.js шаг симуляции обычно
фиксированный (например, 60 Гц), тогда как частота отрисовки зависит от
requestAnimationFrame и может быть нестабильной. Это
приводит к визуальным артефактам:
Физический мир обновляется дискретно, а визуальная система ожидает плавного непрерывного движения. Для устранения этого несоответствия применяется интерполяция трансформаций.
В Oimo.js мир создаётся через объект World:
const world = new OIMO.World({
timestep: 1 / 60,
iterations: 8,
broadphase: 2,
worldscale: 1,
random: false,
info: false
});
Параметр timestep задаёт фиксированный шаг
интегрирования. При корректной архитектуре физика обновляется строго с
этим шагом, независимо от реального FPS:
let accumulator = 0;
const fixedTimeStep = 1 / 60;
function loop(deltaTime) {
accumulator += deltaTime;
while (accumulator >= fixedTimeStep) {
world.step();
accumulator -= fixedTimeStep;
}
}
Без дополнительной обработки визуальные объекты будут получать данные
только после каждого world.step(), что создаёт ступенчатое
движение.
Интерполяция — это вычисление промежуточного состояния между двумя дискретными состояниями физического тела:
state_previous — трансформация на предыдущем шаге;state_current — трансформация на текущем шаге;alpha — коэффициент интерполяции в диапазоне
[0, 1].Формула линейной интерполяции позиции:
[ P_{render} = P_{previous} (1 - ) + P_{current} ]
Где:
[ = ]
Таким образом, визуализация отображает состояние, соответствующее реальному времени между шагами физики.
Oimo.js не хранит автоматически историю трансформаций для рендеринга. Поэтому необходимо сохранять предыдущее положение и ориентацию вручную.
Пример структуры для хранения:
class PhysicsObject {
constructor(body) {
this.body = body;
this.previousPosition = new THREE.Vector3();
this.currentPosition = new THREE.Vector3();
this.previousQuaternion = new THREE.Quaternion();
this.currentQuaternion = new THREE.Quaternion();
}
updateFromPhysics() {
// Сохранение текущего состояния как предыдущего
this.previousPosition.copy(this.currentPosition);
this.previousQuaternion.copy(this.currentQuaternion);
// Получение нового состояния из Oimo
const pos = this.body.getPosition();
const quat = this.body.getQuaternion();
this.currentPosition.set(pos.x, pos.y, pos.z);
this.currentQuaternion.set(quat.x, quat.y, quat.z, quat.w);
}
}
Метод updateFromPhysics() вызывается сразу после
world.step().
Линейная интерполяция (LERP) подходит для позиций:
function interpolatePosition(obj, alpha, target) {
target.lerpVectors(
obj.previousPosition,
obj.currentPosition,
alpha
);
}
Где target — позиция визуального объекта (например,
mesh.position).
Преимущества линейной интерполяции:
Для вращений линейная интерполяция неприменима из-за особенностей кватернионов. Используется сферическая интерполяция — SLERP:
function interpolateRotation(obj, alpha, targetQuaternion) {
THREE.Quaternion.slerp(
obj.previousQuaternion,
obj.currentQuaternion,
targetQuaternion,
alpha
);
}
SLERP обеспечивает:
Полный цикл с интерполяцией:
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
accumulator += deltaTime;
while (accumulator >= fixedTimeStep) {
world.step();
physicsObjects.forEach(obj => {
obj.updateFromPhysics();
});
accumulator -= fixedTimeStep;
}
const alpha = accumulator / fixedTimeStep;
physicsObjects.forEach(obj => {
interpolatePosition(obj, alpha, obj.mesh.position);
interpolateRotation(obj, alpha, obj.mesh.quaternion);
});
renderer.render(scene, camera);
}
Такой подход обеспечивает визуально плавное движение даже при нестабильном FPS.
Существует альтернативный метод — экстраполяция, при которой вычисляется предполагаемое будущее положение на основе текущей скорости:
[ P_{future} = P_{current} + V dt]
Однако в физическом движке с коллизиями экстраполяция может вызывать:
Интерполяция использует уже подтверждённые состояния и не нарушает целостность симуляции.
В Oimo.js тела могут переходить в режим сна (sleep), если их скорость ниже порога. При пробуждении возможен резкий скачок трансформации.
Корректная стратегия:
previousState = currentState;body.sleeping.Это устраняет резкие визуальные скачки.
Если камера привязана к физическому телу (например, автомобилю), интерполяция должна выполняться до обновления положения камеры. Иначе камера будет следовать ступенчато.
Порядок действий:
alpha.При длительной работе возможно накопление ошибок в
accumulator. Рекомендуется:
accumulator = Math.min(accumulator, fixedTimeStep);
Это предотвращает чрезмерную интерполяцию при просадке FPS.
Также следует избегать использования переменного
timestep в world.step(), так как это:
Интерполяция добавляет вычислительную нагрузку:
Даже при сотнях объектов нагрузка остаётся минимальной по сравнению с расчётом коллизий.
Оптимизация возможна:
Float32Array.1. Интерполяция после рендера Нарушает временную последовательность и вызывает один кадр задержки.
2. Использование текущей позиции вместо предыдущей Приводит к отсутствию сглаживания.
3. Смешивание интерполяции и переменного шага Создаёт двойную компенсацию времени.
4. Неверная работа с кватернионами Попытка линейной интерполяции вращений вызывает артефакты.
В архитектурах на основе Entity Component System интерполяция обычно реализуется как отдельная система:
Такой подход обеспечивает чистое разделение логики симуляции и представления.
Корректная интерполяция трансформаций в Oimo.js:
Интерполяция является обязательным элементом профессиональной архитектуры реального времени при использовании дискретной физической симуляции.