Computer Graphics
Quaternions for 3D Rotation
Four numbers that turn any object in space — and never lock up
A quaternion is a four-component number (w, x, y, z) that encodes a 3D rotation as a point on the unit hypersphere, rotating vectors with q·v·q⁻¹ without gimbal lock and interpolating smoothly between orientations via slerp.
- Storage4 floats
- Compose two rotations16 mul, 12 add
- Gimbal lockImpossible
- Smooth interpolationSlerp, O(1)
- Cover of SO(3)2-to-1 (q ≡ −q)
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
How a quaternion encodes a rotation
Every rotation in 3D is, by Euler's rotation theorem, a single turn by some angle θ about some fixed axis. A quaternion bottles exactly that. Given a unit axis (x, y, z) and an angle θ, the rotation quaternion is
q = ( cos(θ/2), x·sin(θ/2), y·sin(θ/2), z·sin(θ/2) )
└── w ──┘ └──────── vector part (the axis) ──────┘
The half-angle is the part everyone trips over, and it falls straight out of how you apply the rotation. You don't multiply a vector by the quaternion once; you sandwich it between the quaternion and its inverse:
v' = q · v · q⁻¹
Here v is the vector written as a pure quaternion (0, vx, vy, vz). Each multiplication contributes θ/2, and the two halves add to the full θ — that's why the constructor uses θ/2. For a unit quaternion the inverse is just the conjugate q* = (w, −x, −y, −z), so the rotation costs no division at all.
Quaternion multiplication is where the magic compresses. Multiply two of them and you get the quaternion for "do the second rotation, then the first" — composition for free, no matrix concatenation. The product is non-commutative (rotations don't commute, and neither do quaternions), and the formula is the Hamilton product:
(w₁ + v₁)(w₂ + v₂) = (w₁w₂ − v₁·v₂) + (w₁v₂ + w₂v₁ + v₁×v₂)
The dot product and the cross product, the two operations that already define 3D geometry, are baked right into the multiplication rule. That's not a coincidence — Gibbs and Heaviside later carved the modern dot and cross products out of Hamilton's quaternion product in the 1880s.
Why Euler angles lock — and quaternions don't
Euler angles describe orientation as three sequential turns, say yaw then pitch then roll. They are intuitive and they are a trap. Pitch the nose up 90° and the yaw axis rotates until it lines up with the roll axis. Now two of your three knobs spin the object around the same line. You have lost a degree of freedom: there is no combination of yaw and roll that produces a turn about the third axis. That is gimbal lock, and it grounded analog flight instruments and very nearly aborted Apollo 11's alignment.
A quaternion stores the rotation as one point on the unit 3-sphere, never as a stack of axis-aligned sub-turns. There is no configuration in which an axis disappears, because there are no separate axes to collide. The cost of admission is that you give up the human-readable "30° yaw" reading — a quaternion's four numbers mean nothing to the eye — but every interpolation, composition, and integration stays well-defined everywhere.
When to reach for a quaternion
- Animation and skinning — blending between keyframe poses needs smooth interpolation that Euler angles can't deliver cleanly; slerp is the standard tool.
- Camera and character orientation in games and VR, where an object may tumble through any orientation and gimbal lock would be catastrophic.
- Spacecraft and drone attitude — IMUs integrate angular velocity into a quaternion every tick precisely because it never locks and renormalizes cheaply.
- Compounding many small rotations — accumulating thousands of incremental turns is cheaper and more stable as quaternion products than as repeated matrix multiplies.
If you only ever apply a fixed rotation to a mesh of vertices and never interpolate or compose, a plain rotation matrix is simpler and the per-vertex transform is faster. Quaternions earn their keep the moment orientation changes over time.
Quaternion vs Euler angles vs rotation matrix vs axis-angle
| Quaternion | Euler angles | 3×3 matrix | Axis-angle | |
|---|---|---|---|---|
| Numbers stored | 4 | 3 | 9 | 4 |
| Gimbal lock | Never | Yes (at ±90° pitch) | Never | Never |
| Compose two rotations | 16 mul, 12 add | Undefined (must convert) | 27 mul, 18 add | Convert first |
| Rotate one vector | ~28 mul (or via matrix) | Convert first | 9 mul, 6 add | Rodrigues, ~22 mul |
| Smooth interpolation | Slerp — constant speed | Wobbles, can flip | Awkward (no clean blend) | Lerp angle + axis, uneven |
| Drift / re-orthonormalize | 1 sqrt to renormalize | None needed | Gram–Schmidt, costly | 1 sqrt on axis |
| Human-readable | No | Yes | No | Somewhat |
| Typical use | Internal orientation state | UI / authoring input | GPU vertex transform | Specifying a single turn |
The pragmatic workflow uses all four: authors type Euler angles, the engine stores a quaternion, slerp interpolates it, and the renderer converts it to a matrix once per frame so the GPU can blast it across every vertex.
What the numbers actually say
- Composition cost. A Hamilton product is 16 multiplies and 12 adds. Multiplying two 3×3 matrices is 27 multiplies and 18 adds. Compose a thousand incremental rotations per frame and that's roughly 16k vs 27k multiplies — a ~40% saving, before you count the storage of 4 floats vs 9.
- Drift is real but slow. Each product adds about one ULP of length error. After ~10⁵ multiplications a single-precision quaternion has drifted on the order of 10⁻³ from unit length — enough to introduce a visible scaling shimmer, so engines renormalize every frame at the cost of one
sqrt(~20 ns). - Slerp is constant-time but not free. One slerp call costs an
acos, twosins, and a divide — on the order of 50–100 ns. Blend a 100-bone skeleton at 60 fps and that's ~6,000 slerps/second; many engines fall back to normalized lerp (nlerp), which is ~10× cheaper and visually indistinguishable when the poses are close. - The double cover bites once. Forgetting the q vs −q sign check on slerp makes a character spin 358° the wrong way instead of 2° the right way — a one-line bug with a very loud symptom.
JavaScript implementation
A self-contained quaternion: build from axis-angle, multiply (Hamilton product), rotate a vector by the sandwich, and slerp between two orientations.
// q = [w, x, y, z]
function fromAxisAngle(ax, ay, az, theta) {
const len = Math.hypot(ax, ay, az) || 1;
const h = theta / 2, s = Math.sin(h) / len;
return [Math.cos(h), ax * s, ay * s, az * s];
}
// Hamilton product: applies b first, then a (composition a ∘ b)
function mul(a, b) {
const [aw, ax, ay, az] = a, [bw, bx, by, bz] = b;
return [
aw*bw - ax*bx - ay*by - az*bz,
aw*bx + ax*bw + ay*bz - az*by,
aw*by - ax*bz + ay*bw + az*bx,
aw*bz + ax*by - ay*bx + az*bw,
];
}
const conj = ([w, x, y, z]) => [w, -x, -y, -z];
function normalize(q) {
const n = Math.hypot(q[0], q[1], q[2], q[3]) || 1;
return [q[0]/n, q[1]/n, q[2]/n, q[3]/n];
}
// Rotate vector v = [x, y, z] by unit quaternion q: q · v · q⁻¹
function rotate(q, v) {
const p = [0, v[0], v[1], v[2]];
const r = mul(mul(q, p), conj(q)); // q⁻¹ = conj(q) for unit q
return [r[1], r[2], r[3]];
}
// Spherical linear interpolation, t in [0,1]
function slerp(a, b, t) {
let dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
if (dot < 0) { b = b.map(c => -c); dot = -dot; } // take the short way
if (dot > 0.9995) { // nearly parallel → lerp
return normalize(a.map((c, i) => c + t * (b[i] - c)));
}
const omega = Math.acos(dot), so = Math.sin(omega);
const wa = Math.sin((1 - t) * omega) / so;
const wb = Math.sin(t * omega) / so;
return a.map((c, i) => wa * c + wb * b[i]);
}
Two details that bite beginners. The dot < 0 flip in slerp is the double-cover guard — without it you sometimes rotate the long way around. And the dot > 0.9995 branch dodges the division-by-near-zero sin(omega) when the two quaternions are almost identical.
Python implementation
The same four operations in Python, plus the famous conversion to a 3×3 matrix that engines hand to the GPU.
import math
def from_axis_angle(ax, ay, az, theta):
n = math.hypot(ax, ay, az) or 1.0
h = theta / 2
s = math.sin(h) / n
return (math.cos(h), ax*s, ay*s, az*s)
def mul(a, b):
aw, ax, ay, az = a
bw, bx, by, bz = b
return (
aw*bw - ax*bx - ay*by - az*bz,
aw*bx + ax*bw + ay*bz - az*by,
aw*by - ax*bz + ay*bw + az*bx,
aw*bz + ax*by - ay*bx + az*bw,
)
def conj(q):
w, x, y, z = q
return (w, -x, -y, -z)
def rotate(q, v):
p = (0.0, *v)
r = mul(mul(q, p), conj(q))
return (r[1], r[2], r[3])
def slerp(a, b, t):
dot = sum(ai*bi for ai, bi in zip(a, b))
if dot < 0: # double-cover: flip to short arc
b = tuple(-c for c in b)
dot = -dot
if dot > 0.9995: # nearly parallel → cheap lerp
out = tuple(ai + t*(bi-ai) for ai, bi in zip(a, b))
n = math.sqrt(sum(c*c for c in out))
return tuple(c/n for c in out)
omega = math.acos(dot)
so = math.sin(omega)
wa = math.sin((1-t)*omega) / so
wb = math.sin(t*omega) / so
return tuple(wa*ai + wb*bi for ai, bi in zip(a, b))
# Famous conversion: unit quaternion -> 3x3 rotation matrix (row-major)
def to_matrix(q):
w, x, y, z = q
return [
[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
[ 2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
[ 2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)],
]
The matrix conversion is the bridge to hardware: keep orientation as a quaternion, do all the composing and slerping there, then call to_matrix once and let the GPU apply that single 3×3 to thousands of vertices per draw call.
Variants and cousins worth knowing
Normalized lerp (nlerp). Linearly interpolate the four components, then renormalize. About 10× cheaper than slerp and gives the same path, but the angular speed isn't constant — it runs fastest in the middle and slowest at the ends. Fine for short blends and bone skinning; wrong for a deliberate slow turn.
Squad (spherical cubic). Slerp produces a continuous path but kinks at every keyframe. Squad chains slerps through control quaternions to get a smooth, C¹-continuous curve through a sequence of orientations — the analog of a Catmull–Rom spline on the sphere.
Dual quaternions. A pair of quaternions that encodes rotation and translation together (a rigid-body transform). Linear blending of dual quaternions avoids the "candy-wrapper" collapse that plain matrix skinning shows at twisting joints, which is why modern skinning often uses them.
Rotors (geometric algebra). The same objects as unit quaternions, derived from the geometric product instead of Hamilton's. Identical math, arguably cleaner intuition, and they generalize to any dimension.
Common bugs and edge cases
- Forgetting the half-angle. Building
q = (cos θ, axis·sin θ)instead ofθ/2rotates by double the intended angle. The most common quaternion bug. - Skipping the double-cover sign check in slerp. When the dot product is negative, you must negate one quaternion or slerp takes the 358° detour. Symptom: an object that should nudge slightly instead whips almost all the way around.
- Never renormalizing. Accumulated products drift off the unit sphere and inject a slow scaling artifact. Renormalize once per frame, not after every multiply.
- Mixing multiplication order. Quaternions don't commute.
q1·q2andq2·q1are different rotations; pick a convention (usually "left-multiply to pre-rotate") and hold it. - Treating w as the last component. Some libraries store
(x, y, z, w)(Three.js, glm) and others(w, x, y, z)(Eigen, many math texts). Cross-wiring two libraries silently scrambles every rotation. - Slerp at the antipode. When two quaternions are exactly opposite (180° apart) the shortest arc is undefined — every great circle is equally short. Detect
|dot| ≈ 0near −1 and pick an arbitrary perpendicular axis, or the interpolation jitters.
Frequently asked questions
What is gimbal lock and how do quaternions avoid it?
Gimbal lock happens when Euler angles align two of the three rotation axes, collapsing three degrees of freedom into two so one axis of control disappears. Quaternions never decompose a rotation into stacked axis angles — they store one axis and one angle as a single point on the 4D unit sphere — so there is no configuration where an axis can vanish.
Why does rotating a vector use q·v·q⁻¹ instead of q·v?
A single multiplication q·v rotates by the full angle but also drags the result out of pure-vector space into a general quaternion. Sandwiching with the conjugate, q·v·q⁻¹, cancels the imaginary leakage and applies exactly half the angle on each side, so the net rotation is the intended angle and the output is a pure vector again. This is why a unit quaternion built from a half-angle rotates by the full angle.
What is slerp and when should I use it instead of lerp?
Slerp (spherical linear interpolation) walks the shortest great-circle arc between two unit quaternions at constant angular velocity, giving smooth, even rotation. Plain lerp on the four components cuts a straight chord through the sphere — it needs re-normalizing and speeds up in the middle. Use lerp (normalized) when the two orientations are close or you blend many at once; use slerp for a single long, visually smooth turn.
Do I have to normalize quaternions?
Only unit quaternions (length 1) represent pure rotations. Repeated multiplication accumulates floating-point error that drifts the length away from 1, which adds a scaling artifact. Renormalize periodically — dividing by the magnitude costs one sqrt — typically once per frame or every few hundred multiplications, not after every single op.
Why do q and −q represent the same rotation?
The unit quaternion sphere is a double cover of 3D rotation space: every orientation maps to two antipodal points, q and −q. Because rotation uses the sandwich q·v·q⁻¹, the signs cancel and both give the identical result. Slerp must check the dot product and flip one quaternion's sign if negative, otherwise it takes the long way around the 720° cover.
Are quaternions faster than rotation matrices?
Composing two rotations is cheaper as quaternions — 16 multiplies and 12 adds versus 27 multiplies and 18 adds for two 3×3 matrices — and they store 4 floats instead of 9. But rotating a vector is cheaper with a matrix (one matrix-vector product). The common pattern is to keep orientation as a quaternion, compose and interpolate there, then convert to a matrix once per frame for the GPU to apply to thousands of vertices.