关于 threejs-impl-physics
threejs-impl-physics 是面向 Three.js 开发者的物理实现参考技能,介绍 cannon-es 与 Rapier 的选择、刚体和碰撞器设置及场景同步。
主要功能
- 01对比 cannon-es 与 Rapier
- 02配置刚体、碰撞器和约束
- 03处理物理步进与碰撞事件
- 04同步物理状态和 Three.js 网格
- 05介绍 React Three Fiber 集成
- 06提供射线检测与调试渲染示例
适用场景
- 01为 Three.js 场景添加重力与碰撞
- 02选择适合场景需求的物理引擎
- 03在 React Three Fiber 中配置物理对象
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-physics安装后在 Claude Code 中输入/threejs-impl-physics确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitkeepreferences/anti-patterns.mdexamples.mdmethods.md详细介绍
threejs-impl-physics
快速参考
引擎选择决策树
| 标准 | cannon-es | Rapier |
|---|---|---|
| 场景规模 | < 100 个刚体 | 100–10,000+ 个刚体 |
| 是否需要确定性 | 否 | 是(跨平台) |
| CCD(快速物体) | 支持有限 | 完全支持 |
| 包体积预算 | ~100 KB | ~300–600 KB(WASM) |
| 初始化 | 同步 | 异步(必须 await init()) |
| 原型开发速度 | 更快(API 更简单) | 更慢(构建器模式) |
| R3F 集成 | @react-three/cannon | @react-three/rapier |
**规则:**对于要求确定性、CCD 或刚体数量 > 100 的生产应用,始终使用 Rapier。原型开发和简单场景使用 cannon-es。
重要警告
绝不要忘记在动画循环中调用 world.step()——否则物理刚体不会移动。
绝不要在 Rapier 中使用 mesh.position.copy(body.position)——Rapier 返回的是普通的 {x, y, z} 对象,而不是 CANNON.Vec3。始终使用 mesh.position.set(pos.x, pos.y, pos.z)。
绝不要在 await RAPIER.init() 完成前使用 Rapier API——WASM 加载完成前,所有 Rapier 类都未定义。
绝不要在动态刚体上使用 Trimesh / trimesh 碰撞体——在这两种引擎中,三角网格都仅支持静态刚体。对于动态凹面几何体,请使用 ConvexPolyhedron / convexHull。
绝不要创建没有对应 Three.js 网格的物理刚体,除非是有意创建不可见碰撞体——孤立的刚体会浪费模拟预算。
cannon-es
世界设置
import * as CANNON from 'cannon-es';
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.solver.iterations = 10;
world.allowSleep = true;
始终设置 world.allowSleep = true——休眠刚体会跳过模拟,大幅提升性能。
对于刚体数量 > 20 的场景,始终使用 SAPBroadphase。NaiveBroadphase 的复杂度为 O(n^2)。
刚体类型
| 类型 | 质量 | 行为 |
|---|---|---|
CANNON.Body.DYNAMIC | > 0 | 受力和碰撞影响 |
CANNON.Body.STATIC | 0 | 不可移动,质量无限 |
CANNON.Body.KINEMATIC | 0 | 通过程序移动,并推动动态刚体 |
const body = new CANNON.Body({
mass: 5,
position: new CANNON.Vec3(0, 10, 0),
shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)), // half-extents
linearDamping: 0.01,
angularDamping: 0.01,
});
world.addBody(body);
形状类型
| 形状 | 构造函数 | 备注 |
|---|---|---|
Box | new CANNON.Box(halfExtents: Vec3) | 轴对齐盒体 |
Sphere | new CANNON.Sphere(radius) | 开销最低的碰撞形状 |
Cylinder | new CANNON.Cylinder(rTop, rBottom, height, segments) | 圆柱体 |
Plane | new CANNON.Plane() | 无限地面 |
ConvexPolyhedron | new CANNON.ConvexPolyhedron({vertices, faces}) | 自定义凸包 |
Trimesh | new CANNON.Trimesh(vertices, indices) | 仅支持静态刚体 |
Heightfield | new CANNON.Heightfield(data, {elementSize}) | 地形 |
Particle | new CANNON.Particle() | 点粒子 |
材质和接触
const groundMat = new CANNON.Material('ground');
const ballMat = new CANNON.Material('ball');
const contact = new CANNON.ContactMaterial(groundMat, ballMat, {
friction: 0.4,
restitution: 0.6,
});
world.addContactMaterial(contact);
// Assign materials to bodies
groundBody.material = groundMat;
ballBody.material = ballMat;
创建 ContactMaterial 后,始终为刚体指定材质——否则 ContactMaterial 不会生效。
约束
| 约束 | 构造函数 | 用例 |
|---|---|---|
PointToPointConstraint | (bodyA, pivotA, bodyB, pivotB) | 球关节 |
DistanceConstraint | (bodyA, bodyB, distance) | 固定距离杆 |
HingeConstraint | (bodyA, bodyB, {pivotA, axisA, pivotB, axisB}) | 门铰链 |
LockConstraint | (bodyA, bodyB) | 刚性锁定 |
ConeTwistConstraint | (bodyA, bodyB, options) | 布娃娃关节 |
Spring | (bodyA, bodyB, options) | 阻尼弹簧 |
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
pivotA: new CANNON.Vec3(0, 0, 0),
axisA: new CANNON.Vec3(0, 1, 0),
pivotB: new CANNON.Vec3(-2, 0, 0),
axisB: new CANNON.Vec3(0, 1, 0),
});
world.addConstraint(hinge);
事件
body.addEventListener('collide', (event) => {
const { contact } = event;
// contact.ni = contact normal
// contact.ri = contact point relative to bodyA
// contact.rj = contact point relative to bodyB
});
body.addEventListener('sleep', () => { /* body went to sleep */ });
body.addEventListener('wakeup', () => { /* body woke up */ });
步进
// ALWAYS use three-argument step for deterministic simulation
const fixedTimeStep = 1 / 60;
const maxSubSteps = 3;
function animate() {
const delta = clock.getDelta();
world.step(fixedTimeStep, delta, maxSubSteps);
}
Rapier
WASM 初始化
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init(); // MUST await — ALL APIs undefined before this resolves
const gravity = { x: 0.0, y: -9.81, z: 0.0 };
const world = new RAPIER.World(gravity);
始终在异步函数或顶层 await 中使用 Rapier。在 init() 完成前调用任何 Rapier 构造函数都会抛出运行时错误。
创建 RigidBody(构建器模式)
// Dynamic body
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(0, 10, 0)
.setLinvel(0, 0, 0)
.setAngvel({ x: 0, y: 0, z: 0 })
.setLinearDamping(0.01)
.setAngularDamping(0.01)
.setCcdEnabled(true);
const rigidBody = world.createRigidBody(bodyDesc);
// Static body
const staticDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(0, 0, 0);
const staticBody = world.createRigidBody(staticDesc);
// Kinematic (position-based)
const kinDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
const kinBody = world.createRigidBody(kinDesc);
// Kinematic (velocity-based)
const kinVelDesc = RAPIER.RigidBodyDesc.kinematicVelocityBased();
碰撞体形状
const colliderDesc = RAPIER.ColliderDesc.cuboid(1, 1, 1)
.setRestitution(0.5)
.setFriction(0.7);
world.createCollider(colliderDesc, rigidBody);
| 形状 | 构造函数 | 说明 |
|---|---|---|
ColliderDesc.cuboid(hx, hy, hz) | 长方体半尺寸 | 最常见 |
ColliderDesc.ball(radius) | 球体 | 计算开销最低的形状 |
ColliderDesc.capsule(halfHeight, radius) | 胶囊体 | 适用于角色 |
ColliderDesc.cylinder(halfHeight, radius) | 圆柱体 | |
ColliderDesc.cone(halfHeight, radius) | 圆锥体 | |
ColliderDesc.convexHull(vertices) | 凸包 | 来自 Float32Array |
ColliderDesc.trimesh(vertices, indices) | 三角网格 | 仅限静态物体 |
ColliderDesc.heightfield(nrows, ncols, heights, scale) | 地形 | |
ColliderDesc.roundCuboid(hx, hy, hz, borderRadius) | 圆角长方体 |
射线投射
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 });
const hit = world.castRay(ray, 100, true);
if (hit) {
const hitPoint = ray.pointAt(hit.timeOfImpact);
const hitCollider = world.getCollider(hit.colliderHandle);
}
CCD(连续碰撞检测)
对于快速移动的物体,务必启用 CCD,以防其穿透薄几何体:
const desc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true);
碰撞事件
const eventQueue = new RAPIER.EventQueue(true);
world.step(eventQueue);
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
// started === true: collision began
// started === false: collision ended
});
eventQueue.drainContactForceEvents((event) => {
const force = event.totalForceMagnitude();
});
调试渲染
const { vertices, colors } = world.debugRender();
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4));
const material = new THREE.LineBasicMaterial({ vertexColors: true });
const lines = new THREE.LineSegments(geometry, material);
scene.add(lines);
Three.js 同步模式
cannon-es 同步
// Store body-mesh pairs
const pairs = [];
function addPhysicsObject(mesh, body) {
scene.add(mesh);
world.addBody(body);
pairs.push({ mesh, body });
}
function updatePhysics(delta) {
world.step(1 / 60, delta, 3);
for (const { mesh, body } of pairs) {
mesh.position.copy(body.position); // Vec3 → Vector3 (compatible)
mesh.quaternion.copy(body.quaternion); // Quaternion → Quaternion (compatible)
}
}
cannon-es 的 Vec3 和 Quaternion 与 Three.js 的 .copy() 直接兼容。
Rapier 同步
const pairs = [];
function addPhysicsObject(mesh, rigidBody) {
scene.add(mesh);
pairs.push({ mesh, rigidBody });
}
function updatePhysics() {
world.step();
for (const { mesh, rigidBody } of pairs) {
const pos = rigidBody.translation(); // returns {x, y, z}
const rot = rigidBody.rotation(); // returns {x, y, z, w}
mesh.position.set(pos.x, pos.y, pos.z);
mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w);
}
}
切勿对 Rapier 返回值使用 .copy() —— 它们是普通对象,并非 Three.js 类型。
React Three Fiber 集成
@react-three/rapier
import { Physics, RigidBody } from '@react-three/rapier';
function Scene() {
return (
<Physics gravity={[0, -9.81, 0]}>
<RigidBody type="fixed">
<mesh position={[0, -1, 0]}>
<boxGeometry args={[20, 1, 20]} />
<meshStandardMaterial />
</mesh>
</RigidBody>
<RigidBody>
<mesh position={[0, 5, 0]}>
<sphereGeometry args={[1]} />
<meshStandardMaterial />
</mesh>
</RigidBody>
</Physics>
);
}
@react-three/cannon
import { Physics, useBox, useSphere } from '@react-three/cannon';
function Floor() {
const [ref] = useBox(() => ({ mass: 0, args: [20, 1, 20], position: [0, -1, 0] }));
return (
<mesh ref={ref}>
<boxGeometry args={[20, 1, 20]} />
<meshStandardMaterial />
</mesh>
);
}
性能比较
| 特性 | cannon-es | Rapier |
|---|---|---|
| 语言 | JavaScript | 编译为 WASM 的 Rust |
| 实时物体数量 | ~1,000 | ~10,000+ |
| 包体积 | ~100 KB | ~300–600 KB |
| CCD | 有限支持 | 全面支持 |
| 确定性 | 否 | 是(跨平台) |
| 调试渲染 | 手动 | 内置 world.debugRender() |
| API 风格 | 基于构造函数 | 构建器模式 |
参考链接
- references/methods.md — cannon-es 和 Rapier 的 API 签名
- references/examples.md — 可运行的集成示例
- references/anti-patterns.md — 常见物理错误及修复方法