Интеграция физических симуляций твердых тел (Rigid Body Dynamics) в веб-приложения исторически сталкивалась с серьезными вычислительными ограничениями. Первые движки (Cannon.js, Oimo.js) были написаны на чистом JavaScript: динамическая типизация V8, частые аллокации объектов и сборка мусора (Garbage Collection) приводили к нерегулярным паузам и падению FPS уже при сотне активных тел. Портирование движка Bullet Physics через Emscripten (Ammo.js) решило проблему скорости, но создало гигантский бинарный размер (более 2 МБ) и сложное низкоуровневое управление неуправляемой памятью C++.
Rapier представляет собой стандарт нового поколения для веб-физики. В связке с пакетом @react-three/rapier он переносит декларативную мощь React на физический конвейер WebAssembly.
┌─────────────────────────────────────────────────────────────────────────────┐
│ REACT COMPONENT TREE (React Fiber Reconciler) │
│ <Physics gravity={[0, -9.81, 0]} timeStep="vary"> │
│ <RigidBody ref={bodyRef} type="dynamic" colliders="ball"> │
│ <mesh><sphereGeometry /></mesh> │
│ </RigidBody> │
│ </Physics> │
└──────────────────────────────────────┬──────────────────────────────────────┘
│
▼ (Синхронизация через WASM мост)
┌─────────────────────────────────────────────────────────────────────────────┐
│ RAPIER PHYSICS PIPELINE (Rust / WebAssembly) │
│ 1. Broad Phase: BVH (Bounding Volume Hierarchy) & Sweep-and-Prune │
│ 2. Narrow Phase: GJK (Gilbert-Johnson-Keerthi) & EPA (Penetration Depth) │
│ 3. Island Management: Перевод неподвижных тел в состояние сна (Sleep Mode) │
│ 4. Solver: Итеративное решение ограничений импульсов (Velocity / Position) │
│ 5. WASM Linear Memory: Запись новых Transform-матриц напрямую в ArrayBuffer │
└──────────────────────────────────────┬──────────────────────────────────────┘
│
▼ (useFrame Loop: Прямая мутация матриц)
┌─────────────────────────────────────────────────────────────────────────────┐
│ THREE.JS SCENE GRAPH (GPU Rendering) │
│ mesh.matrixWorld.copy(wasmBodyMatrix); // Нулевые React ререндеры! │
└─────────────────────────────────────────────────────────────────────────────┘Физический цикл функционирует изолированно от графического рендера. Пока видеокарта вычисляет пиксели шейдеров на GPU, процессор CPU в изолированном модуле WASM рассчитывает дифференциальные уравнения движения Ньютона-Эйлера, обрабатывает контактные манифолды и разрешает взаимные проникновения геометрий.
Ментальная модель @react-three/rapier разделяет визуальное представление объекта (Three.js Mesh) и его математическую модель взаимодействия (RigidBody + Collider):
Three.js Mesh (Визуальная геометрия)
▲
│ (Синхронизация координат каждый тик)
▼
┌─────────────────────────────────────────┐
│ RigidBody (Твердое тело) │
│ - Масса, инерция, скорость, трение │
│ - Вектор линейной/угловой скорости │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Collider (Коллайдер) │ │
│ │ - Геометрическая форма (Hull) │ │
│ │ - Упругость (Restitution) │ │
│ │ - Трение скольжения (Friction) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘RigidBody: Описывает кинематические и динамические свойства: массу, центр тяжести, линейную скорость (linvel), угловую скорость (angvel) и линейное/угловое демпфирование (Damping).Collider: Определяет границы столкновений. Один RigidBody может быть составным (Compound) и объединять внутри себя несколько дочерних коллайдеров с разными формами и локальными смещениями.dynamic: Полноценное тело под действием гравитации и внешних сил.fixed: Статическое неподвижное препятствие с бесконечной массой (стены, пол).kinematicPositionBased / kinematicVelocityBased: Тело, управляемое кодом скрипта, которое игнорирует гравитацию, но сдвигает динамические объекты при столкновении.Контейнер Physics аккумулирует прошедшее время delta и разбивает его на фиксированные дискретные шаги timeStep (например, 1/60 с).
Быстрое исключение заведомо ненакладывающихся тел с помощью деревьев ограничивающих объемов (AABB / BVH).
Точный расчет точек контакта и глубины проникновения через алгоритмы GJK и EPA для оставшихся кандидатов.
Rapier копирует обновленные координаты (x, y, z) и кватернионы (qx, qy, qz, qw) напрямую в Three.js Object3D.
В интерактивном 3D-приложении создается двигающаяся платформа лифта, которая должна плавно перемещаться по синусоидальной траектории и при этом поднимать стоящие на ней динамические коробки. Какой тип RigidBody следует назначить платформе?
В инновационных интерфейсах карточки контента могут реагировать на броски курсора мыши, гравитационно падать в корзину или взаимодействовать по законам физики. Использование RigidBody type="dynamic" в комбинации с ограничением осей вращения enabledRotations={[false, false, true]} позволяет создавать физические слайдеры и колеса фортуны с кинематографической инерцией.
Создание реалистичной подвески автомобиля или роботизированного манипулятора строится на соединении звеньев шарнирами (useRevoluteJoint, usePrismaticJoint). Шарниры накладывают ограничения на степени свободы тел (Degrees of Freedom), вычисляя угловые ограничения, амортизацию пружин и пределы растяжения непосредственно в WASM-ядре.
В 3D-играх и виртуальных шоурумах определение точки клика или выстрела через Three.js Raycaster не учитывает физические коллайдеры. Использование world.castRay в Rapier выполняет пространственный запрос по оптимизированному дереву BVH в Rust за микросекунды, мгновенно возвращая точку соударения, вектор нормали поверхности и ссылку на физическое тело для приложения взрывного импульса (applyImpulseAtPoint).
При анимации падения персонажей или разрушения конструкций скелетная иерархия переключается из режима ключевых кадров в рэгдолл. Кости привязываются к цепочке динамических тел капсульной формы (capsule), соединенных сферическими шарнирами (useSphericalJoint), обеспечивая естественную реакцию тела на удары о ступени и препятствия.
Вычислительная сложность обнаружения столкновений критически зависит от формы коллайдера:
cuboid, ball, capsule): Самые быстрые. Расчет пересечения сфер сводится к элементарному сравнению расстояния между центрами с суммой радиусов.hull): Оптимальный компромисс для сложных мешей. Алгоритм берет крайние точки и «натягивает» выпуклый многогранник. Обрабатывается алгоритмом GJK (Gilbert-Johnson-Keerthi).trimesh): Максимально точная копия меша, состоящая из сотен треугольников. Категорически запрещена для dynamic-тел, так как не имеет объема и приводит к проваливанию объектов. Допустима исключительно для статических ландшафтов (type="fixed").Создадим сцену с гравитацией, интерактивным спавном динамических тел, шарнирным маятником и Raycast-взаимодействием.
import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import {
Physics,
RigidBody,
CuboidCollider,
useRapier,
RapierRigidBody,
useRevoluteJoint,
} from '@react-three/rapier';
import * as THREE from 'three';
// 1. Физический маятник с шарниром вращения (Revolute Joint)
function PhysicsPendulum() {
const anchorRef = useRef<RapierRigidBody>(null);
const bobRef = useRef<RapierRigidBody>(null);
// Соединение двух тел шарниром: точка вращения вокруг оси Z
useRevoluteJoint(anchorRef, bobRef, [
[0, 0, 0], // Опорная точка в анкерном теле
[0, 2.5, 0], // Опорная точка в подвешенном теле
[0, 0, 1], // Ось вращения (Z)
]);
return (
<group position={[0, 4, 0]}>
{/* Неподвижная точка крепления шарнира */}
<RigidBody ref={anchorRef} type="fixed">
<mesh>
<sphereGeometry args={[0.2, 16, 16]} />
<meshStandardMaterial color="#64748b" />
</mesh>
</RigidBody>
{/* Подвешенный физический груз */}
<RigidBody
ref={bobRef}
type="dynamic"
colliders="ball"
restitution={0.8}
linearDamping={0.1}
angularDamping={0.1}
>
<mesh position={[0, -2.5, 0]}>
<sphereGeometry args={[0.6, 32, 32]} />
<meshStandardMaterial color="#ef4444" roughness={0.3} />
</mesh>
</RigidBody>
</group>
);
}
// 2. Интерактивный эмиттер физических тел с импульсным кликом
function PhysicalShooter() {
const { rapier, world } = useRapier();
const boxBodyRef = useRef<RapierRigidBody>(null);
// Обработка физического импульса при клике на куб
const handlePointerDown = (event: any) => {
event.stopPropagation();
if (!boxBodyRef.current) return;
// Приложение вертикального и направленного импульса
boxBodyRef.current.applyImpulse(
{ x: (Math.random() - 0.5) * 8, y: 12, z: (Math.random() - 0.5) * 8 },
true // wakeUp: принудительно разбудить тело из режима сна
);
// Добавление вращательного крутящего момента (Torque)
boxBodyRef.current.applyTorqueImpulse(
{ x: Math.random() * 2, y: Math.random() * 2, z: Math.random() * 2 },
true
);
};
return (
<RigidBody
ref={boxBodyRef}
type="dynamic"
colliders="cuboid"
position={[2, 2, 0]}
restitution={0.6}
friction={0.4}
>
<mesh onClick={handlePointerDown} castShadow>
<boxGeometry args={[1.2, 1.2, 1.2]} />
<meshStandardMaterial color="#3b82f6" />
</mesh>
</RigidBody>
);
}
// 3. Кинематическая двигающаяся платформа
function KinematicElevator() {
const platformRef = useRef<RapierRigidBody>(null);
useFrame(({ clock }) => {
if (!platformRef.current) return;
const t = clock.getElapsedTime();
const targetY = 1 + Math.sin(t * 1.5) * 1.2;
// Плавное программное управление позицией с корректной передачей скоростей
platformRef.current.setNextKinematicTranslation({
x: -2.5,
y: targetY,
z: 0,
});
});
return (
<RigidBody ref={platformRef} type="kinematicPositionBased" colliders="cuboid">
<mesh receiveShadow>
<boxGeometry args={[2.5, 0.3, 2.5]} />
<meshStandardMaterial color="#10b981" />
</mesh>
</RigidBody>
);
}
// 4. Корневой компонент физического мира
export function RapierWorld() {
return (
<div className="w-full h-[600px] bg-slate-900 rounded-2xl overflow-hidden">
<Canvas shadows camera={{ position: [0, 5, 10], fov: 50 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 12, 5]} intensity={1.5} castShadow />
{/* Контейнер Physics с гравитацией Земли */}
<Physics gravity={[0, -9.81, 0]} timeStep="vary">
<PhysicsPendulum />
<PhysicalShooter />
<KinematicElevator />
{/* Статический пол сцены */}
<RigidBody type="fixed" friction={0.8} restitution={0.2}>
<mesh position={[0, -0.5, 0]} receiveShadow>
<boxGeometry args={[20, 1, 20]} />
<meshStandardMaterial color="#1e293b" />
</mesh>
</RigidBody>
</Physics>
</Canvas>
</div>
);
}Обновление векторов линейных и угловых скоростей под воздействием сил тяжести и приложенных импульсов.
Исполняется в 32-битных регистрах WASM SIMD.
Иерархическое дерево AABB быстро отбрасывает изолированные пары тел, снижая сложность с O(N^2) до O(N log N).
Позволяет симулировать до 5000 тел без фризов.
Проекционный метод Гаусса-Зейделя (Projected Gauss-Seidel) рассчитывает импульсы реакции опор, устраняя проникновения.
Выполняется от 4 до 8 итераций за шаг.
Тела с кинетической энергией ниже порога замораживаются (Sleeping), полностью исключаясь из вычислений физики.
Экономит до 80% времени CPU.
Маленький динамический шар падает с огромной скоростью и пролетает сквозь статическую стену пола толщиной 0.1 единицы, не зарегистрировав соударения. Каков физический механизм этой ошибки и как ее устранить?
Назначение <RigidBody type="dynamic" colliders="trimesh"> приводит к падению производительности и нестабильности. Trimesh представляет собой пустую оболочку из несвязанных треугольников без объема. При взаимном проникновении двух динамических сеток алгоритм не может однозначно вычислить глубину выталкивания, вызывая «взрывной» разлет объектов или их застревание.
Решение: Для динамических тел используйте только выпуклые формы: аналитические примитивы (cuboid, ball, capsule) или выпуклую оболочку colliders="hull". trimesh допустим исключительно для type="fixed".
Попытка переместить объект через meshRef.current.position.x += 1 внутри физического мира приведет к мгновенному рассинхрону. На следующем шаге симуляции Rapier перепишет координаты меша значениями из своей внутренней памяти WASM, отменив пользовательское изменение.
Решение: Перемещайте динамические тела импульсами (applyImpulse) или скоростями (setLinvel), а кинематические тела — через метод setNextKinematicTranslation({ x, y, z }).
Если изменить масштаб трехмерного узла <mesh scale={[2, 2, 2]}> внутри уже инициализированного <RigidBody>, визуальная модель увеличится вдвое, однако физический коллайдер в WASM останется исходного единичного размера.
Решение: Коллайдеры в Rapier не масштабируются динамически в рантайме. При необходимости изменения размеров пересоздавайте коллайдер с уникальным key либо управляйте масштабом до инициализации физического тела.
Когда тело неподвижно лежит на поверхности несколько секунд, движок переводит его в состояние сна (Sleeping) для экономии процессорного времени. Вызов applyImpulse без параметра wakeUp=true не разбудит тело, и импульс будет проигнорирован.
Решение: Всегда передавайте true вторым аргументом: bodyRef.current.applyImpulse(vec, true).
@react-three/rapier разделяет визуальную модель (<mesh>) и физическую сущность (<RigidBody> + <Collider>).fixed с геометрией trimesh, в то время как для подвижных динамических тел допустимы исключительно выпуклые формы (cuboid, ball, capsule, hull).kinematicPositionBased с обновлением через setNextKinematicTranslation для корректной передачи скоростей стоящим объектам.ccd={true} (Continuous Collision Detection).