Physics with Rapier for Web Games

Updated June 2026
Rapier is a physics engine written in Rust and compiled to WebAssembly, making it the fastest general-purpose physics library available for browser games as of 2026. This guide walks through setting it up from scratch, creating bodies and colliders, applying forces, and wiring it to your rendering loop.

Rapier supports both 2D and 3D simulation through separate npm packages. The 3D package is @dimforge/rapier3d (or @dimforge/rapier3d-simd for browsers with WASM SIMD support). The 2D package is @dimforge/rapier2d. The API is nearly identical between them, with 2D using Vec2 and 3D using Vec3. This guide uses the 3D package, but everything applies to 2D with the obvious dimensional adjustments.

Install Rapier and Initialize the WASM Module

Install Rapier from npm with your package manager of choice. The package exports an asynchronous init function that loads and compiles the WASM binary. You must await this function before using any Rapier API. In a typical game setup, you call init() during your loading screen and store the resulting module reference for later use.

The initialization looks like: import RAPIER from '@dimforge/rapier3d'; followed by await RAPIER.init(); at startup. After init resolves, the RAPIER object exposes all the classes you need: World, RigidBodyDesc, ColliderDesc, and so on. If you want SIMD acceleration, import from '@dimforge/rapier3d-simd' instead. Modern Chrome, Firefox, and Edge all support WASM SIMD. Safari added support in version 16.4.

For projects using a bundler like Vite or Webpack, the WASM file is typically handled automatically by the bundler's WASM support. If you are using a plain script tag setup, you can load Rapier from a CDN and call the init function from a global variable. The Rapier documentation provides examples for both bundled and unbundled setups.

Create the Physics World and Configure Gravity

The world is the central object that holds all bodies, colliders, and joints. Create it by passing a gravity vector to the constructor: let world = new RAPIER.World(new RAPIER.Vector3(0.0, -9.81, 0.0)); This sets standard Earth gravity pointing downward along the y-axis. For a space game you might pass zero gravity. For a sidescroller with exaggerated jumping, you might use a stronger value like -20.0.

The world object manages the simulation pipeline internally: broadphase, narrowphase, island management, and constraint solving. You do not need to configure these individually for most games. The defaults are tuned for general-purpose real-time simulation. If you need to adjust solver iterations for stability (useful for complex constraint chains), you can set world.integrationParameters.numSolverIterations to a higher value, though the default of 4 handles most scenarios well.

Add Rigid Bodies and Colliders

Bodies and colliders are separate objects in Rapier. A rigid body defines the physical properties (mass, velocity, body type), while a collider defines the shape used for collision detection. You create a body first, then attach one or more colliders to it.

To create a dynamic box: build a RigidBodyDesc with RigidBodyDesc.dynamic() and set its initial translation with .setTranslation(x, y, z). Pass it to world.createRigidBody(bodyDesc) to get a RigidBody handle. Then create a ColliderDesc with ColliderDesc.cuboid(halfX, halfY, halfZ) and attach it with world.createCollider(colliderDesc, body). The cuboid dimensions are half-extents, so a 2x2x2 meter box uses cuboid(1, 1, 1).

For static ground, use RigidBodyDesc.fixed() instead of .dynamic(). Fixed bodies have infinite mass and never move. For a moving platform, use RigidBodyDesc.kinematicPositionBased() and set its position each frame with body.setNextKinematicTranslation(). The engine computes the velocity needed to reach that position within one timestep and applies it, pushing any dynamic bodies in the way.

Rapier supports sphere, capsule, cuboid, cylinder, cone, convex hull, convex mesh, triangle mesh, and heightfield collider shapes. For characters, a capsule is the standard choice because it slides smoothly along walls and over small ledges without catching on edges the way a box would.

You can set physical material properties on colliders: .setRestitution(0.5) for moderate bounciness, .setFriction(0.7) for moderate grip. Density can be set with .setDensity(1.0), and the engine computes mass automatically from density and shape volume, or you can override mass directly on the rigid body.

Apply Forces, Impulses and Torque

Forces and impulses are the gameplay interface to the physics simulation. A force is accumulated over the timestep and produces acceleration proportional to the body's mass (F=ma). An impulse is an immediate change in velocity, independent of the timestep duration.

To push a body continuously (like a thruster), call body.addForce(new RAPIER.Vector3(0, 500, 0), true) each frame. The second parameter wakes the body if it is sleeping. To make the player jump, call body.applyImpulse(new RAPIER.Vector3(0, 10, 0), true) once when the jump button is pressed. The difference matters: forces accumulate smoothly, while impulses produce instant velocity changes.

Torque and torque impulses work the same way for rotation. body.applyTorqueImpulse(new RAPIER.Vector3(0, 5, 0), true) spins the body around the y-axis. This is useful for spinning projectiles, tumbling debris, or top-down vehicle steering.

For direct velocity control (common in platformers where you want precise horizontal speed), you can read the current velocity with body.linvel(), modify the x and z components, and write it back with body.setLinvel(). This bypasses the force model but gives you frame-exact control over movement speed. Keep the y component from linvel() unchanged so gravity still works naturally.

Step the World and Sync with Your Renderer

Each frame in your requestAnimationFrame loop, call world.step() to advance the simulation by one fixed timestep (1/60th of a second by default). After stepping, loop through your game objects and copy each body's position and rotation to the corresponding visual mesh.

The synchronization code reads body.translation() to get a {x, y, z} object and body.rotation() to get a {x, y, z, w} quaternion. For Three.js, set mesh.position.copy(body.translation()) and mesh.quaternion.copy(body.rotation()). For Babylon.js, set mesh.position from the translation and mesh.rotationQuaternion from the rotation.

For smoother visuals, use the accumulator pattern. Track elapsed real time between frames and accumulate it. Step the world once for each full timestep that fits in the accumulator. The fractional remainder tells you how far between the last two physics states the current render moment falls. Interpolate visual positions using that fraction: renderPos = prevPos + (currPos - prevPos) * alpha. This eliminates the stuttering that occurs when the render rate and physics rate are not perfectly aligned.

For complex scenes, consider running Rapier in a Web Worker. The WASM module initializes fine in a worker context. Send input commands to the worker via postMessage, step the world there, and send back an array of transforms via SharedArrayBuffer or transferable ArrayBuffers. This keeps the main thread free for rendering and UI, preventing physics spikes from causing visible frame drops.

Use Ray Casting and Character Controllers

Ray casting is essential for gameplay queries like ground detection, line-of-sight checks, and mouse picking. Rapier's world.castRay() takes a ray origin, a ray direction, a maximum distance, and optionally a collision filter. It returns the first collider hit along the ray and the distance to the hit point. For ground checks, cast a short ray downward from the player's feet. If it hits within a small threshold (like 0.1 units), the player is grounded and can jump.

Rapier also provides a built-in KinematicCharacterController that handles common character movement patterns. You create it with world.createCharacterController(skinWidth), where skinWidth is a small offset that prevents the character from intersecting walls. Then each frame, call controller.computeColliderMovement(collider, desiredMovement) to compute the allowed movement after colliding with the environment. The controller handles stepping over small obstacles, sliding along walls, and snapping to slopes. It returns the corrected movement vector, which you apply to the body's position. This saves you from building ground detection, wall sliding, and slope handling from scratch.

Shape casting (sweeping a shape along a path) and intersection tests (checking if a shape overlaps any colliders at a point) are also available for more complex queries like explosion radius checks and overlap triggers.

Key Takeaway

Rapier delivers near-native physics performance in the browser through WebAssembly. The setup follows a clear pattern: initialize WASM, create a world, add bodies with colliders, step each frame, and copy transforms to your renderer. Its character controller and ray casting APIs cover the most common gameplay needs without custom code.