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.
Four numbers from an axis and an angle
| Rotation | w | x | y | z |
|---|---|---|---|---|
Multiplying quaternions
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)})";}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)
q and −q are the same rotation
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)})";}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
Why programs prefer quaternions
| Euler angles | 3×3 matrix | Quaternion | |
|---|---|---|---|
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]};After 36,000 steps |q| = 1.000723 After dividing by it = 1.000000
Quaternions in .NET
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.###");Same as turnZ * turnX: True (0, 1, 0) -> (0, 0, 1) Matrix gives the same: (0, 0, 1)
Show a hint
Show the solution
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)})";}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.