Listen up. If you're building modern applications, understanding Physics in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1Why You Need a Physics Engine
You can move objects manually, but if you want realistic gravity, collisions, and bouncing, you need a Physics Engine.
Hand-rolling gravity by nudging a mesh's position.y down a little each frame works fine for a single falling object, but it falls apart once objects need to collide, stack, or transfer momentum to one another ā that requires broad-phase collision detection, contact resolution, and constraint solving, which is exactly what a dedicated physics engine is built to do.
Physics engines like Cannon.js and Rapier run this simulation independently of Three.js's rendering loop, computing positions, rotations, and collision responses in their own internal units before you ever touch a mesh.
// š Adding gravity to the world3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2Three.js Has No Built-In Physics
Three.js doesn't have built-in physics. We must pair it with a separate physics library like Cannon.js or Rapier.
Three.js only owns the rendering side of the equation ā geometries, materials, cameras, and the WebGL draw calls that turn them into pixels. It has no concept of mass, velocity, or collision shapes anywhere on a Mesh or Scene.
Cannon.js (and its actively maintained fork cannon-es) run pure JavaScript physics math, while Rapier is a Rust library compiled to WebAssembly, which makes it noticeably faster for scenes with many simultaneously colliding bodies.
// Three.js handles the GRAPHICS.
// Cannon.js handles the MATH.3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3Checkpoint: Does Three.js Include Physics?
Does the core Three.js library include built-in physics for calculating gravity and collisions between meshes?
The answer is no ā THREE.Physics doesn't exist, and there's no gravity property anywhere on a Mesh or Scene. Three.js deliberately stays a rendering-only library so it can be paired with whichever physics engine best fits a given project.
This separation is a common pattern in graphics programming: the renderer draws whatever state it's given, while a completely separate simulation system decides what that state should be each frame.
// Does THREE.Physics exist?3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4The Physics World and Body Pairing
How does it work? You create a hidden 'Physics World'. Every visible 3D Mesh gets a matching invisible 'Body' in the physics world.
The World object is the physics engine's own scene graph ā it tracks every rigid body, its shape, mass, and material properties, completely separately from THREE.Scene. Setting world.gravity.set(0, -9.82, 0) applies Earth-like acceleration to every dynamic body inside it.
Each Body you add to the world is invisible on its own; it's pure math (position, velocity, collision shape) with nothing to render. You always keep a Mesh and a Body as a paired set, one for looks and one for physics.
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // Earth gravity!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5Stepping the World and Syncing Transforms
In your animation loop, you tell the physics engine to calculate the next step, and then you copy the coordinates from the invisible Body to the visible Mesh.
world.step(1/60) advances the simulation by a fixed time slice, resolving gravity, collisions, and constraints for every body in the world during that tick. It doesn't touch your meshes at all ā it only updates each physics body's internal position and quaternion.
The copy calls are what actually make the simulation visible: mesh.position.copy(body.position) and mesh.quaternion.copy(body.quaternion) pull the freshly computed physics transform onto the rendered object every frame, which is why forgetting this step is such a common bug ā the physics runs perfectly, but nothing on screen moves.
function animate() {
world.step(1/60); // Math!
mesh.position.copy(body.position); // Sync visual to math
mesh.quaternion.copy(body.quaternion);
}3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6Checkpoint: Syncing Body to Mesh Each Frame
In vanilla Three.js physics, what must you constantly do inside your requestAnimationFrame loop?
The correct answer is copying Body coordinates to Mesh coordinates. Every frame you must call world.step() to advance the simulation, then sync each tracked mesh's position and quaternion from its corresponding body.
Skip the step call and gravity never advances; skip the copy and the physics simulation runs invisibly while every mesh stays frozen in its last rendered position ā both are among the most common bugs when wiring up a physics engine by hand.
function animate() {
???
}3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7Simplifying Physics With @react-three/rapier
Doing this manually is tedious. React Three Fiber has a package called @react-three/rapier that abstracts all the syncing for you!
@react-three/rapier wraps the Rapier WASM engine in a declarative React API ā the <Physics> component creates and steps the World for you automatically on every frame, hooked directly into React Three Fiber's render loop.
This eliminates the manual world.step() and position-copying boilerplate entirely; you describe which meshes should behave physically using components, and the library handles the Body-to-Mesh synchronization internally.
import { Physics, RigidBody } from '@react-three/rapier';
<Canvas>
<Physics>
{/* Physics World */}
</Physics>
</Canvas>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8Wrapping Meshes in RigidBody
Any mesh you want to be affected by gravity must be wrapped in a <RigidBody>. It's that simple.
<RigidBody> automatically infers a collider shape from its child geometry by default (a box for boxGeometry, a sphere for sphereGeometry, and so on), and it keeps the wrapped mesh's transform in sync with the underlying Rapier rigid body every frame.
By default a <RigidBody> is 'dynamic' ā affected by gravity and collisions ā but that behavior can be overridden with the type prop, which is exactly what's needed for objects like floors that should never fall.
<RigidBody>
<mesh>
<boxGeometry />
</mesh>
</RigidBody>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Checkpoint: The RigidBody Component
In @react-three/rapier, what component must you wrap around your <mesh> so that it falls due to gravity?
The answer is <RigidBody>. Without it, a mesh is purely visual ā React Three Fiber will render it, but Rapier has no rigid body registered for it, so gravity and collisions simply don't apply.
A bare <mesh> and a <mesh> wrapped in <RigidBody> look identical the moment the scene mounts; the difference only becomes visible once the physics world starts stepping and one of them starts falling while the other stays frozen in place.
<???>
<mesh />
</???>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10Fixed RigidBodies for Static Geometry
But wait, what about the floor? We don't want the floor to fall! We must set the floor's RigidBody type to 'fixed'.
A type="fixed" RigidBody is immovable ā it still participates in collisions (other bodies bounce off it) but gravity and forces never move it, which is exactly the behavior a floor, wall, or terrain mesh needs.
The colliders="cuboid" prop on the falling box explicitly sets its collision shape to a box matching the geometry's bounds, which is more predictable than relying on shape auto-detection for non-trivial geometries.
{/* A falling box */}
<RigidBody colliders="cuboid">
<mesh><boxGeometry /></mesh>
</RigidBody>
{/* A static floor */}
<RigidBody type="fixed">
<mesh><planeGeometry /></mesh>
</RigidBody>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Simulating a Physics Drop With useFrame
Let's see a 3D Preview! Because we can't load Rapier here natively, we will simulate a physics drop using pure math and useFrame.
This preview hand-rolls the same gravity-and-bounce behavior a real physics engine would give you, just without collision detection between multiple bodies: velocityY accumulates a small downward increment every frame inside useFrame, and that velocity is added to the mesh's position.y.
The floor collision check (if (position.y < -1.5)) clamps the ball back to the floor height and flips velocityY negative, multiplying it by 0.8 so each bounce loses 20% of its energy ā a cheap way to fake restitution without an actual physics library.
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
const BouncingBall = () => {
const ref = useRef();
let velocityY = 0;
useFrame(() => {
if (ref.current) {
velocityY -= 0.01; // Gravity!
ref.current.position.y += velocityY;
// Collision with floor (y = -1.5)
if (ref.current.position.y < -1.5) {
ref.current.position.y = -1.5;
velocityY *= -0.8; // Bounce and lose energy
}
}
});
return (
<mesh ref={ref} position={[0, 3, 0]}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial color="#00F0FF" />
</mesh>
);
};
const App = () => (
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<BouncingBall />
<mesh position={[0, -2, 0]} rotation={[-Math.PI/2, 0, 0]}>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="gray" />
</mesh>
</Canvas>
);
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
12Physics Lesson Complete
And that's it! By combining Three.js, React Three Fiber, Shaders, and Physics, you can literally build anything you can imagine.
With rendering, cameras, materials, shaders, and now physics in your toolkit, you have every core building block a real-time 3D web application needs ā the remaining work is mostly about composing these pieces for your specific project.
Physics engines like Rapier scale to hundreds of simultaneously simulated bodies with careful use of fixed timesteps and sleeping bodies (physics bodies that stop being simulated once they come to rest), so what you've learned here is directly usable well beyond toy demos.
// š You have completed the Three.js journey!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
13Step-by-Step Breakdown
You can move objects manually, but if you want realistic gravity, collisions, and bouncing, you need a Physics Engine.
Three.js doesn't have built-in physics. We must pair it with a separate physics library like Cannon.js or Rapier.
Does the core Three.js library include built-in physics for calculating gravity and collisions between meshes?
- āYes, it is built-in
- āNo, you must use an external library
How does it work? You create a hidden 'Physics World'. Every visible 3D Mesh gets a matching invisible 'Body' in the physics world.
In your animation loop, you tell the physics engine to calculate the next step, and then you copy the coordinates from the invisible Body to the visible Mesh.
In vanilla Three.js physics, what must you constantly do inside your requestAnimationFrame loop?
- āDelete overlapping objects
- āCopy Body coordinates to Mesh coordinates
- āNothing, it's automatic
Doing this manually is tedious. React Three Fiber has a package called @react-three/rapier that abstracts all the syncing for you!
Any mesh you want to be affected by gravity must be wrapped in a <RigidBody>. It's that simple.
In @react-three/rapier, what component must you wrap around your <mesh> so that it falls due to gravity?
- āPhysicsBox
- āGravity
- āRigidBody
But wait, what about the floor? We don't want the floor to fall! We must set the floor's RigidBody type to 'fixed'.
Let's see a 3D Preview! Because we can't load Rapier here natively, we will simulate a physics drop using pure math and useFrame.
And that's it! By combining Three.js, React Three Fiber, Shaders, and Physics, you can literally build anything you can imagine.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Respect prefers-reduced-motion for Physics-Driven Animation
Continuously falling, bouncing, or colliding objects driven by a physics simulation can trigger discomfort for users with vestibular motion sensitivity ā check window.matchMedia('(prefers-reduced-motion: reduce)') and offer a way to pause the simulation or settle bodies into a static resting pose instead of an active drop.
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
<Physics paused={reduceMotion}>...</Physics>SEO Implications
- 1
Physics Configuration Is Runtime State, but the Concepts Are Crawlable
Gravity values, RigidBody types, and collider shapes are runtime simulation state with no crawlable footprint, but well-written, indexable explanations of RigidBody, colliders, and restitution are genuine SEO value, since these are frequently searched Three.js and React Three Fiber terms.
Best Practices
Use type="fixed" for Static Geometry, Not Extreme Mass
Mark floors, walls, and terrain with type="fixed" rather than trying to fake immovability with a very large mass value. Fixed bodies are excluded from the dynamic solver entirely, which is both physically correct and cheaper to simulate.
Let Bodies Sleep When They Stop Moving
Physics engines like Rapier automatically put bodies to sleep once their velocity drops below a threshold, removing them from the active simulation step. Scenes with many resting objects rely on this to stay performant, so avoid continuously applying forces to bodies that don't need to keep moving.
Frequent Bugs
The mesh doesn't move even though the physics simulation appears to be running correctly.
In vanilla Three.js plus Cannon.js, you forgot to copy the Body's position and quaternion onto the Mesh every frame. In @react-three/rapier, check that the mesh is actually wrapped in a <RigidBody> ā a bare <mesh> has no physics body registered for it at all.
Real-World Examples
Interactive Product Configurators with Falling Parts
E-commerce configurators often use RigidBody physics so accessories or attachments visually drop and settle into place when added to a product, giving the 3D preview a designed, physical feel instead of parts snapping instantly into position.
<RigidBody colliders="hull">
<mesh geometry={accessoryGeometry} />
</RigidBody>