Interpolating rotations: lerp vs slerp
Blend smoothly from one orientation to another for camera paths, door swings and construction sequences. See why blending matrices or angles goes wrong, and how slerp turns at a constant speed along the shortest arc.
Why blending matrices fails
Why blending Euler angles is not much better
Nlerp: blend the quaternion and rescale
Slerp: constant speed on the shortest arc
Shortest path: if A·B < 0, use −B instead. It is the same orientation, but it is on the near side of the sphere, so the turn is at most 180°. Nearly equal ends: when A·B is close to 1, sin Ω is close to 0 and the formula divides by almost nothing. Use nlerp there; for tiny turns the two are the same.
var a = new Q(1, 0, 0, 0);var b = Q.FromAxisAngle(new V3(0, 0, 1), 90); Console.WriteLine(" t nlerp slerp matrix-lerp column length");foreach (double t in new[] { 0, 0.25, 0.5, 0.75, 1 }){ double n = TurnedFrom(a, Nlerp(a, b, t)), s = TurnedFrom(a, Slerp(a, b, t)); double col = Math.Sqrt((1 - t) * (1 - t) + t * t); // first column: (1 - t, t, 0) Console.WriteLine($"{t,4:0.00} {n,6:0.0} deg {s,5:0.0} deg {col:0.000}");} static Q Nlerp(Q a, Q b, double t) => new Q(a.W + (b.W - a.W) * t, a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t, a.Z + (b.Z - a.Z) * t).Normalized(); static Q Slerp(Q a, Q b, double t){ double d = Q.Dot(a, b); if (d < 0) { b = -b; d = -d; } // shortest path if (d > 0.9995) return Nlerp(a, b, t); // nearly equal: avoid dividing by ~0 double om = Math.Acos(d), s = Math.Sin(om); double ka = Math.Sin((1 - t) * om) / s, kb = Math.Sin(t * om) / s; return new(a.W * ka + b.W * kb, a.X * ka + b.X * kb, a.Y * ka + b.Y * kb, a.Z * ka + b.Z * kb);} static double TurnedFrom(Q a, Q q) => 2 * Math.Acos(Math.Min(1, Math.Abs(Q.Dot(a, q)))) * 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)})";}t nlerp slerp matrix-lerp column length 0.00 0.0 deg 0.0 deg 1.000 0.25 21.6 deg 22.5 deg 0.791 0.50 45.0 deg 45.0 deg 0.707 0.75 68.4 deg 67.5 deg 0.791 1.00 90.0 deg 90.0 deg 1.000
The shortest-path check
var from = Q.FromAxisAngle(new V3(0, 0, 1), 350); // a door at 350 deg ...var to = Q.FromAxisAngle(new V3(0, 0, 1), 10); // ... opening to 10 degConsole.WriteLine($"from . to = {Q.Dot(from, to):0.###}"); Console.WriteLine($"Euler blend, halfway: yaw {(350 + 10) / 2.0:0} deg");Console.WriteLine($"Slerp without the check: yaw {Yaw(SlerpRaw(from, to, 0.5)):0} deg");Console.WriteLine($"Slerp with the check: yaw {Yaw(SlerpRaw(from, Q.Dot(from, to) < 0 ? -to : to, 0.5)):0} deg"); static Q SlerpRaw(Q a, Q b, double t) // no shortest-path check{ double om = Math.Acos(Math.Clamp(Q.Dot(a, b), -1, 1)), s = Math.Sin(om); double ka = Math.Sin((1 - t) * om) / s, kb = Math.Sin(t * om) / s; return new(a.W * ka + b.W * kb, a.X * ka + b.X * kb, a.Y * ka + b.Y * kb, a.Z * ka + b.Z * kb);} static double Yaw(Q q) // where the X axis ends up, in plan{ var x = q.Rotate(new V3(1, 0, 0)); double d = Math.Atan2(x.Y, x.X) * 180 / Math.PI; return Math.Abs(d) < 1e-9 ? 0 : (d + 360) % 360;} 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)})";}from . to = -0.985 Euler blend, halfway: yaw 180 deg Slerp without the check: yaw 180 deg Slerp with the check: yaw 0 deg
Slerp in .NET
using System.Numerics; var closed = Quaternion.Identity;var open = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 100 * MathF.PI / 180); for (int frame = 0; frame <= 5; frame++){ float t = frame / 5f; var q = Quaternion.Slerp(closed, open, t); var leaf = Vector3.Transform(new Vector3(0.9f, 0, 0), q); // a 900 mm leaf, hinge at the origin Console.WriteLine($"t = {t:0.0}: {2 * MathF.Acos(MathF.Min(1, q.W)) * 180 / MathF.PI,5:0.0} deg, " + $"leaf end ({leaf.X:0.000}, {leaf.Y:0.000})");}t = 0.0: 0.0 deg, leaf end (0.900, 0.000) t = 0.2: 20.0 deg, leaf end (0.846, 0.308) t = 0.4: 40.0 deg, leaf end (0.689, 0.579) t = 0.6: 60.0 deg, leaf end (0.450, 0.779) t = 0.8: 80.0 deg, leaf end (0.156, 0.886) t = 1.0: 100.0 deg, leaf end (-0.156, 0.886)
Show a hint
Show the solution
var a = new Q(1, 0, 0, 0);foreach (double turn in new[] { 90.0, 170.0 }){ var b = Q.FromAxisAngle(new V3(0, 0, 1), turn); double worst = 0, at = 0; for (int i = 0; i <= 100; i++) { double t = i / 100.0; double err = Math.Abs(TurnedFrom(a, Nlerp(a, b, t)) - TurnedFrom(a, Slerp(a, b, t))); if (err > worst) { worst = err; at = t; } } Console.WriteLine($"{turn} deg turn: nlerp is off by up to {worst:0.00} deg (at t = {at:0.00})");} static Q Nlerp(Q a, Q b, double t) => new Q(a.W + (b.W - a.W) * t, a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t, a.Z + (b.Z - a.Z) * t).Normalized(); static Q Slerp(Q a, Q b, double t){ double d = Q.Dot(a, b); if (d < 0) { b = -b; d = -d; } if (d > 0.9995) return Nlerp(a, b, t); double om = Math.Acos(d), s = Math.Sin(om); double ka = Math.Sin((1 - t) * om) / s, kb = Math.Sin(t * om) / s; return new(a.W * ka + b.W * kb, a.X * ka + b.X * kb, a.Y * ka + b.Y * kb, a.Z * ka + b.Z * kb);} static double TurnedFrom(Q a, Q q) => 2 * Math.Acos(Math.Min(1, Math.Abs(Q.Dot(a, q)))) * 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 turn: nlerp is off by up to 0.92 deg (at t = 0.22) 170 deg turn: nlerp is off by up to 6.75 deg (at t = 0.24)
Remember
Blending matrix entries does not give rotations: objects shrink, and a 180° blend collapses. Blending Euler angles gives rotations, but along a path set by the convention, at uneven speed and with wrap-around trouble. Nlerp (blend the quaternion, then normalise) follows the shortest arc; it is cheap but faster in the middle. Slerp turns at constant speed along the shortest arc: A·sin((1 − t)Ω)/sin Ω + B·sin(tΩ)/sin Ω. Flip B to −B when A·B < 0, and fall back to nlerp when A and B are nearly equal.