
Threejs Impl Physics
- 18 installs
- 11 repo stars
- Updated July 8, 2026
- openaec-foundation/three.js-claude-skill-package
Helps with ai & agent building tasks.
About
threejs-impl-physics is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- threejs-impl-physics
- AI & Agent Building
- AI-coding skill
Threejs Impl Physics by the numbers
- 18 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #10,719 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/three.js-claude-skill-package --skill threejs-impl-physicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/three.js-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
threejs-impl-physics
Quick Reference
Engine Selection Decision Tree
| Criterion | cannon-es | Rapier |
|---|---|---|
| Scene size | < 100 bodies | 100–10,000+ bodies |
| Determinism needed | No | Yes (cross-platform) |
| CCD (fast objects) | Limited | Full support |
| Bundle size budget | ~100 KB | ~300–600 KB (WASM) |
| Initialization | Synchronous | Async (MUST await init()) |
| Prototyping speed | Faster (simpler API) | Slower (builder pattern) |
| R3F integration | @react-three/cannon | @react-three/rapier |
Rule: ALWAYS use Rapier for production applications requiring determinism, CCD, or > 100 bodies. Use cannon-es for prototyping and simple scenes.
Critical Warnings
NEVER forget to call world.step() in the animation loop — physics bodies will NOT move without it.
NEVER use mesh.position.copy(body.position) with Rapier — Rapier returns plain {x, y, z} objects, NOT CANNON.Vec3. ALWAYS use mesh.position.set(pos.x, pos.y, pos.z).
NEVER use Rapier APIs before await RAPIER.init() completes — ALL Rapier classes are undefined until WASM loads.
NEVER use Trimesh / trimesh colliders on dynamic bodies — triangle meshes are STATIC only in both engines. Use ConvexPolyhedron / convexHull for dynamic concave geometry.
NEVER create physics bodies without corresponding Three.js meshes unless intentionally creating invisible colliders — orphaned bodies waste simulation budget.
---
cannon-es
World Setup
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;ALWAYS set world.allowSleep = true — sleeping bodies skip simulation and dramatically improve performance.
ALWAYS use SAPBroadphase for scenes with > 20 bodies. NaiveBroadphase is O(n^2).
Body Types
| Type | Mass | Behavior |
|---|---|---|
CANNON.Body.DYNAMIC | > 0 | Affected by forces and collisions |
CANNON.Body.STATIC | 0 | Immovable, infinite mass |
CANNON.Body.KINEMATIC | 0 | Moved programmatically, pushes dynamic bodies |
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);Shape Types
| Shape | Constructor | Notes |
|---|---|---|
Box | new CANNON.Box(halfExtents: Vec3) | Axis-aligned box |
Sphere | new CANNON.Sphere(radius) | Cheapest collision shape |
Cylinder | new CANNON.Cylinder(rTop, rBottom, height, segments) | Cylinder |
Plane | new CANNON.Plane() | Infinite ground plane |
ConvexPolyhedron | new CANNON.ConvexPolyhedron({vertices, faces}) | Custom convex hull |
Trimesh | new CANNON.Trimesh(vertices, indices) | STATIC only |
Heightfield | new CANNON.Heightfield(data, {elementSize}) | Terrain |
Particle | new CANNON.Particle() | Point particle |
Materials and Contacts
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;ALWAYS assign materials to bodies after creating ContactMaterial — without assignment, the ContactMaterial has NO effect.
Constraints
| Constraint | Constructor | Use Case |
|---|---|---|
PointToPointConstraint | (bodyA, pivotA, bodyB, pivotB) | Ball joint |
DistanceConstraint | (bodyA, bodyB, distance) | Fixed distance rod |
HingeConstraint | (bodyA, bodyB, {pivotA, axisA, pivotB, axisB}) | Door hinge |
LockConstraint | (bodyA, bodyB) | Rigid lock |
ConeTwistConstraint | (bodyA, bodyB, options) | Ragdoll joints |
Spring | (bodyA, bodyB, options) | Damped spring |
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);Events
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 */ });Stepping
// 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 Initialization
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);ALWAYS wrap Rapier usage in an async function or top-level await. Calling ANY Rapier constructor before init() resolves throws a runtime error.
RigidBody Creation (Builder Pattern)
// 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();Collider Shapes
const colliderDesc = RAPIER.ColliderDesc.cuboid(1, 1, 1)
.setRestitution(0.5)
.setFriction(0.7);
world.createCollider(colliderDesc, rigidBody);| Shape | Constructor | Notes |
|---|---|---|
ColliderDesc.cuboid(hx, hy, hz) | Box half-extents | Most common |
ColliderDesc.ball(radius) | Sphere | Cheapest shape |
ColliderDesc.capsule(halfHeight, radius) | Capsule | Good for characters |
ColliderDesc.cylinder(halfHeight, radius) | Cylinder | |
ColliderDesc.cone(halfHeight, radius) | Cone | |
ColliderDesc.convexHull(vertices) | Convex hull | From Float32Array |
ColliderDesc.trimesh(vertices, indices) | Triangle mesh | STATIC only |
ColliderDesc.heightfield(nrows, ncols, heights, scale) | Terrain | |
ColliderDesc.roundCuboid(hx, hy, hz, borderRadius) | Rounded box |
Ray Casting
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 (Continuous Collision Detection)
ALWAYS enable CCD on fast-moving bodies to prevent tunneling through thin geometry:
const desc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true);Collision Events
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();
});Debug Rendering
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 Sync Pattern
cannon-es Sync
// 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 and Quaternion are directly compatible with Three.js .copy().
Rapier Sync
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);
}
}NEVER use .copy() with Rapier return values — they are plain objects, NOT Three.js types.
---
React Three Fiber Integration
@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>
);
}---
Performance Comparison
| Feature | cannon-es | Rapier |
|---|---|---|
| Language | JavaScript | Rust compiled to WASM |
| Real-time bodies | ~1,000 | ~10,000+ |
| Bundle size | ~100 KB | ~300–600 KB |
| CCD | Limited | Full support |
| Deterministic | No | Yes (cross-platform) |
| Debug rendering | Manual | Built-in world.debugRender() |
| API style | Constructor-based | Builder pattern |
---
Reference Links
- references/methods.md — API signatures for cannon-es and Rapier
- references/examples.md — Working integration examples
- references/anti-patterns.md — Common physics mistakes and fixes
Official Sources
- https://pmndrs.github.io/cannon-es/docs/
- https://rapier.rs/docs/user_guides/javascript/getting_started
- https://github.com/pmndrs/react-three-rapier
- https://github.com/pmndrs/use-cannon
threejs-impl-physics — Anti-Patterns
Anti-Pattern 1: Forgetting world.step() in the Animation Loop
Wrong:
function animate() {
requestAnimationFrame(animate);
// world.step() is missing!
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
renderer.render(scene, camera);
}Why it fails: Without calling world.step(), the physics simulation NEVER advances. Bodies remain frozen at their initial positions regardless of gravity or applied forces.
Correct:
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
world.step(1 / 60, delta, 3); // ALWAYS step the world
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
renderer.render(scene, camera);
}---
Anti-Pattern 2: Using .copy() with Rapier Return Values
Wrong:
// Rapier sync
const pos = rigidBody.translation();
mesh.position.copy(pos); // FAILS — pos is {x, y, z}, not a Vector3Why it fails: Rapier translation() and rotation() return plain JavaScript objects {x, y, z} and {x, y, z, w}. Three.js .copy() expects objects with a .copy() method or matching class instances. This may silently fail or throw errors depending on the Three.js version.
Correct:
const pos = rigidBody.translation();
const rot = rigidBody.rotation();
mesh.position.set(pos.x, pos.y, pos.z);
mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w);Note: cannon-es Vec3 and Quaternion ARE compatible with Three.js .copy() — this anti-pattern applies ONLY to Rapier.
---
Anti-Pattern 3: Using Rapier APIs Before await RAPIER.init()
Wrong:
import RAPIER from '@dimforge/rapier3d-compat';
// Calling immediately without init
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 }); // THROWS: WASM not loadedWhy it fails: Rapier is a Rust library compiled to WASM. The WASM binary MUST be loaded and initialized before ANY Rapier class is available. Accessing constructors before init() resolves causes runtime errors.
Correct:
import RAPIER from '@dimforge/rapier3d-compat';
async function main() {
await RAPIER.init(); // MUST await first
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// Now safe to use all Rapier APIs
}
main();---
Anti-Pattern 4: Using Trimesh on Dynamic Bodies
Wrong:
// cannon-es
const body = new CANNON.Body({
mass: 5, // dynamic
shape: new CANNON.Trimesh(vertices, indices), // BROKEN — Trimesh is static only
});
// Rapier
const desc = RAPIER.RigidBodyDesc.dynamic();
const body = world.createRigidBody(desc);
world.createCollider(RAPIER.ColliderDesc.trimesh(vertices, indices), body); // BROKENWhy it fails: Triangle mesh collision detection in both cannon-es and Rapier is designed ONLY for static geometry. Dynamic trimesh colliders produce incorrect collision responses, tunneling, and undefined behavior. The engines do NOT support mesh-mesh dynamic collision.
Correct:
// Use convex hull for dynamic bodies
// cannon-es
const body = new CANNON.Body({
mass: 5,
shape: new CANNON.ConvexPolyhedron({ vertices: convexVerts, faces: convexFaces }),
});
// Rapier
const desc = RAPIER.ColliderDesc.convexHull(new Float32Array(flatVertices));ALWAYS decompose concave dynamic geometry into convex parts using libraries like v-hacd or manual decomposition.
---
Anti-Pattern 5: Wrong Half-Extents for Box Shapes
Wrong:
// Three.js box is 2x2x2 (full size)
const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2));
// cannon-es — passing full size instead of half-extents
const shape = new CANNON.Box(new CANNON.Vec3(2, 2, 2)); // WRONG — this creates a 4x4x4 box
// Rapier — same mistake
const collider = RAPIER.ColliderDesc.cuboid(2, 2, 2); // WRONG — 4x4x4Why it fails: Both cannon-es Box and Rapier cuboid use HALF-EXTENTS, not full dimensions. A BoxGeometry(2, 2, 2) has half-extents of (1, 1, 1). Passing full sizes doubles the physics collider, causing objects to collide in empty space.
Correct:
const width = 2, height = 2, depth = 2;
const mesh = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth));
// cannon-es — half-extents
const shape = new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2));
// Rapier — half-extents
const collider = RAPIER.ColliderDesc.cuboid(width / 2, height / 2, depth / 2);---
Anti-Pattern 6: Variable Timestep Physics
Wrong:
function animate() {
const delta = clock.getDelta();
world.step(delta); // WRONG — variable timestep causes non-deterministic simulation
}Why it fails: Using delta directly as the timestep produces different simulation results at different frame rates. Objects may tunnel through walls at low FPS or behave differently on fast vs slow machines.
Correct (cannon-es):
function animate() {
const delta = clock.getDelta();
world.step(1 / 60, delta, 3); // fixed step with interpolation
}Correct (Rapier):
// Rapier uses a fixed internal timestep by default (1/60)
world.step(); // ALWAYS call without arguments for consistent behaviorALWAYS use a fixed timestep. cannon-es achieves this via the three-argument world.step(). Rapier uses a fixed timestep internally by default.
---
Anti-Pattern 7: Not Assigning Materials to Bodies
Wrong:
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);
// Materials never assigned to bodies — ContactMaterial has NO effect
const ground = new CANNON.Body({ mass: 0, shape: new CANNON.Plane() });
const ball = new CANNON.Body({ mass: 1, shape: new CANNON.Sphere(1) });Why it fails: Creating a ContactMaterial only defines rules for interactions between two Material instances. Unless the Material is explicitly assigned to each Body via the .material property, the engine uses default material settings and the ContactMaterial is ignored.
Correct:
ground.material = groundMat;
ball.material = ballMat;---
Anti-Pattern 8: Not Disposing Rapier WASM Resources
Wrong:
// Switching scenes, removing physics
function cleanupPhysics() {
// Just setting world to null — WASM memory leaks
world = null;
}Why it fails: Rapier objects live in WASM linear memory, which is NOT managed by JavaScript's garbage collector. Failing to call .free() on the world and event queue causes permanent memory leaks that persist until page reload.
Correct:
function cleanupPhysics() {
if (eventQueue) {
eventQueue.free();
eventQueue = null;
}
if (world) {
world.free();
world = null;
}
}ALWAYS call .free() on Rapier World and EventQueue objects when they are no longer needed.
---
Anti-Pattern 9: Ignoring Sleep for Static Scenes
Wrong:
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
// allowSleep defaults to false — ALL bodies simulated every frameWhy it fails: Without sleep, every body in the world is simulated every physics step, even if it has come to rest. In a scene with hundreds of stacked boxes that have settled, this wastes the majority of the physics budget on bodies that are not moving.
Correct:
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.allowSleep = true; // bodies at rest stop being simulatedALWAYS enable sleep in cannon-es. Rapier enables sleep by default.
---
Anti-Pattern 10: Creating Physics Ground with Wrong Orientation
Wrong (cannon-es):
const ground = new CANNON.Body({
mass: 0,
shape: new CANNON.Plane(),
// No rotation — Plane faces +Z by default, not +Y
});
world.addBody(ground);Why it fails: CANNON.Plane faces the local +Z axis by default. Without rotating it, the ground plane is vertical (like a wall), and objects fall through the intended ground position.
Correct:
const ground = new CANNON.Body({
mass: 0,
shape: new CANNON.Plane(),
});
ground.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // rotate to face +Y (up)
world.addBody(ground);ALWAYS rotate CANNON.Plane by -Math.PI / 2 around X to create a horizontal ground.
threejs-impl-physics — Examples
Example 1: cannon-es — Falling Boxes Scene
Complete scene with a static ground plane and dynamic falling boxes.
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// Three.js setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 10, 20);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Lighting
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;
scene.add(dirLight);
scene.add(new THREE.AmbientLight(0x404040, 0.5));
// Physics world
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.allowSleep = true;
// Contact material
const defaultMat = new CANNON.Material('default');
const defaultContact = new CANNON.ContactMaterial(defaultMat, defaultMat, {
friction: 0.3,
restitution: 0.4,
});
world.addContactMaterial(defaultContact);
// Ground
const groundBody = new CANNON.Body({
mass: 0, // static
shape: new CANNON.Plane(),
material: defaultMat,
});
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
const groundMesh = new THREE.Mesh(
new THREE.PlaneGeometry(50, 50),
new THREE.MeshStandardMaterial({ color: 0x808080 }),
);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
// Dynamic boxes
const pairs = [];
for (let i = 0; i < 20; i++) {
const size = 0.5 + Math.random() * 0.5;
const halfSize = size / 2;
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(
(Math.random() - 0.5) * 4,
5 + i * 2,
(Math.random() - 0.5) * 4,
),
shape: new CANNON.Box(new CANNON.Vec3(halfSize, halfSize, halfSize)),
material: defaultMat,
});
world.addBody(body);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(size, size, size),
new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }),
);
mesh.castShadow = true;
scene.add(mesh);
pairs.push({ mesh, body });
}
// Animation loop
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
world.step(1 / 60, delta, 3);
for (const { mesh, body } of pairs) {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
}
renderer.render(scene, camera);
}
animate();---
Example 2: Rapier — Sphere Drop with Ray Casting
Complete scene using Rapier WASM with ray-based ground detection.
import * as THREE from 'three';
import RAPIER from '@dimforge/rapier3d-compat';
async function main() {
await RAPIER.init();
// Three.js setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 8, 15);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(new THREE.DirectionalLight(0xffffff, 1));
scene.add(new THREE.AmbientLight(0x404040, 0.5));
// Physics world
const gravity = { x: 0.0, y: -9.81, z: 0.0 };
const world = new RAPIER.World(gravity);
// Ground (fixed body)
const groundDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(0, -0.5, 0);
const groundBody = world.createRigidBody(groundDesc);
const groundCollider = RAPIER.ColliderDesc.cuboid(25, 0.5, 25)
.setRestitution(0.3)
.setFriction(0.8);
world.createCollider(groundCollider, groundBody);
const groundMesh = new THREE.Mesh(
new THREE.BoxGeometry(50, 1, 50),
new THREE.MeshStandardMaterial({ color: 0x808080 }),
);
groundMesh.position.set(0, -0.5, 0);
scene.add(groundMesh);
// Spheres
const pairs = [];
for (let i = 0; i < 50; i++) {
const radius = 0.3 + Math.random() * 0.3;
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(
(Math.random() - 0.5) * 6,
5 + i * 1.5,
(Math.random() - 0.5) * 6,
)
.setCcdEnabled(true);
const rigidBody = world.createRigidBody(bodyDesc);
const colliderDesc = RAPIER.ColliderDesc.ball(radius)
.setRestitution(0.7)
.setFriction(0.5);
world.createCollider(colliderDesc, rigidBody);
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(radius, 16, 16),
new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }),
);
scene.add(mesh);
pairs.push({ mesh, rigidBody });
}
// Raycaster marker
const markerGeo = new THREE.SphereGeometry(0.1, 8, 8);
const markerMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const marker = new THREE.Mesh(markerGeo, markerMat);
scene.add(marker);
// Ray cast on click
window.addEventListener('click', () => {
const ray = new RAPIER.Ray({ x: 0, y: 20, z: 0 }, { x: 0, y: -1, z: 0 });
const hit = world.castRay(ray, 50, true);
if (hit) {
const point = ray.pointAt(hit.timeOfImpact);
marker.position.set(point.x, point.y, point.z);
}
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
world.step();
for (const { mesh, rigidBody } of pairs) {
const pos = rigidBody.translation();
const rot = rigidBody.rotation();
mesh.position.set(pos.x, pos.y, pos.z);
mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w);
}
renderer.render(scene, camera);
}
animate();
}
main();---
Example 3: cannon-es — Hinge Constraint (Door)
A door attached to a frame with a hinge constraint.
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
// Frame (static)
const frameBody = new CANNON.Body({ mass: 0 });
frameBody.addShape(new CANNON.Box(new CANNON.Vec3(0.1, 1.5, 0.1)));
frameBody.position.set(-1, 1.5, 0);
world.addBody(frameBody);
// Door (dynamic)
const doorBody = new CANNON.Body({
mass: 5,
shape: new CANNON.Box(new CANNON.Vec3(1, 1.5, 0.05)),
position: new CANNON.Vec3(0, 1.5, 0),
});
world.addBody(doorBody);
// Hinge constraint
const hinge = new CANNON.HingeConstraint(frameBody, doorBody, {
pivotA: new CANNON.Vec3(0.1, 0, 0),
axisA: new CANNON.Vec3(0, 1, 0),
pivotB: new CANNON.Vec3(-1, 0, 0),
axisB: new CANNON.Vec3(0, 1, 0),
});
world.addConstraint(hinge);
// Three.js meshes (create scene, camera, renderer as in Example 1)
const doorMesh = new THREE.Mesh(
new THREE.BoxGeometry(2, 3, 0.1),
new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
);
scene.add(doorMesh);
// Apply force to open door
doorBody.applyImpulse(new CANNON.Vec3(0, 0, 10), new CANNON.Vec3(1, 0, 0));
// Sync in animation loop
function updatePhysics(delta) {
world.step(1 / 60, delta, 3);
doorMesh.position.copy(doorBody.position);
doorMesh.quaternion.copy(doorBody.quaternion);
}---
Example 4: Rapier — Debug Rendering
Visualize Rapier collision shapes as wireframe overlays.
import * as THREE from 'three';
import RAPIER from '@dimforge/rapier3d-compat';
async function main() {
await RAPIER.init();
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// ... create bodies and colliders ...
// Debug line mesh
let debugMesh = null;
function updateDebugRender() {
const { vertices, colors } = world.debugRender();
if (debugMesh) {
scene.remove(debugMesh);
debugMesh.geometry.dispose();
debugMesh.material.dispose();
}
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, depthTest: false });
debugMesh = new THREE.LineSegments(geometry, material);
debugMesh.renderOrder = 999;
scene.add(debugMesh);
}
function animate() {
requestAnimationFrame(animate);
world.step();
updateDebugRender();
renderer.render(scene, camera);
}
animate();
}
main();---
Example 5: React Three Fiber with @react-three/rapier
Declarative physics with R3F.
import { Canvas } from '@react-three/fiber';
import { Physics, RigidBody, CuboidCollider } from '@react-three/rapier';
function Ground() {
return (
<RigidBody type="fixed">
<CuboidCollider args={[25, 0.5, 25]} position={[0, -0.5, 0]} />
<mesh position={[0, -0.5, 0]}>
<boxGeometry args={[50, 1, 50]} />
<meshStandardMaterial color="#808080" />
</mesh>
</RigidBody>
);
}
function FallingBox({ position }) {
return (
<RigidBody restitution={0.5} friction={0.7}>
<mesh position={position} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
</RigidBody>
);
}
export default function App() {
return (
<Canvas shadows camera={{ position: [0, 10, 20], fov: 75 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 10, 5]} castShadow />
<Physics gravity={[0, -9.81, 0]} debug>
<Ground />
{Array.from({ length: 10 }, (_, i) => (
<FallingBox key={i} position={[(Math.random() - 0.5) * 4, 5 + i * 2, 0]} />
))}
</Physics>
</Canvas>
);
}threejs-impl-physics — Methods Reference
cannon-es API
CANNON.World
| Method / Property | Signature | Description |
|---|---|---|
constructor | new CANNON.World() | Creates a physics world |
.gravity | Vec3 | World gravity vector; default (0, 0, 0) |
.broadphase | Broadphase | Collision broadphase algorithm |
.solver | Solver | Constraint solver; .iterations controls accuracy |
.allowSleep | boolean | Enable body sleeping; default false |
.step() | (fixedTimeStep: number, timeSinceLastCalled?: number, maxSubSteps?: number) => void | Advance simulation; ALWAYS use three-argument form |
.addBody() | (body: Body) => void | Add a rigid body to the world |
.removeBody() | (body: Body) => void | Remove a rigid body from the world |
.addConstraint() | (constraint: Constraint) => void | Add a constraint |
.removeConstraint() | (constraint: Constraint) => void | Remove a constraint |
.addContactMaterial() | (cm: ContactMaterial) => void | Register a contact material pair |
.removeContactMaterial() | (cm: ContactMaterial) => void | Remove a contact material pair |
CANNON.Body
| Method / Property | Signature | Description |
|---|---|---|
constructor | new CANNON.Body(options: { mass, position?, shape?, material?, linearDamping?, angularDamping?, type?, fixedRotation?, collisionFilterGroup?, collisionFilterMask? }) | Creates a rigid body |
.position | Vec3 | Body position in world space |
.quaternion | Quaternion | Body orientation |
.velocity | Vec3 | Linear velocity |
.angularVelocity | Vec3 | Angular velocity |
.mass | number | Body mass in kg; 0 = static |
.type | number | DYNAMIC, STATIC, or KINEMATIC |
.material | `Material \ | null` |
.linearDamping | number | Linear velocity damping [0, 1] |
.angularDamping | number | Angular velocity damping [0, 1] |
.fixedRotation | boolean | Lock rotation; default false |
.sleepState | number | AWAKE, SLEEPY, or SLEEPING |
.addShape() | (shape: Shape, offset?: Vec3, orientation?: Quaternion) => Body | Add a collision shape |
.removeShape() | (shape: Shape) => Body | Remove a collision shape |
.applyForce() | (force: Vec3, worldPoint?: Vec3) => void | Apply force at world point |
.applyImpulse() | (impulse: Vec3, worldPoint?: Vec3) => void | Apply instant impulse |
.applyLocalForce() | (force: Vec3, localPoint?: Vec3) => void | Apply force in local space |
.applyLocalImpulse() | (impulse: Vec3, localPoint?: Vec3) => void | Apply impulse in local space |
.applyTorque() | (torque: Vec3) => void | Apply rotational torque |
.sleep() | () => void | Force body to sleep |
.wakeUp() | () => void | Force body awake |
.addEventListener() | (type: string, listener: Function) => void | Listen for 'collide', 'sleep', 'wakeup' |
.removeEventListener() | (type: string, listener: Function) => void | Remove event listener |
CANNON.Vec3
| Method | Signature | Description |
|---|---|---|
constructor | new CANNON.Vec3(x?: number, y?: number, z?: number) | 3D vector |
.set() | (x: number, y: number, z: number) => Vec3 | Set components |
.copy() | (source: Vec3) => Vec3 | Copy from another Vec3 |
.vadd() | (v: Vec3, target?: Vec3) => Vec3 | Vector addition |
.vsub() | (v: Vec3, target?: Vec3) => Vec3 | Vector subtraction |
.scale() | (scalar: number, target?: Vec3) => Vec3 | Scalar multiply |
.dot() | (v: Vec3) => number | Dot product |
.cross() | (v: Vec3, target?: Vec3) => Vec3 | Cross product |
.length() | () => number | Vector magnitude |
.normalize() | () => Vec3 | Normalize in place |
.distanceTo() | (v: Vec3) => number | Distance to another Vec3 |
CANNON Shape Constructors
| Shape | Constructor |
|---|---|
Box | new CANNON.Box(halfExtents: Vec3) |
Sphere | new CANNON.Sphere(radius: number) |
Cylinder | new CANNON.Cylinder(radiusTop: number, radiusBottom: number, height: number, numSegments: number) |
Plane | new CANNON.Plane() |
ConvexPolyhedron | new CANNON.ConvexPolyhedron({ vertices: Vec3[], faces: number[][] }) |
Trimesh | new CANNON.Trimesh(vertices: number[], indices: number[]) |
Heightfield | new CANNON.Heightfield(data: number[][], options: { elementSize: number }) |
Particle | new CANNON.Particle() |
CANNON.Material and CANNON.ContactMaterial
| Constructor | Signature |
|---|---|
Material | new CANNON.Material(name?: string) |
ContactMaterial | new CANNON.ContactMaterial(m1: Material, m2: Material, options: { friction?: number, restitution?: number, contactEquationStiffness?: number, contactEquationRelaxation?: number }) |
CANNON Constraint Constructors
| Constraint | Constructor |
|---|---|
PointToPointConstraint | new CANNON.PointToPointConstraint(bodyA: Body, pivotA: Vec3, bodyB: Body, pivotB: Vec3, maxForce?: number) |
DistanceConstraint | new CANNON.DistanceConstraint(bodyA: Body, bodyB: Body, distance?: number, maxForce?: number) |
HingeConstraint | new CANNON.HingeConstraint(bodyA: Body, bodyB: Body, options: { pivotA: Vec3, axisA: Vec3, pivotB: Vec3, axisB: Vec3, maxForce?: number }) |
LockConstraint | new CANNON.LockConstraint(bodyA: Body, bodyB: Body, options?: { maxForce?: number }) |
ConeTwistConstraint | new CANNON.ConeTwistConstraint(bodyA: Body, bodyB: Body, options: { pivotA: Vec3, axisA: Vec3, pivotB: Vec3, axisB: Vec3, angle?: number, twistAngle?: number }) |
Spring | new CANNON.Spring(bodyA: Body, bodyB: Body, options: { restLength?: number, stiffness?: number, damping?: number, localAnchorA?: Vec3, localAnchorB?: Vec3 }) |
CANNON Broadphase Types
| Type | Constructor | Complexity |
|---|---|---|
NaiveBroadphase | new CANNON.NaiveBroadphase() | O(n^2) — test all pairs |
SAPBroadphase | new CANNON.SAPBroadphase(world) | O(n log n) — sweep and prune |
GridBroadphase | new CANNON.GridBroadphase(options) | O(n) — spatial hash grid |
---
Rapier API
RAPIER Module
| Method | Signature | Description |
|---|---|---|
init() | () => Promise<void> | Initialize WASM module; MUST await before ANY other API call |
RAPIER.World
| Method / Property | Signature | Description |
|---|---|---|
constructor | new RAPIER.World(gravity: {x, y, z}) | Creates a physics world |
.step() | (eventQueue?: EventQueue) => void | Advance simulation one fixed step |
.gravity | {x: number, y: number, z: number} | World gravity; mutable |
.createRigidBody() | (desc: RigidBodyDesc) => RigidBody | Create a rigid body from descriptor |
.removeRigidBody() | (body: RigidBody) => void | Remove and destroy a rigid body |
.createCollider() | (desc: ColliderDesc, parent?: RigidBody) => Collider | Create a collider, optionally attached to a body |
.removeCollider() | (collider: Collider, wakeUpParent: boolean) => void | Remove and destroy a collider |
.getCollider() | (handle: number) => Collider | Get collider by handle |
.getRigidBody() | (handle: number) => RigidBody | Get rigid body by handle |
.castRay() | `(ray: Ray, maxToi: number, solid: boolean) => RayColliderHit \ | null` |
.castRayAndGetNormal() | `(ray: Ray, maxToi: number, solid: boolean) => RayColliderHit \ | null` |
.intersectionsWithShape() | (position: {x,y,z}, rotation: {x,y,z,w}, shape: Shape, callback: (collider) => boolean) => void | Query shape overlaps |
.intersectionsWithPoint() | (point: {x,y,z}, callback: (collider) => boolean) => void | Query point containment |
.debugRender() | () => { vertices: Float32Array, colors: Float32Array } | Get debug wireframe data |
.free() | () => void | Destroy world and release WASM memory |
RAPIER.RigidBodyDesc (Builder)
| Method | Signature | Description |
|---|---|---|
.dynamic() | static () => RigidBodyDesc | Dynamic body descriptor |
.fixed() | static () => RigidBodyDesc | Static/fixed body descriptor |
.kinematicPositionBased() | static () => RigidBodyDesc | Kinematic position-driven body |
.kinematicVelocityBased() | static () => RigidBodyDesc | Kinematic velocity-driven body |
.setTranslation() | (x: number, y: number, z: number) => RigidBodyDesc | Set initial position |
.setRotation() | (rotation: {x, y, z, w}) => RigidBodyDesc | Set initial rotation quaternion |
.setLinvel() | (x: number, y: number, z: number) => RigidBodyDesc | Set initial linear velocity |
.setAngvel() | (angvel: {x, y, z}) => RigidBodyDesc | Set initial angular velocity |
.setLinearDamping() | (damping: number) => RigidBodyDesc | Set linear damping |
.setAngularDamping() | (damping: number) => RigidBodyDesc | Set angular damping |
.setCcdEnabled() | (enabled: boolean) => RigidBodyDesc | Enable continuous collision detection |
.setCanSleep() | (canSleep: boolean) => RigidBodyDesc | Allow/disallow sleeping |
.lockTranslations() | () => RigidBodyDesc | Lock all translation axes |
.lockRotations() | () => RigidBodyDesc | Lock all rotation axes |
.setGravityScale() | (scale: number) => RigidBodyDesc | Per-body gravity multiplier |
RAPIER.RigidBody
| Method | Signature | Description |
|---|---|---|
.translation() | () => {x, y, z} | Current position |
.rotation() | () => {x, y, z, w} | Current rotation quaternion |
.linvel() | () => {x, y, z} | Current linear velocity |
.angvel() | () => {x, y, z} | Current angular velocity |
.setTranslation() | (translation: {x, y, z}, wakeUp: boolean) => void | Set position |
.setRotation() | (rotation: {x, y, z, w}, wakeUp: boolean) => void | Set rotation |
.setLinvel() | (vel: {x, y, z}, wakeUp: boolean) => void | Set linear velocity |
.setAngvel() | (vel: {x, y, z}, wakeUp: boolean) => void | Set angular velocity |
.applyForce() | (force: {x, y, z}, wakeUp: boolean) => void | Apply force at center of mass |
.applyImpulse() | (impulse: {x, y, z}, wakeUp: boolean) => void | Apply instant impulse |
.applyTorque() | (torque: {x, y, z}, wakeUp: boolean) => void | Apply rotational torque |
.applyTorqueImpulse() | (impulse: {x, y, z}, wakeUp: boolean) => void | Apply instant torque |
.applyForceAtPoint() | (force: {x,y,z}, point: {x,y,z}, wakeUp: boolean) => void | Apply force at world point |
.applyImpulseAtPoint() | (impulse: {x,y,z}, point: {x,y,z}, wakeUp: boolean) => void | Apply impulse at world point |
.isSleeping() | () => boolean | Check sleep state |
.wakeUp() | () => void | Force body awake |
.sleep() | () => void | Force body to sleep |
.handle | number | Unique handle for lookups |
RAPIER.ColliderDesc (Builder)
| Method | Signature | Description |
|---|---|---|
.cuboid() | static (hx, hy, hz) => ColliderDesc | Box with half-extents |
.ball() | static (radius) => ColliderDesc | Sphere |
.capsule() | static (halfHeight, radius) => ColliderDesc | Capsule |
.cylinder() | static (halfHeight, radius) => ColliderDesc | Cylinder |
.cone() | static (halfHeight, radius) => ColliderDesc | Cone |
.convexHull() | `static (vertices: Float32Array) => ColliderDesc \ | null` |
.trimesh() | static (vertices: Float32Array, indices: Uint32Array) => ColliderDesc | Triangle mesh |
.heightfield() | static (nrows, ncols, heights: Float32Array, scale: {x,y,z}) => ColliderDesc | Terrain |
.roundCuboid() | static (hx, hy, hz, borderRadius) => ColliderDesc | Rounded box |
.setRestitution() | (coeff: number) => ColliderDesc | Set bounciness |
.setFriction() | (coeff: number) => ColliderDesc | Set friction |
.setDensity() | (density: number) => ColliderDesc | Set density (affects mass) |
.setMass() | (mass: number) => ColliderDesc | Set explicit mass |
.setSensor() | (isSensor: boolean) => ColliderDesc | Make sensor (trigger only, no physics response) |
.setTranslation() | (x, y, z) => ColliderDesc | Offset relative to parent body |
.setRotation() | (rotation: {x,y,z,w}) => ColliderDesc | Rotation relative to parent body |
.setActiveEvents() | (events: number) => ColliderDesc | Enable collision events |
RAPIER.Ray
| Constructor | Signature |
|---|---|
Ray | new RAPIER.Ray(origin: {x, y, z}, dir: {x, y, z}) |
RAPIER.EventQueue
| Method | Signature | Description |
|---|---|---|
constructor | new RAPIER.EventQueue(autoDrain: boolean) | Creates event queue |
.drainCollisionEvents() | (callback: (handle1: number, handle2: number, started: boolean) => void) => void | Process collision events |
.drainContactForceEvents() | (callback: (event: ContactForceEvent) => void) => void | Process contact force events |
.free() | () => void | Release WASM memory |