<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Rotations in depth
All lessons in this course

Lesson 31 of 44 16 minAdvanced

Quaternions without fear

Store a rotation as four numbers, an axis and half the angle. Rotate points with q·v·q*, combine rotations by multiplying, and see why q and −q are the same rotation.

Quaternions have a reputation for being mysterious, mostly because of how they are usually introduced: as four-dimensional numbers with three different square roots of −1. You do not need any of that to use them well. For rotations, a quaternion is just an axis and an angle, packed into four numbers in a clever way.

Interactive

A quaternion is an axis and half an angle

XYZ
θ / 245°
w = cos θ/20.707
(x, y, z) = sin θ/2 · k(0, 0, 0.707)
|q|1.000
nose (1, 0, 0) →(0, 1, 0)
q = (cos 45°, sin 45° · k). The axis sits in x, y, z, scaled by sin θ/2, and w = cos θ/2 says how far to turn. All four squared add up to 1, which is why the length stays 1.

Four numbers from an axis and an angle

To turn by θ about a unit axis k = (kx, ky, kz), write:

q = (w, x, y, z) = (cos θ/2, kx·sin θ/2, ky·sin θ/2, kz·sin θ/2)

The axis sits in the last three numbers, scaled down by sin θ/2, and w says how far to turn. Because cos² + sin² = 1 and k has length 1, w² + x² + y² + z² = 1: a rotation is always a unit quaternion. No angle is special. There is no lock, and every orientation is reached by a small change of the four numbers from any nearby orientation.

Rotationwxyz
None (identity)1000
90° about Z0.707000.707
180° about X0100
120° about (1, 1, 1)0.50.50.50.5
360° about any axis−1000

Multiplying quaternions

Quaternions have their own multiplication (the Hamilton product, after the mathematician who invented them). You can treat it as a formula to copy: it is the operator * in the Q struct below. What matters is what it does: a · b is the rotation b followed by a, just as with matrices. Like matrices it is associative but not commutative.

To rotate a vector v, write it as a quaternion with w = 0 and compute q · v · q*, where q* = (w, −x, −y, −z) is the conjugate. For a unit quaternion the conjugate is also the inverse, the turn back. You do not need to know why the vector is sandwiched like this, only that the result again has w = 0 and its x, y, z are the rotated vector.

QuaternionRotate.cs
var turnZ = Q.FromAxisAngle(new V3(0, 0, 1), 90);var turnX = Q.FromAxisAngle(new V3(1, 0, 0), 90);Console.WriteLine($"90 deg about Z: {turnZ}");Console.WriteLine($"(1, 0, 0) -> {turnZ.Rotate(new V3(1, 0, 0))}"); var both = turnZ * turnX;                      // turn about X first, then about ZConsole.WriteLine($"X then Z: {both}");foreach (var v in new V3[] { new(1, 0, 0), new(0, 1, 0), new(0, 0, 1) })    Console.WriteLine($"  {v} -> {both.Rotate(v)}"); var (axis, angle) = ToAxisAngle(both);Console.WriteLine($"One turn of {angle:0.#} deg about {axis}"); static (V3 Axis, double Degrees) ToAxisAngle(Q q){    if (q.W < 0) q = -q;                                       // pick the turn of 180 deg or less    double half = Math.Acos(Math.Min(1, q.W)), s = Math.Sin(half);    if (s < 1e-9) return (new V3(1, 0, 0), 0);                 // no turn: any axis will do    return (new V3(q.X / s, q.Y / s, q.Z / s), 2 * half * 180 / Math.PI);} readonly record struct Q(double W, double X, double Y, double Z){    public static Q FromAxisAngle(V3 axis, double degrees)    {        var k = axis.Normalized();        double h = degrees * Math.PI / 360, s = Math.Sin(h);     // half the angle, in radians        return new(Math.Cos(h), k.X * s, k.Y * s, k.Z * s);    }    public static Q operator *(Q a, Q b) => new(               // b first, then a        a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,        a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,        a.W * b.Y - a.X * b.Z + a.Y * b.W + a.Z * b.X,        a.W * b.Z + a.X * b.Y - a.Y * b.X + a.Z * b.W);    public static Q operator -(Q q) => new(-q.W, -q.X, -q.Y, -q.Z);    public static double Dot(Q a, Q b) => a.W * b.W + a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public Q Conjugate => new(W, -X, -Y, -Z);    public double Length => Math.Sqrt(Dot(this, this));    public Q Normalized() { double l = Length; return new(W / l, X / l, Y / l, Z / l); }    public V3 Rotate(V3 v)                                     // q * v * conjugate(q)    {        var p = this * new Q(0, v.X, v.Y, v.Z) * Conjugate;        return new(p.X, p.Y, p.Z);    }    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(W)}, {F(X)}, {F(Y)}, {F(Z)})";} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
90 deg about Z: (0.707, 0, 0, 0.707)
(1, 0, 0) -> (0, 1, 0)
X then Z: (0.5, 0.5, 0.5, 0.5)
  (1, 0, 0) -> (0, 1, 0)
  (0, 1, 0) -> (0, 0, 1)
  (0, 0, 1) -> (1, 0, 0)
One turn of 120 deg about (0.577, 0.577, 0.577)

A quarter turn about X followed by a quarter turn about Z is a single turn of 120° about the diagonal (1, 1, 1), which cycles the axes X → Y → Z → X. Getting the axis and angle back out is just the first formula backwards: half the angle is acos w, and the axis is (x, y, z) divided by sin of that half angle.

q and −q are the same rotation

In the widget, turn the angle up to 360°. The box is back where it started, but q = (−1, 0, 0, 0), not (1, 0, 0, 0). Because the formula uses half the angle, the quaternion goes round only halfway while the object makes a full turn, and needs 720° to come back to where it started. Every rotation therefore has exactly two quaternions, q and −q. Tick “Use −q” to see that flipping all four signs does not move the box.

DoubleCover.cs
var q = Q.FromAxisAngle(new V3(0, 0.6, 0.8), 70);var full = Q.FromAxisAngle(new V3(0, 0, 1), 360);var v = new V3(2, 1, 0.5); Console.WriteLine($"q          = {q}");Console.WriteLine($"-q         = {-q}");Console.WriteLine($"q rotates  {v} -> {q.Rotate(v)}");Console.WriteLine($"-q rotates {v} -> {(-q).Rotate(v)}");Console.WriteLine($"360 deg about Z: {full}, rotates {v} -> {full.Rotate(v)}");Console.WriteLine($"q . (-q) = {Q.Dot(q, -q):0.###}"); readonly record struct Q(double W, double X, double Y, double Z){    public static Q FromAxisAngle(V3 axis, double degrees)    {        var k = axis.Normalized();        double h = degrees * Math.PI / 360, s = Math.Sin(h);     // half the angle, in radians        return new(Math.Cos(h), k.X * s, k.Y * s, k.Z * s);    }    public static Q operator *(Q a, Q b) => new(               // b first, then a        a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,        a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,        a.W * b.Y - a.X * b.Z + a.Y * b.W + a.Z * b.X,        a.W * b.Z + a.X * b.Y - a.Y * b.X + a.Z * b.W);    public static Q operator -(Q q) => new(-q.W, -q.X, -q.Y, -q.Z);    public static double Dot(Q a, Q b) => a.W * b.W + a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public Q Conjugate => new(W, -X, -Y, -Z);    public double Length => Math.Sqrt(Dot(this, this));    public Q Normalized() { double l = Length; return new(W / l, X / l, Y / l, Z / l); }    public V3 Rotate(V3 v)                                     // q * v * conjugate(q)    {        var p = this * new Q(0, v.X, v.Y, v.Z) * Conjugate;        return new(p.X, p.Y, p.Z);    }    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(W)}, {F(X)}, {F(Y)}, {F(Z)})";} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
q          = (0.819, 0, 0.344, 0.459)
-q         = (-0.819, 0, -0.344, -0.459)
q rotates  (2, 1, 0.5) -> (0.214, 2.24, -0.43)
-q rotates (2, 1, 0.5) -> (0.214, 2.24, -0.43)
360 deg about Z: (-1, 0, 0, 0), rotates (2, 1, 0.5) -> (2, 1, 0.5)
q . (-q) = -1

This matters in two places. When you compare two orientations, q and −q must count as equal: test |q₁ · q₂| close to 1 (the dot product of the four numbers), not the numbers themselves. And when you interpolate, the sign decides whether you take the short or the long way round, as the next lesson shows.

Why programs prefer quaternions

Euler angles3×3 matrixQuaternion
Numbers stored394
Gimbal lockYesNoNo
Combine two rotationsHardMatrix product (27 multiplications)Hamilton product (16)
Rotate a pointBuild a matrix firstCheapest (9 multiplications)More work; convert to a matrix for many points
Fix rounding driftNot neededRe-orthonormalise the columnsDivide by the length
Smooth interpolationPoorPoorSlerp (next lesson)

The usual pattern is to keep a quaternion as the master copy of an orientation, combine and interpolate with it, and convert it to a matrix only when many points have to be transformed. Rounding is the other reason. Every product adds a little error, so after thousands of small turns a rotation is no longer exactly a rotation. Here is a 0.1° turn applied 36,000 times in single precision (float, as graphics code uses):

Drift.cs
float h = 0.05f * MathF.PI / 180;                       // half of 0.1 degfloat qs = MathF.Sin(h), qc = MathF.Cos(h);float[] step = { qc, 0, 0.6f * qs, 0.8f * qs };         // 0.1 deg about (0, 0.6, 0.8)float[] q = { 1, 0, 0, 0 }; for (int i = 0; i < 36000; i++)                          // ten full turns    q = Mul(step, q); Console.WriteLine($"After 36,000 steps |q| = {Len(q):0.000000}");float l = Len(q);for (int i = 0; i < 4; i++) q[i] /= l;Console.WriteLine($"After dividing by it  = {Len(q):0.000000}"); static float Len(float[] q) => MathF.Sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); static float[] Mul(float[] a, float[] b) => new[]{    a[0] * b[0] - a[1] * b[1] - a[2] * b[2] - a[3] * b[3],    a[0] * b[1] + a[1] * b[0] + a[2] * b[3] - a[3] * b[2],    a[0] * b[2] - a[1] * b[3] + a[2] * b[0] + a[3] * b[1],    a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0]};
Output
After 36,000 steps |q| = 1.000723
After dividing by it  = 1.000000

The quaternion has grown 0.07% too long, and because q appears twice in q·v·q*, every point it rotates is now scaled by |q|², about 1.0014: a 30 m building would grow by 4 cm. The fix is one division. Repairing a drifted matrix means making three columns unit length and at right angles again (the Gram–Schmidt process), which is more work and easier to get wrong.

Quaternions in .NET

.NET has a ready-made float quaternion in System.Numerics, the same library that provides Vector3 and Matrix4x4. Its CreateFromAxisAngle takes radians, and Quaternion.Concatenate(a, b) means “a, then b”, which is b · a in the Hamilton order used above.

NumericsQuaternion.cs
using System.Numerics; var turnX = Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathF.PI / 2);var turnZ = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2); var both = Quaternion.Concatenate(turnX, turnZ);        // X first, then ZConsole.WriteLine($"Same as turnZ * turnX: {Quaternion.Dot(both, turnZ * turnX) > 0.99999f}");Console.WriteLine($"(0, 1, 0) -> {F(Vector3.Transform(Vector3.UnitY, both))}"); var m = Matrix4x4.CreateFromQuaternion(both);           // for transforming many pointsConsole.WriteLine($"Matrix gives the same: {F(Vector3.Transform(Vector3.UnitY, m))}"); static string F(Vector3 v) => $"({R(v.X)}, {R(v.Y)}, {R(v.Z)})";static string R(float x) => MathF.Abs(x) < 5e-4f ? "0" : x.ToString("0.###");
Output
Same as turnZ * turnX: True
(0, 1, 0) -> (0, 0, 1)
Matrix gives the same: (0, 0, 1)
Quick check

What is the quaternion for a 180° turn about the Z axis?

Quick check

Two stored orientations have quaternions q₁ and q₂ with q₁ · q₂ = −1 (dot product). What does that mean?

Practice

Turn Euler angles into a quaternion by multiplying three single-axis quaternions in the same order as the matrices: q = qZ(yaw) · qY(pitch) · qX(roll). For yaw 30°, pitch 20° and roll 10°, rotate the nose (1, 0, 0) and the up vector (0, 0, 1) with it, and check the answers against the Euler matrix Rz·Ry·Rx.

Show a hint

Use Q.FromAxisAngle for the three single turns. For the matrix, reuse Euler, Mul and the rotation matrices from GimbalLock.cs and write a small Apply.

Show the solution
Solution.cs
double yaw = 30, pitch = 20, roll = 10;var q = Q.FromAxisAngle(new V3(0, 0, 1), yaw)      * Q.FromAxisAngle(new V3(0, 1, 0), pitch)      * Q.FromAxisAngle(new V3(1, 0, 0), roll);var m = Euler(yaw, pitch, roll);Console.WriteLine($"q = {q}"); foreach (var v in new V3[] { new(1, 0, 0), new(0, 0, 1) }){    V3 a = q.Rotate(v), b = Apply(m, v);    Console.WriteLine($"{v}: quaternion {a}, matrix {b}, match {(a - b).Length < 1e-12}");} static V3 Apply(double[,] m, V3 v) => new(    m[0, 0] * v.X + m[0, 1] * v.Y + m[0, 2] * v.Z,    m[1, 0] * v.X + m[1, 1] * v.Y + m[1, 2] * v.Z,    m[2, 0] * v.X + m[2, 1] * v.Y + m[2, 2] * v.Z);static double[,] Euler(double yaw, double pitch, double roll) => Mul(Rz(yaw), Mul(Ry(pitch), Rx(roll)));static double[,] Rx(double d) { double c = Cos(d), s = Sin(d); return new[,] { { 1, 0, 0 }, { 0, c, -s }, { 0, s, c } }; }static double[,] Ry(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, 0, s }, { 0, 1, 0 }, { -s, 0, c } }; }static double[,] Rz(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, -s, 0 }, { s, c, 0 }, { 0, 0, 1 } }; }static double Cos(double d) => Math.Cos(d * Math.PI / 180);static double Sin(double d) => Math.Sin(d * Math.PI / 180);static double[,] Mul(double[,] a, double[,] b){    var r = new double[3, 3];    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)            for (int k = 0; k < 3; k++) r[i, j] += a[i, k] * b[k, j];    return r;} readonly record struct Q(double W, double X, double Y, double Z){    public static Q FromAxisAngle(V3 axis, double degrees)    {        var k = axis.Normalized();        double h = degrees * Math.PI / 360, s = Math.Sin(h);     // half the angle, in radians        return new(Math.Cos(h), k.X * s, k.Y * s, k.Z * s);    }    public static Q operator *(Q a, Q b) => new(               // b first, then a        a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,        a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,        a.W * b.Y - a.X * b.Z + a.Y * b.W + a.Z * b.X,        a.W * b.Z + a.X * b.Y - a.Y * b.X + a.Z * b.W);    public static Q operator -(Q q) => new(-q.W, -q.X, -q.Y, -q.Z);    public static double Dot(Q a, Q b) => a.W * b.W + a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public Q Conjugate => new(W, -X, -Y, -Z);    public double Length => Math.Sqrt(Dot(this, this));    public Q Normalized() { double l = Length; return new(W / l, X / l, Y / l, Z / l); }    public V3 Rotate(V3 v)                                     // q * v * conjugate(q)    {        var p = this * new Q(0, v.X, v.Y, v.Z) * Conjugate;        return new(p.X, p.Y, p.Z);    }    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(W)}, {F(X)}, {F(Y)}, {F(Z)})";} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
q = (0.952, 0.038, 0.189, 0.239)
(1, 0, 0): quaternion (0.814, 0.47, -0.342), matrix (0.814, 0.47, -0.342), match True
(0, 0, 1): quaternion (0.379, 0.018, 0.925), matrix (0.379, 0.018, 0.925), match True

Remember

  • A rotation of θ about the unit axis k is the unit quaternion (cos θ/2, k·sin θ/2).
  • a · b means b, then a; rotate a vector with q · v · q*, where q* = (w, −x, −y, −z) is the inverse.
  • q and −q are the same rotation; compare orientations with |q₁ · q₂| ≈ 1.
  • Quaternions have no gimbal lock, are cheap to combine and are repaired by dividing by their length.
  • In .NET use System.Numerics.Quaternion: angles in radians, Concatenate(a, b) is a then b, and storage order is X, Y, Z, W.
0 of 44 lessons completed