<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 32 of 44 14 minAdvanced

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.

A walkthrough camera moving between two saved views, a door swing drawn as a sequence of positions, a crane jib turning in a 4D construction sequence: all of them need the orientations between a start A and an end B. Moving a point from A to B is easy: A + t·(B − A), for t from 0 to 1. Orientations are harder. Try the four methods in the widget on each end orientation.

Interactive

From one orientation to another

XYZAB
127°0t = 1
Eulernlerpslerpturned so far
turned so far60.0°
slerp at this t60.0°
|X column|1.000
det1.000
Slerp moves along the shortest arc between A and B at a constant angular speed: at t = 0.50 the box has turned exactly 0.50 of the way. This is what animation, camera paths and 4D sequencing tools use.

Why blending matrices fails

The obvious idea is to blend each of the nine numbers of the matrix. Halfway between “no turn” and “90° about Z”, the first column is halfway between (1, 0, 0) and (0, 1, 0): (0.5, 0.5, 0), which has length 0.707, not 1. The blend is not a rotation. Objects shrink in the middle of the move, and for a turn of 180° the halfway matrix squashes everything flat.

Why blending Euler angles is not much better

Blending yaw, pitch and roll separately does give a rotation at every step. But the path depends on the convention rather than on the two orientations, so it is usually not the shortest one, the speed changes along the way, and near gimbal lock it can swing wildly. It also wraps badly: blending a yaw of 350° to one of 10° goes the long way through 180°, turning 340° instead of 20°.

Nlerp: blend the quaternion and rescale

Blend the four quaternion numbers and divide by the length so the result is a unit quaternion again. This nlerp (normalised lerp) follows the right path, the shortest arc from A to B, and is cheap. Its only flaw is speed: it moves faster in the middle than at the ends, and the larger the turn, the more uneven it is.

Slerp: constant speed on the shortest arc

Unit quaternions lie on a sphere (in four dimensions, but the idea is the same as on a globe). Slerp, spherical linear interpolation, walks along the great-circle arc between A and B at a constant speed. If Ω is the angle between the two quaternions (cos Ω = A·B):

slerp(A, B, t) = A · sin((1 − t)Ω) / sin Ω + B · sin(tΩ) / sin Ω

At t = 0.25 the object has turned exactly a quarter of the way. The angle Ω between the quaternions is half the angle the object turns through, because of the half angle in the quaternion. Two details make slerp robust:

  • 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.
LerpVsSlerp.cs
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)})";}
Output
  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

For a 90° turn nlerp is never more than about 1° behind or ahead of slerp, which nobody notices in a door swing (the exercise measures it exactly). The matrix blend, meanwhile, has shrunk the object to 71% of its size halfway through.

The shortest-path check

In the widget, choose “Yaw 270°” and switch the shortest-path check off. A yaw of 270° is the same orientation as −90°, but its quaternion has w = cos 135° < 0, so it lies on the far side of the sphere from the identity. Without the check, slerp faithfully turns 270° the long way round. With it, slerp uses −B and turns 90° the other way.

ShortestPath.cs
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)})";}
Output
from . to = -0.985
Euler blend, halfway:      yaw 180 deg
Slerp without the check:   yaw 180 deg
Slerp with the check:      yaw 0 deg

Halfway from 350° to 10°, the door should be at 0°. Blending the angle sends it to 180°, and slerp without the check does too. Only slerp with the check turns the 20° a person expects.

Slerp in .NET

System.Numerics provides Quaternion.Slerp and Quaternion.Lerp (which, despite the name, is nlerp: it normalises the result). Both already include the shortest-path check.

DoorSwing.cs
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})");}
Output
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)
Quick check

Halfway between the identity and a 180° turn about Z, what does blending the matrix entries give?

Quick check

Why does slerp replace B with −B when A·B < 0?

Practice

Find the worst speed error of nlerp. For a turn of 90° and one of 170° about Z, sample t from 0 to 1 in steps of 0.01 and find the largest difference between the angle nlerp has turned and the angle slerp has turned. Print both.

Show a hint

Reuse Nlerp, Slerp and TurnedFrom from LerpVsSlerp.cs. Keep the largest absolute difference.

Show the solution
Solution.cs
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)})";}
Output
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.
0 of 44 lessons completed