Interpolating Orientation with Quaternions

A camera swings to face a new target; a character's head turns to follow a bird; a spaceship banks into a curve. Each is an orientation keyframe problem: you have a start rotation at one frame and an end rotation at another, and the tool must invent every rotation in between. For positions the answer is easy — slide along a straight line. For orientations the naive answer is a trap. Rotations do not live on a flat line; they live on a curved space (SO(3)), and the only way to glide across it at a steady rate is to walk a great-circle arc on the sphere of unit quaternions. That walk is called SLERP, and getting it right — including one notorious sign flip — is what makes rotation animation look smooth instead of drunk.

This page is the animation-specific companion to spherical linear interpolation: we take the maths you already met and put it to work on orientation tracks — blending, splining, and the cheap approximations you will actually meet in a game engine.

Why the obvious methods fail

There are two "obvious" ways to interpolate a rotation, and both are wrong. The first is to store the rotation as three Euler angles (yaw, pitch, roll) and lerp each number independently. The second is to store it as a 3\times 3 rotation matrix and lerp the nine entries. Watch them break:

The cure is to interpolate on the natural home of rotations. A unit quaternion is a point on the unit sphere in four dimensions, and every point on that sphere is a valid rotation — there is no "off the manifold" to fall through. Interpolation just becomes: walk from one point on the sphere to another along the shortest arc.

Unit quaternions double-cover the rotations

A rotation of angle \alpha about a unit axis \mathbf{\hat n} is the unit quaternion

q = \left(\cos\tfrac{\alpha}{2},\; \sin\tfrac{\alpha}{2}\,\mathbf{\hat n}\right).

Notice the half-angle. Because of it, turning all the way around (\alpha \to \alpha + 2\pi) sends q \to -q, yet it is the same physical orientation. So q and -q — antipodal points on the 4-sphere — name one and the same rotation. This is the famous double cover.

This is not a curiosity — it is the single fact that most rotation-interpolation bugs come from. When you blend q_0 toward q_1, the engine has a choice of two arcs (toward q_1 or toward -q_1), and only one of them is the short way. Pick wrong and a gentle 90^\circ turn will swing the long way round through 270^\circ.

SLERP: the constant-speed arc

Let \theta be the angle between the two unit quaternions on the sphere, given by \cos\theta = q_0 \cdot q_1 (the four-component dot product). Spherical linear interpolation walks the great-circle arc between them:

Constant angular velocity is the whole point. It means equal steps in t produce equal turns — the rotation reads as a single, even sweep, exactly what "linear" between two orientation keys should feel like. The two weights are just the sine-rule coefficients that split the arc; as \theta \to 0 they collapse to (1-t) and t, so SLERP degrades gracefully into an ordinary lerp for nearby orientations (where \sin\theta would otherwise divide by near-zero — real code falls back to NLERP there).

Before you SLERP, check the sign of the dot product. If q_0 \cdot q_1 < 0, the two quaternions are on opposite hemispheres of the 4-sphere, so the great-circle arc between them as written goes the long way — more than 180^\circ around. Because q_1 and -q_1 are the same rotation (double cover!), the fix is free: negate one of them, q_1 \leftarrow -q_1, and interpolate to the antipode instead. Now \cos\theta \ge 0, \theta \le 90^\circ, and you take the short arc.

Skip this one line and a crisp 90^\circ head-turn can whip around the 270^\circ back way — the classic "my character's head spun like an owl" bug. Every production SLERP starts with:

function slerp(q0: Quat, q1: Quat, t: number): Quat { let d = dot(q0, q1); if (d < 0) { q1 = negate(q1); d = -d; } // shortest path via the double cover if (d > 0.9995) return normalize(lerp(q0, q1, t)); // nearly parallel: nlerp fallback const theta = Math.acos(d); const s = Math.sin(theta); const w0 = Math.sin((1 - t) * theta) / s; const w1 = Math.sin(t * theta) / s; return add(scale(q0, w0), scale(q1, w1)); }

Worked example: SLERP two quaternions 90° apart

Take q_0 = (1,0,0,0) (no rotation) and a quaternion representing a 90^\circ rotation about the x-axis, q_1 = (\cos 45^\circ,\, \sin 45^\circ, 0, 0) = (0.7071, 0.7071, 0, 0). Their dot product is \cos\theta = 0.7071, so the sphere angle is \theta = 45^\circ (half the 90^\circ physical turn — that half-angle again). It is positive, so no sign flip is needed.

Evaluate at t = 0.5. The weights are equal:

w_0 = w_1 = \frac{\sin(0.5 \cdot 45^\circ)}{\sin 45^\circ} = \frac{\sin 22.5^\circ}{\sin 45^\circ} = \frac{0.3827}{0.7071} = 0.5412.

So q(0.5) = 0.5412\,(q_0 + q_1) = 0.5412\,(1.7071,\,0.7071,\,0,\,0) = (0.9239,\,0.3827,\,0,\,0). Its scalar part is \cos\tfrac{\alpha}{2} = 0.9239 = \cos 22.5^\circ, so the rotation angle is \alpha = 45^\circexactly halfway between 0^\circ and 90^\circ. That is constant angular speed, confirmed.

Now compare NLERP at t = 0.25. True SLERP gives 0.25 \times 90^\circ = 22.5^\circ. NLERP forms the straight-line blend (1-t)q_0 + t\,q_1 = (0.9268,\,0.1768,\,0,\,0) and normalizes it to (0.9823,\,0.1874,\,0,\,0). Its scalar part 0.9823 = \cos 10.78^\circ gives a rotation of 21.6^\circ — nearly a whole degree behind where it should be. NLERP lags early in the arc and rushes through the middle: the endpoints and the exact midpoint are perfect, but the speed in between is not constant.

SLERP vs NLERP, seen as a speed graph

The cleanest way to see the difference is to plot the angle turned so far against the parameter t, for the same two orientations 90^\circ apart. SLERP is a dead-straight line from 0^\circ to 90^\circ — equal t steps, equal turns, constant speed. NLERP (normalized lerp) is a gentle S-curve: it starts slower than SLERP, speeds up through the middle, then slows again. Both agree at the two ends and at the exact midpoint; everywhere else NLERP is off. Drag t and read the gap between the two curves.

For a small arc the S-curve hugs the straight line and NLERP is a fine, cheap substitute — no \arccos, no sine, no division by a tiny \sin\theta. That is why real engines SLERP for big blends and NLERP for the thousands of tiny per-frame ones.

NLERP: the cheap cousin

Normalized linear interpolation is exactly what it says: lerp the four components on the straight chord, then normalize back onto the sphere.

\operatorname{nlerp}(q_0, q_1, t) = \frac{(1-t)\,q_0 + t\,q_1}{\lVert (1-t)\,q_0 + t\,q_1\rVert}.

Because the path is right and only the timing along it is slightly bowed, NLERP is the default for real-time skeletal animation, where you blend many bone rotations per frame and the tiny arc between adjacent poses makes the speed error invisible.

SQUAD: splining through many orientation keys

SLERP joins two keys. An animation track has many orientation keys, and if you just SLERP from each key to the next you get straight arcs with a sharp kink at every key — the angular velocity jumps, and the motion visibly stutters at each keyframe. The fix is the rotational analogue of a Catmull–Rom spline: SQUAD (Spherical and QUADrangle interpolation).

SQUAD is to orientation what a Catmull–Rom (or Bézier) spline is to position: a C^1 curve pinned to the keys, built entirely out of SLERPs of SLERPs. The control quaternions s_i are the quaternion tangent handles.

Interpolating a whole orientation track

Putting it together, evaluating an object's rotation at an arbitrary time t along a keyframed track is a short recipe:

The same track can feed a camera, a bone, or a prop; the only per-object work is the two lines of blending. That is why quaternions won: correct rotation interpolation reduces to a dot product, a sign check, and a couple of SLERPs.

For a single static rotation, a matrix is fine. Quaternions earn their keep the moment you need to interpolate, compose repeatedly, or store many keys. They are four numbers instead of nine or sixteen; composing them is one quaternion product instead of a matrix multiply; they never drift into a non-rotation the way accumulated matrix products do (you just renormalize four numbers); and — the killer feature here — they have SLERP, a clean, closed-form, constant-speed blend with no gimbal lock. A modern engine keeps orientations as quaternions everywhere and only expands to a matrix at the last moment, to actually transform vertices.