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:

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.

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.