Rigid-Body Dynamics
A bouncing particle only ever answers one question: where am I? A tumbling crate, a spun
coin, a thrown wrench — these answer a second one too: which way am I facing? A
rigid body is a solid object whose every point keeps a fixed distance from every
other point: it cannot squash, stretch or bend. That rigidity is a gift to the simulator, because it
means the whole object's motion collapses into just two things — the journey of a single point (the
centre of mass) and the orientation the body is turned to. Track
those two, and you know where every atom of the object is.
This page builds the simulator's core loop for solids: the state a rigid body
carries, the Newton–Euler equations that push that state forward, and the one
genuinely new subtlety — integrating orientation, which is not a position and must not be
treated like one. It builds on
rigid-body motion and the
particle simulators of
computer animation.
The state of a rigid body
A particle's whole world is a position and a velocity. A rigid body doubles that: it needs a
linear part (how the body drifts through space) and an
angular part (how it spins). The complete state is four quantities:
- Position \mathbf{x} — the location of the
centre of mass (COM), the special point that moves as if all the mass sat there.
- Orientation \mathbf{q} — how the body is turned,
stored as a unit quaternion (or a 3\times 3 rotation
matrix).
- Linear velocity \mathbf{v} — how fast the COM
moves (equivalently the linear momentum \mathbf{P} = m\mathbf{v}).
- Angular velocity \boldsymbol{\omega} — the axis and
rate of spin (equivalently the angular momentum \mathbf{L}, related by
the the inertia tensor
I through \mathbf{L} = I\boldsymbol{\omega}).
The first two, (\mathbf{x}, \mathbf{q}), are the configuration —
they say precisely where every point of the object is right now. The last two,
(\mathbf{v}, \boldsymbol{\omega}), are how that configuration is changing.
Everything the simulator does is push these four forward one small time-step at a time.
Why a point and a turn are enough
Here is the fact that makes solids cheap to simulate. Because the body is rigid, the world-space
position of any point \mathbf{r}_{\text{body}} fixed in the object
is completely determined by the COM position and the orientation:
\mathbf{p}_{\text{world}} \;=\; \mathbf{x} \;+\; R(\mathbf{q})\,\mathbf{r}_{\text{body}},
where R(\mathbf{q}) is the rotation matrix built from the orientation. Turn
the object (change \mathbf{q}) and every point swings around the COM
together; slide the object (change \mathbf{x}) and every point shifts by the
same vector. You never store the millions of vertices' motions individually — a corner of a crate and
its opposite corner share the very same (\mathbf{x}, \mathbf{q}). That is
the whole payoff of rigidity: six numbers of configuration (three for
\mathbf{x}, three of freedom in \mathbf{q}) stand
in for the entire body.
The Newton–Euler equations
Two laws advance the two halves of the state. The linear half is plain Newton: the
net force accelerates the centre of mass, and nothing else. The angular half is
Euler's law: the net torque changes the angular momentum.
- Translation (Newton):
\mathbf{F} = m\mathbf{a} = \dot{\mathbf{P}} — the total force moves the
COM exactly as if the body were a point of mass m.
- Rotation (Euler):
\boldsymbol{\tau} = \dot{\mathbf{L}} — the total torque about the COM is
the rate of change of angular momentum.
The beautiful thing is how they split. For the position you can forget the shape entirely —
summing forces and dividing by mass gives the COM acceleration, identical to a particle simulator.
The orientation is driven by a separate accumulator, torque, which lives in its own channel.
In a step the simulator does, in effect:
type Body = {
x: Vec3; q: Quat; // configuration: COM position, orientation
v: Vec3; omega: Vec3; // velocities: linear, angular
m: number; invI: Mat3; // mass, inverse inertia (world space)
};
function step(b: Body, force: Vec3, torque: Vec3, dt: number): void {
// Linear half — pure Newton, shape-independent.
const a = scale(force, 1 / b.m); // a = F / m
b.v = add(b.v, scale(a, dt));
b.x = add(b.x, scale(b.v, dt));
// Angular half — Euler. omega = invI * L
const angAcc = mul(b.invI, torque); // dω/dt from torque
b.omega = add(b.omega, scale(angAcc, dt));
b.q = integrateOrientation(b.q, b.omega, dt); // NOT q += omega*dt !
}
Everything above the divider is a particle. Everything below it is what makes a solid a solid — and
that last line, integrateOrientation, hides the one real trap, which we unpack next.
Forces AND torques: where a push lands matters
A particle only cares how hard you push it. A rigid body also cares where. Apply a
force \mathbf{F} at a point offset \mathbf{r}
from the centre of mass, and it does two jobs at once: it adds to the force total (translating the
COM) and it produces a torque about the COM given by the cross product
\boldsymbol{\tau} \;=\; \mathbf{r} \times \mathbf{F}.
So the simulator keeps two running sums each frame: a force accumulator and a torque
accumulator. Every contact, thruster or hit contributes to both.
function applyForceAtPoint(b: Body, force: Vec3, worldPoint: Vec3): void {
b.forceAccum = add(b.forceAccum, force); // always translates the COM
const r = sub(worldPoint, b.x); // offset from centre of mass
b.torqueAccum = add(b.torqueAccum, cross(r, force)); // τ = r × F
}
Two special cases make the geometry click. A force aimed straight through the COM has
\mathbf{r} \parallel \mathbf{F}, so
\mathbf{r} \times \mathbf{F} = \mathbf{0} — pure translation, no spin. And
the reason gravity never spins a free body is exactly this: gravity pulls on every particle, but its
net effect acts at the COM, so its offset is zero. A push at the rim, by contrast, has a big
perpendicular \mathbf{r} and spins the body hard.
Feel the offset
Below, a square box is struck by a fixed upward impulse. Drag the offset slider to
move the strike point away from the centre of mass along the bottom edge. At zero offset the box
simply rises — no rotation. As the offset grows, the same impulse delivers more torque
(\tau = r \times F grows with r), and the box is
drawn turned by a correspondingly larger angle. Notice the impulse arrow never changes length: only
where it lands does, and that alone conjures the spin.
This is the whole reason off-centre hits feel alive: a single number, the offset, silently converts a
straight shove into a shove-plus-spin.
Worked example: a shove at the edge of a box
Take a flat box of mass m = 2\ \text{kg} lying in the plane, its COM at the
origin. Its right edge is at x = 0.5\ \text{m}. We push straight
up with force \mathbf{F} = (0,\, 10,\, 0)\ \text{N}, applied at the
top-right corner \mathbf{r} = (0.5,\, 0.3,\, 0)\ \text{m} measured from the
COM. Split the effect into its two jobs.
Translation of the COM. The location is irrelevant here — only the force total
matters:
\mathbf{a} \;=\; \frac{\mathbf{F}}{m} \;=\; \frac{(0,\,10,\,0)}{2} \;=\; (0,\,5,\,0)\ \text{m/s}^2.
The whole box's centre accelerates upward at 5\ \text{m/s}^2, exactly as a
2\ \text{kg} particle would.
Spin about the COM. Now the offset earns its keep. The torque is the cross product
of the offset with the force. With \mathbf{r} = (0.5, 0.3, 0) and
\mathbf{F} = (0, 10, 0), only the z component
survives:
\boldsymbol{\tau} \;=\; \mathbf{r}\times\mathbf{F} \;=\; \big(r_x F_y - r_y F_x\big)\,\hat{\mathbf{z}} \;=\; (0.5)(10) - (0.3)(0) \;=\; 5\ \text{N·m}\ \hat{\mathbf{z}}.
A positive z-torque means an anticlockwise spin: the pushed-up
right corner rotates the box counter-clockwise as it rises. The same upward shove delivered at the
centre (r_x = 0) would give
\boldsymbol{\tau} = \mathbf{0} — pure lift, no turn. The
5\ \text{N·m} of torque, fed through
\dot{\boldsymbol{\omega}} = I^{-1}\boldsymbol{\tau}, is precisely the spin
that the offset bought us. Same force, different place, entirely different motion.
Integrating orientation — the quaternion derivative
Position integrates trivially: \mathbf{x} \mathrel{+}= \mathbf{v}\,\Delta t.
Orientation does not — you cannot just add "angle times time" to a quaternion, because a quaternion
lives on the unit sphere, not on a flat line. The correct rule relates the angular velocity to the
rate of change of the quaternion. Writing \boldsymbol{\omega} as a
pure quaternion \omega = (0,\, \boldsymbol{\omega}) (zero
scalar part), the quaternion derivative is
\dot{\mathbf{q}} \;=\; \tfrac{1}{2}\,\omega\,\mathbf{q},
a quaternion multiplication. You then take a small step and, crucially,
re-normalise so the quaternion stays a unit quaternion (a valid rotation):
function integrateOrientation(q: Quat, omega: Vec3, dt: number): Quat {
const wq: Quat = { w: 0, x: omega.x, y: omega.y, z: omega.z }; // ω as a pure quaternion
const qDot = scaleQ(mulQ(wq, q), 0.5); // q̇ = ½ ω q
const qNew = addQ(q, scaleQ(qDot, dt)); // q ← q + q̇ Δt
return normalize(qNew); // RE-NORMALISE — keep it a unit quaternion
}
The re-normalisation matters because the additive step nudges the quaternion slightly off the
unit sphere every frame; without a re-normalise those tiny errors accumulate and the rotation matrix
it produces starts to shear and scale the model. One cheap normalise per step keeps the orientation
honest.
The two commonest ways to wreck a rigid-body simulator both come from forgetting that rotation is
special. First: do not treat orientation like a position. Adding
\boldsymbol{\omega}\,\Delta t straight onto three Euler angles seems to work
for a frame or two, then drifts, and eventually hits gimbal lock — two of the axes
line up and a whole degree of freedom silently vanishes, freezing the spin. Euler angles are a
display convenience, not an integration variable. Store the orientation as a quaternion (or matrix),
advance it with the quaternion derivative \dot{\mathbf{q}} = \tfrac12\,\omega\mathbf{q},
and re-normalise every step — skip the normalise and the quaternion slowly leaves the
unit sphere, turning your rotations into skews.
Second: torque comes from off-centre forces. A force's magnitude alone tells
you the COM translation, but never the spin. The spin depends on where the force is applied —
the offset \mathbf{r} in \boldsymbol{\tau} = \mathbf{r}\times\mathbf{F}.
Forget to accumulate torque and every object slides around like a frictionless puck that can never
rotate, no matter how you clip it at the corner. Accumulate both a force sum and a torque sum
each frame.
Part folklore, part physics. Because gravity acts at the centre of mass it exerts no torque,
so a phone only tumbles if it was already spinning when it left your hand — which, having slipped off a
fingertip at the edge (a nice off-centre \mathbf{r}\times\mathbf{F}!), it
usually is. There is also a deeper twist: a phone is a thin slab whose three principal moments of
inertia are all different, and the intermediate-axis theorem (the "tennis-racket
theorem") says spin about the middle axis is unstable, so a real tumble flips chaotically. Add the
typical drop height giving just about half a rotation, and face-down becomes the depressingly likely
outcome. The full inertia story is
the inertia tensor.