This page is intended to be a brief reference on implementing rotations with quaternions. It is not intended to be a full discussion of the topic, only a reference.

1 Definition

Let i\mathbf{i}, j\mathbf{j}, and k\mathbf{k} be three different basis quaternions. Let i2=j2=k2=ijk=1\mathbf{i}^2 = \mathbf{j}^2 = \mathbf{k}^2 = \mathbf{ijk} = -1.

Quaternion multiplication is associative but not commutative: jiij\mathbf{ji} \ne \mathbf{ij}.

From this we can derive

  • ij=k\mathbf{ij} = \mathbf{k} (because (ij)k=1=kk(\mathbf{ij})\mathbf{k} = -1 = \mathbf{kk})
  • jk=i\mathbf{jk} = \mathbf{i} (because i(jk)=1=ii\mathbf{i}(\mathbf{jk}) = -1 = \mathbf{ii})
  • ki=j\mathbf{ki} = \mathbf{j} (because ki=(ij)(jk)=i(1)k=ik=i(ij)=j=j\mathbf{ki} = (\mathbf{ij})(\mathbf{jk}) = \mathbf{i}(-1)\mathbf{k} = -\mathbf{ik} = -\mathbf{i}(\mathbf{ij}) = --\mathbf{j} = \mathbf{j})
  • ji=k\mathbf{ji} = \mathbf{-k} (because ji=j(jk)=k\mathbf{ji} = \mathbf{j}(\mathbf{jk}) = -\mathbf{k})
  • kj=i\mathbf{kj} = \mathbf{-i} (because kj=k(ki)=i\mathbf{kj} = \mathbf{k}(\mathbf{ki}) = -\mathbf{i})
  • ik=j\mathbf{ik} = \mathbf{-j} (because ik=i(ij)=j\mathbf{ik} = \mathbf{i}(\mathbf{ij}) = -\mathbf{j})

Given a quaternion w+xi+yj+zkw + x \mathbf{i} + y \mathbf{j} + z \mathbf{k}, its multiplicative inverse is wxiyjzkw2+x2+y2+z2\displaystyle \frac{w - x \mathbf{i} - y \mathbf{j} - z \mathbf{k}}{w^2 + x^2 + y^2 + z^2}: that is, the product of those two quaternions is 1.

Thus the quaternion w+xi+yj+zkw + x \mathbf{i} + y \mathbf{j} + z \mathbf{k} are closed under addition, multiplication, and division.

2 Storage as vectors

We will write the quaternion w+xi+yj+zkw + x \mathbf{i} + y \mathbf{j} + z \mathbf{k} as just w;x,y,z\langle w;x,y,z \rangle. This is just one of many notations used in graphics; none seem to have a majority.

The conjugate of w;x,y,z\langle w;x,y,z \rangle is w;x,y,z\langle w;-x,-y,-z \rangle.

The product w1;x1,y1,z1w2;x2,y2,z2\langle w_1;x_1,y_1,z_1 \rangle \langle w_2;x_2,y_2,z_2 \rangle is w1w2x1x2y1y2z1z2;w1x2+x1w2+y1z2z1y2,w1y2+y1w2+z1x2x1z2,w1z2+z1w2+x1y2y1x2\begin{matrix} \langle w_1w_2 - x_1x_2 - y_1y_2 - z_1z_2;\\ w_1x_2 + x_1w_2 + y_1z_2 - z_1y_2,\\ w_1y_2 + y_1w_2 + z_1x_2 - x_1z_2,\\ w_1z_2 + z_1w_2 + x_1y_2 - y_1x_2 \rangle \end{matrix} Notice: the terms above are such that the xx coordinate multiplies xx and ww in both directions and multiplies the other two coordinates in both directions but puts a negative sign on the one that’s out-of-order (xy, yz, and zx are in-order in a wrapping alphabet).

The magnitude of a quaternion qq is qq=w2+x2+y2+z2\sqrt{q^{*} q} = \sqrt{w^2 + x^2+y^2+z^2}. The magnitude is always a scalar (zero x,y,zx,y,z). A normalized quaternion is one with a magnitude of 11.

To multiply a point (x,y,z)(x,y,z) by a quaternion, treat the point as 0;x,y,z\langle 0;x,y,z \rangle and use the quaternion product. Note that this will give a quaternion (non-zero ww), not a point (zero ww). However, qxqq x q^{*} will always be a point if xx is a point.

3 To rotation matrix

To rotate point xx by unit-length quaternion qq we use qxqq x q^{*}. But we generally will want to convert the quaternion to a matrix intead.

The 3×3 rotation matrix of an arbitrary (non-normalized) quaternion can be found using the following three equations: n=w2+x2+y2+z2n = w^2 + x^2 + y^2 + z^2 s={0if n=02notherwises = \begin{cases}0 & \text{if } n=0\\\frac{2}{n} &\text{otherwise}\end{cases} [1s(y2+z2)s(xyzw)s(xz+yw)s(xy+zw)1s(x2+z2)s(yzxw)s(xzyw)s(yz+xw)1s(x2+y2)]\begin{bmatrix} 1-s(y^2+z^2) & s(xy-zw) & s(xz+yw) \\ s(xy+zw) & 1-s(x^2+z^2) & s(yz-xw) \\ s(xz-yw) & s(yz+xw) & 1-s(x^2+y^2) \\ \end{bmatrix} In the special case of a normalized quaternion, n=1n = 1, s=2s=2, and we have [w2+x2y2z22(xyzw)2(xz+yw)2(xy+zw)w2x2+y2z22(yzxw)2(xzyw)2(yz+xw)w2x2y2+z2]\begin{bmatrix} w^2+x^2-y^2-z^2 & 2(xy-zw) & 2(xz+yw) \\ 2(xy+zw) & w^2-x^2+y^2-z^2 & 2(yz-xw) \\ 2(xz-yw) & 2(yz+xw) & w^2-x^2-y^2+z^2 \\ \end{bmatrix} Note we did a bit of conversion: direct substitution would have given us diagonal terms like 12(y2+z2)1-2(y^2+z^2) but n=1n = 1 means we can re-write that as w2+x2+y2+z22(y2+z2)w^2 + x^2 + y^2 + z^2 - 2(y^2+z^2), which simplifies to the matrix given.

There are many terms in the above formulae and thus many of chances for a typo. A quick sanity check is that multiplying any rotation matrix (including the rotation matrix from a random quaternion) by its transpose should result in the identity matrix.

Note that all of the above quaternion rotation formulae only have pairs of terms from the quaternion, meaning if we replace qq with q-q we get the same rotation.

JavaScript code to turn a quaternion into a matrix

Assuming a column-major 4x4 matrix:

const m4fromQ = (q) => { 
  let n = dot(q,q)
  let s = n ? 2/n : 0
  let xx = s*q[1]*q[1], xy = s*q[1]*q[2], xz = s*q[1]*q[3], xw = s*q[1]*q[0]
  let yy = s*q[2]*q[2], yz = s*q[2]*q[3], yw = s*q[2]*q[0]
  let zz = s*q[3]*q[3], zw = s*q[3]*q[0]
  return new Float32Array([
    1-yy-zz, xy+zw, xz-yw, 0,
    xy-zw, 1-xx-zz, yz+xw, 0,
    xz+yw, yz-xw, 1-xx-yy, 0,
    0,0,0,1,
  ])
}

4 From rotation matrix

Given a rotation matrix, we can also derive a quaternion that represents it. Note that there are many such quaternions, as multiplying a quaternion by a scalar does not change the rotation it represents.

The basic approach here is to add cells of the simpler unit-quaternion matrix together to cancel some terms.

  • Add the main diagonal to get 3w2x2y2z23w^2 - x^2 - y^2 - z^2; because w2+x2+y2+z2=1w^2 + x^2 + y^2 + z^2 = 1, this is equivalent to 4w214w^2 - 1, so we can find ww. Then we can subtract antidiagonal pairs (like 2(yzxw)2(yz+xw)=4xw2(yz-xw) - 2(yz+xw) = -4xw) and divide by ww to get the other terms.

    • w=12a0,0+a1,1+a2,2+1w = \frac{1}{2} \sqrt{a_{0,0} + a_{1,1} + a_{2,2} + 1}

    • x=a2,1a1,24wx = \dfrac{a_{2,1} - a_{1,2}}{4w}

    • y=a0,2a2,04wy = \dfrac{a_{0,2} - a_{2,0}}{4w}

    • z=a1,0a0,14wz = \dfrac{a_{1,0} - a_{0,1}}{4w}

    But we can multiply the entire quaternion by a scalar without changing the rotation it represents; multiplying by 4w4w, we get

    • w=a0,0+a1,1+a2,2+1w = a_{0,0} + a_{1,1} + a_{2,2} + 1

    • x=a2,1a1,2x = a_{2,1} - a_{1,2}

    • y=a0,2a2,0y = a_{0,2} - a_{2,0}

    • z=a1,0a0,1z = a_{1,0} - a_{0,1}

  • Subtract two main diagonals from the third to get 3x2w2y2z23x^2 - w^2 - y^2 - z^2, which again is equivalent to 4x214x^2-1 to find xx; then add antidiagonal pairs (like 2(xyzw)+2(xy+zw)=4xy2(xy-zw) + 2(xy+zw) = 4xy) and divide by xx to get the other terms.

    • x=12a0,0a1,1a2,2+1x = \frac{1}{2} \sqrt{a_{0,0} - a_{1,1} - a_{2,2} + 1}

    • y=a0,1+a1,04xy = \dfrac{a_{0,1} + a_{1,0}}{4x}

    • z=a0,2+a2,04xz = \dfrac{a_{0,2} + a_{2,0}}{4x}

    • w=a2,1a1,24xw = \dfrac{a_{2,1} - a_{1,2}}{4x}

    Again, we can multiply all four terms by 4x4x to get a more easily computed quaternion representing the same rotation.

… and so on for the other two axes. In the end, we get four quaternion options:

  • a0,0+a1,1+a2,2+1;a2,1a1,2,a0,2a2,0,a1,0a0,1\langle a_{0,0} + a_{1,1} + a_{2,2} + 1; a_{2,1} - a_{1,2}, a_{0,2} - a_{2,0}, a_{1,0} - a_{0,1}\rangle

  • a2,1a1,2;a0,0a1,1a2,2+1,a0,1+a1,0,a0,2+a2,0\langle a_{2,1} - a_{1,2}; a_{0,0} - a_{1,1} - a_{2,2} + 1, a_{0,1} + a_{1,0}, a_{0,2} + a_{2,0}\rangle

  • a0,2a2,0;a1,0+a0,1,a1,1a0,0a2,2+1,a1,2+a2,1\langle a_{0,2} - a_{2,0}; a_{1,0} + a_{0,1}, a_{1,1} - a_{0,0} - a_{2,2} + 1, a_{1,2} + a_{2,1}\rangle

  • a1,0a0,1;a0,2+a2,0,a1,2+a2,1,a2,2a0,0a1,1+1\langle a_{1,0} - a_{0,1}; a_{0,2} + a_{2,0}, a_{1,2} + a_{2,1}, a_{2,2} - a_{0,0} - a_{1,1} + 1\rangle

Assuming AA is a rotation matrix, each of these is a scalar multiple of the others, so we only need to compute one of them. To optimize numerical precision, we should pick the one with the largest four-term sum.

Consider the rotation matrix [010100001]\begin{bmatrix}0&1&0\\1&0&0\\0&0&-1\end{bmatrix} The four quaternion equations give us

  • 0;0,0,0\langle 0;0,0,0 \rangle
  • 0;2,2,0\langle 0;2,2,0 \rangle
  • 0;2,2,0\langle 0;2,2,0 \rangle
  • 0;0,0,0\langle 0;0,0,0 \rangle

Two of these are useless while the other two are useful. But there’s nothing special about those two; a different rotation [100010001]\begin{bmatrix}-1&0&0\\0&-1&0\\0&0&1\end{bmatrix} results in

  • 0;0,0,0\langle 0;0,0,0 \rangle
  • 0;0,0,0\langle 0;0,0,0 \rangle
  • 0;0,0,0\langle 0;0,0,0 \rangle
  • 0;0,0,4\langle 0;0,0,4 \rangle

In general we must check all four options as any three of them may give zero quaternions. Because of the way we constructed these equations, it is sufficient to check the four-term sum alone.

JavaScript code to turn a matrix into a quaternion

Assuming a column-major 4x4 matrix:

const m4toQ = (m) => {
  let a00 = m[0], a11 = m[5], a22 = m[10]
  if (a00 + a11 + a22 > 0)
    return normalize([a00+a11+a22+1, m[6]-m[9], m[8]-m[2], m[1]-m[4]])
  if ((a00 > a11) && (a00 > a22))
    return normalize([m[6]-m[9], a00-a11-a22+1, m[1]+m[4], m[8]-m[2]])
  if (a11 > a22)
    return normalize([m[8]-m[2], m[1]+m[4], a11-a00-a22+1, m[6]+m[9]])
  return normalize([m[1]-m[4], m[2]+m[8], m[6]+m[9], a22-a00-a11+1])
}

5 Relationship with axis-angle

Rotating θ\theta radians around the unit-vector axis of rotation r\vec r is represented by the quaternion ccos(θ2);sin(θ2)rc \left\langle \cos\left(\frac{\theta}{2}\right) ; \sin\left(\frac{\theta}{2}\right) \vec r \right\rangle where w;v\left\langle w; \vec v \right\rangle is shorthand for w;vx,vy,vz\left\langle w; v_x, v_y, v_z \right\rangle and cc may be any non-zero constant.

Thus, any quaternion w;r\langle w; \vec r\rangle represents a rotation around axis r\vec r by θ=2arctan(rw)\theta = 2\arctan\left(\frac{\|\vec r\|}{w}\right).

6 Slerp

Unit quaternions are used to represent angles primarily to facilitate interpolation of angles. But linearly interpolating unit vectors does not yield other unit vectors, and normalizing interpolated unit vectors causes nonlinear speeds.

With a little trigonometry we can derive an interpolation of unit vectors that progresses across the surface of the unit ball at linear speed. Instead of the lerp (1t)p0+(t)p1(1-t)p_0 + (t)p_1 we use the slerp sin((1t)Ω)sin(Ω)p0+sin((t)Ω)sin(Ω)p1\frac{\sin\big((1-t)\Omega\big)}{\sin(\Omega)}p_0 + \frac{\sin\big((t)\Omega\big)}{\sin(\Omega)}p_1 where Ω\Omega is the angle between p0p_0 and p1p_1: i.e. arccos(p0p1)\arccos(p_0 \cdot p_1). Clearly this is much more expensive to compute than the lerp, so we shouldn’t use it on per-vertex computations, but for per-frame computations like interpolating bone orientations it works quite well.

The slerp is numerically unstable for very small Ω\Omega, but for very small Ω\Omega the slerp and lerp are effectively identical so we can default to a lerp for small angles instead. It is also unstable for Ω\Omega near π\pi, but that is because interpolating between opposite orientations is intrinsically unstable: no numerical trick will make it less so.

Because qq and q-q represent the same orientation, we sometimes need to invert qq to get the best slerp.

Consider the quaternions representing the rotations of 0°, 120°, and 240° around the zz axis:

angle option A option B
1.0;0,0,0\langle 1.0; 0,0,0 \rangle 1.0;0,0,0\langle -1.0; 0,0,0 \rangle
120° 0.5;0.75,0,0\langle 0.5; \sqrt{0.75},0,0 \rangle 0.5;0.75,0,0\langle -0.5; -\sqrt{0.75},0,0 \rangle
240° 0.5;0.75,0,0\langle 0.5; -\sqrt{0.75},0,0 \rangle 0.5;0.75,0,0\langle -0.5; \sqrt{0.75},0,0 \rangle

Slerping between 0°A and 120°A goes the short way around (moving 120°), while slerping between 0°A and 120°B goes the long way (moving 240°). We can tell a short way because its angle is under 180°, meaning its cosine (i.e. dot product) is positive.

If we want to loop though the three points always going the short way around we’d need to go do something like 0°A → 120°A → 240°B → 0°B → 120°B → 240°A → 0°A. Note we flipped half-way through: no fixed sign of the three quaternions will always work, we have to pick pairs of signs that match up.

JavaScript code to slerp unit vectors

Slerp works for unit vectors of any dimension, including quaternions, but it only works for unit vectors; if you have non-unit vectors you must normalize them first. We also have a sign-picking version qlerp for use with quaternions.

const lerp = (t,p0,p1) => add(mul(p0,1-t), mul(p1,t))
const slerp = (t,q0,q1) => {
  let d = dot(q0,q1)
  if (d > 0.9999) return normalize(lerp(t,q0,q1))
  let o = Math.acos(d), den = Math.sin(o)
  return add(mul(q0, Math.sin((1-t)*o)/den), mul(q1, Math.sin(t*o)/den))
}
const qlerp = (t,q0,q1) => {
  let d = dot(q0,q1)
  if (d < 0) { q1 = mul(q1,-1); d = -d; }
  if (d > 0.9999) return normalize(lerp(t,q0,q1))
  let o = Math.acos(d), den = Math.sin(o)
  return add(mul(q0, Math.sin((1-t)*o)/den), mul(q1, Math.sin(t*o)/den))
}

Because Bézier curves and various splines use repeated lerps to find points on the curve, we can use slerp to apply the same ideas to make curves of vectors on the unit ball. Note that while this will create smooth curves, it will not create true Bézier curves and many of the properties of Bézier curves, such as the intermediate control points of de Casteljau’s algorithm defining smaller Bézier curves and so on, do not apply if slerps are used instead of lerps.

JavaScript code for spherical Bézier curves

This is identical to the linear Bézier code, but using slerp/qlerp instead of lerp.

const sbez = (t, ...p) => {
  while(p.length > 1) p = p.slice(1).map((e,i) => slerp(t,p[i],e))
  return p[0]
}
const qbez = (t, ...p) => {
  while(p.length > 1) p = p.slice(1).map((e,i) => qlerp(t,p[i],e))
  return p[0]
}