Adding Physics to Three.js with Rapier

Updated June 2026
Rapier is a Rust-based physics engine compiled to WebAssembly that provides high-performance rigid body dynamics, collision detection, raycasting, and character controllers for browser games. Integrating it with Three.js involves creating parallel representations of game objects (a Three.js mesh for rendering and a Rapier body for physics), stepping the simulation at a fixed rate, and synchronizing positions each frame. This guide covers the complete integration from installation to character controllers.

Three.js provides no built-in physics, so you need an external library. The main options in 2026 are Rapier (WASM, high performance, deterministic), Cannon-es (pure JavaScript, simpler API, slower), and Ammo.js (Bullet physics port, large bundle, declining community). Rapier has become the preferred choice because its WASM compilation delivers performance close to native physics engines while its API is modern and well-documented. The deterministic simulation mode is also essential for multiplayer games where physics results must be identical across clients.

Step 1: Install and Initialize Rapier

Install Rapier with npm install @dimforge/rapier3d-compat. The -compat variant uses a compatibility layer that works with all bundlers and does not require top-level await. Import and initialize it at application startup: the init function loads and compiles the WASM module, which is an asynchronous operation that must complete before you can create physics objects.

Create the physics world with new RAPIER.World(gravity), where gravity is a vector, typically {x: 0, y: -9.81, z: 0} for Earth-like gravity. The world object is the container for all rigid bodies, colliders, joints, and the simulation itself. You will step this world at a fixed interval in your game loop.

Rapier uses a unit system where 1 unit equals 1 meter by default. If your Three.js scene uses a different scale (for example, 1 unit equals 1 centimeter), you need to account for this in gravity magnitude, collider sizes, and force values. Keeping Three.js and Rapier at the same scale (1 unit = 1 meter) simplifies everything and is the recommended approach.

Step 2: Create Rigid Bodies and Colliders

Every physics-enabled object needs a rigid body and at least one collider. The rigid body defines the object's type (dynamic, kinematic, or static) and physical properties. Dynamic bodies respond to forces and gravity. Kinematic bodies are moved by code and push dynamic objects but are not affected by forces. Static bodies never move and are used for terrain, walls, and fixed scenery.

Create a rigid body with world.createRigidBody(bodyDesc), where bodyDesc is built from RAPIER.RigidBodyDesc.dynamic(), .kinematicPositionBased(), or .fixed(). Set initial position with .setTranslation(x, y, z). Then create a collider attached to the body with world.createCollider(colliderDesc, body).

Rapier supports many collider shapes: cuboid, ball, capsule, cylinder, cone, convex hull, triangle mesh, and heightfield. For game objects, prefer simple shapes over triangle meshes. A character is best represented by a capsule. A crate is a cuboid. A rock might be a convex hull. Triangle mesh colliders are accurate but expensive, so reserve them for static environment geometry like terrain and level architecture. Set the collider's friction and restitution (bounciness) properties to control how objects slide and bounce on contact.

Step 3: Synchronize Physics with Rendering

Physics simulation must run at a fixed timestep for stability. In your game loop, accumulate elapsed time and step the physics world in fixed increments (typically 1/60th of a second) using world.step(). After all physics steps for the current frame are complete, iterate over your physics-enabled objects and copy their positions and rotations from Rapier bodies to Three.js meshes.

Read the position with body.translation() and the rotation with body.rotation(). The rotation is returned as a quaternion with x, y, z, w components. Apply them to the Three.js mesh with mesh.position.set(t.x, t.y, t.z) and mesh.quaternion.set(r.x, r.y, r.z, r.w).

For smooth rendering between physics steps, use interpolation. Store the previous and current physics positions, calculate an interpolation factor from the remaining accumulated time, and blend between them. This eliminates the visual stutter that occurs when the rendering frame rate does not divide evenly into the physics tick rate. The game loop article in this pillar covers interpolation in detail.

Step 4: Handle Collision Events

Rapier provides an event queue that reports collisions, sensor triggers, and contact forces after each simulation step. Drain the event queue after calling world.step() to process events. Collision events tell you which two colliders made contact and whether the contact started or ended. Sensor events fire when a collider marked as a sensor (a volume that detects overlap without physical response) is entered or exited.

Use collision events for gameplay logic: deal damage when a bullet hits an enemy, play a sound when an object lands on the ground, trigger a door opening when the player enters a sensor volume. Map collider handles back to your game entities by storing the collider handle on the entity object when you create it, or by using Rapier's user data feature to attach arbitrary data to colliders.

Contact force events report the magnitude of impact forces, which you can use for effects like impact particles, destruction physics, or fall damage. A character falling from a great height generates a high contact force with the ground, which you can threshold to apply damage or play a heavy landing animation.

Step 5: Use Raycasting for Gameplay

Raycasting shoots an invisible ray from an origin point in a direction and reports what it hits. Rapier's raycaster operates on the physics world, which is faster and more appropriate for gameplay queries than Three.js's visual raycaster because the physics representation is simpler (colliders vs. full mesh geometry).

Cast a ray with world.castRay(ray, maxDistance, solid, filterFlags, filterGroups). The filter parameters let you exclude certain collision groups, so a player's raycast does not hit their own collider. The result includes the hit collider, the distance along the ray, and the surface normal at the hit point.

Common raycast uses in games include: ground detection (cast downward from the player to check distance to the ground for jump eligibility and slope detection), bullet trajectories (cast from the weapon muzzle in the firing direction to determine hit targets), line of sight (cast from an AI entity to the player to check if they can see each other, with the ray blocked by walls), and interaction checks (cast from the camera center to find interactive objects the player is looking at).

Rapier also provides shape casting, which sweeps a shape (sphere, capsule, box) along a ray rather than a point. This is useful for thick bullet traces, explosion radius checks, and determining if a character can fit through an opening before they try to move through it.

Step 6: Build a Character Controller

Rapier's KinematicCharacterController provides collision-aware movement for player and NPC characters. Unlike dynamic rigid bodies that respond to forces, a kinematic character controller moves by a desired displacement each frame, and the controller resolves collisions by sliding along surfaces, climbing slopes, and stepping over small obstacles.

Create the controller with world.createCharacterController(offset), where offset is a small skin width (0.01-0.1 units) that prevents the character from getting stuck in geometry. Configure the maximum slope angle, step height, and whether to slide on non-climbable surfaces. Attach a capsule collider to a kinematic rigid body for the character.

Each frame, compute the desired movement vector from player input (forward/backward, left/right, plus vertical velocity for gravity and jumping). Call controller.computeColliderMovement(collider, desiredMovement). Read the computed movement with controller.computedMovement(), which is the actual movement after collision resolution. Apply this to the rigid body with body.setNextKinematicTranslation(newPosition).

Check controller.computedGrounded() after each movement to determine if the character is on the ground. Use this for jump eligibility, fall damage detection, animation state (grounded vs. airborne), and gravity application. When grounded, reset vertical velocity to a small downward value to maintain ground contact. When airborne, accumulate gravity each frame.

Key Takeaway

Rapier provides the performance of a native physics engine in the browser through WebAssembly. The integration pattern is straightforward: create parallel physics and rendering objects, step the simulation at fixed intervals, and synchronize positions each frame. The character controller handles the most complex part of game physics, so start there for player-facing features.