This page was generated from doc/rotation/slerp.ipynb. Interactive online version: Binder badge.

Spherical Linear Interpolation (Slerp)#

The term “Slerp” for “spherical linear interpolation” (a.k.a. “great arc in-betweening”) has been coined by Shoemake [Sho85], section 3.3. It describes an interpolation (with constant angular velocity) along the shortest path (a.k.a. geodesic) on the unit hypersphere between two quaternions \(q_1\) and \(q_2\). It is defined as:

\begin{equation*} \operatorname{Slerp}(q_1, q_2; u) = q_1 \, \left({q_1}^{-1} q_2\right)^u \end{equation*}

The parameter \(u\) moves from \(0\) (where the expression simplifies to \(q_1\)) to \(1\) (where the expression simplifies to \(q_2\)).

The Wikipedia article for Slerp provides four equivalent ways to describe the same thing:

\begin{align*} \operatorname{Slerp}(q_0, q_1; t) & = q_0 \, \left({q_0}^{-1} q_1\right)^t \\ & = q_1 \, \left({q_1}^{-1} q_0\right)^{1-t} \\ & = \left(q_0 {q_1}^{-1}\right)^{1-t} \, q_1 \\ & = \left(q_1 {q_0}^{-1}\right)^t \, q_0 \end{align*}

Shoemake [Sho85] also provides an alternative formulation (attributed to Glenn Davis):

\begin{equation*} \operatorname{Slerp}(q_1, q_2; u) = \frac{\sin (1-u) \theta}{\sin \theta} q_1 + \frac{\sin u \theta}{\sin \theta} q_2, \end{equation*}

where the dot product \(q_1 \cdot q_2 = \cos \theta\).

Latter equation works for unit-length elements of any arbitrary-dimensional inner product space (i.e. a vector space that also has an inner product), while the preceding equations only work for quaternions.

The Slerp function for quaternions is quite easy to implement …

[1]:
def slerp(one, two, t):
    """Spherical Linear intERPolation."""
    return (two * one.inverse())**t * one

… but for your convenience an implementation is also provided in splines.quaternion.slerp().

Derivation#

Before looking at the general case \(\operatorname{Slerp}(q_0, q_1; t)\), which interpolates from \(q_0\) to \(q_1\), let’s look at the much simpler case of interpolating from the identity \(\boldsymbol{1}\) to some unit quaternion \(q\).

\begin{align*} \boldsymbol{1} &= (1, (0, 0, 0))\\ q &= \left(\cos \frac{\alpha}{2}, \vec{n} \sin \frac{\alpha}{2}\right) \end{align*}

To move along the great arc from \(\boldsymbol{1}\) to \(q\), we simply have to change the angle from \(0\) to \(\alpha\) while the rotation axis \(\vec{n}\) stays unchanged.

\begin{equation*} \operatorname{Slerp}(\boldsymbol{1}, q; t) = \left(\cos \frac{\alpha t}{2}, \vec{n} \sin \frac{\alpha t}{2}\right) = q^t \text{, where } 0 \le t \le 1 \end{equation*}

To generalize this to the great arc from \(q_0\) to \(q_1\), we can start with \(q_0\) and left-multiply an appropriate Slerp using the relative rotation (global frame) \(q_{0,1}\):

\begin{equation*} \operatorname{Slerp}(q_0, q_1; t) = \operatorname{Slerp}(\boldsymbol{1}, q_{0,1}; t) \, q_0 \end{equation*}

Inserting \(q_{0,1} = q_1 {q_0}^{-1}\), we get:

\begin{equation*} \operatorname{Slerp}(q_0, q_1; t) = \left(q_1 {q_0}^{-1}\right)^t \, q_0 \end{equation*}

Alternatively, we can start with \(q_0\) and right-multiply an appropriate Slerp using the relative rotation (local frame) \(q_{0,1} = {q_0}^{-1} q_1\):

\begin{equation*} \operatorname{Slerp}(q_0, q_1; t) = q_0 \operatorname{Slerp}(\boldsymbol{1}, q_{0,1}; t) = q_0 \, \left({q_0}^{-1} q_1\right)^t \end{equation*}

We can also start with \(q_1\), swap \(q_0\) and \(q_1\) in the relative rotation and invert the parameter by using \(1 - t\), leading to the two further alternatives mentioned above.

Visualization#

First, let’s import NumPy

[2]:
import numpy as np

… and a few helper functions from helper.py:

[3]:
from helper import angles2quat, animate_rotations, display_animation

We can now define two example quaternions:

[4]:
q1 = angles2quat(45, -20, -60)
q2 = angles2quat(-45, 20, 30)

Just out of curiosity, let’s use the method rotation_to() to calculate the angle between the two quaternions:

[5]:
np.degrees(q1.rotation_to(q2).angle)
[5]:
123.9513586527906

If this angle is smaller than 180 degrees, we know that we will get the smallest difference in rotation. If it is larger than 180 degrees, we can negate the second quaternion to get a smaller rotation – see canonicalization.

[6]:
ani_times = np.linspace(0, 1, 50)

We show both the original target quaternion and its antipodal point in this animation:

[7]:
ani = animate_rotations({
    'slerp(q1, q2)': slerp(q1, q2, ani_times),
    'slerp(q1, -q2)': slerp(q1, -q2, ani_times),
})
[8]:
display_animation(ani, default_mode='reflect')

Let’s create some still images as well:

[9]:
from helper import plot_rotations
[10]:
plot_times = np.linspace(0, 1, 9)
[11]:
plot_rotations({
    'slerp(q1, q2)': slerp(q1, q2, plot_times),
    'slerp(q1, -q2)': slerp(q1, -q2, plot_times),
}, figsize=(8, 3))
../_images/rotation_slerp_30_0.svg

slerp(q1, q2) and slerp(q1, -q2) move along the same great circle, albeit in different directions. In total, they cover half the circumference of that great circle, which means a rotation angle of 360 degrees. Note that q2 and -q2 represent the same rotation (because of the double cover property).

Piecewise Slerp#

The class PiecewiseSlerp provides a rotation spline that consists of Slerp sections between the given quaternions.

[13]:
s = PiecewiseSlerp([
    angles2quat(0, 0, 0),
    angles2quat(90, 0, 0),
    angles2quat(90, 90, 0),
    angles2quat(90, 90, 90),
], grid=[0, 1, 2, 3, 6], closed=True)
[14]:
ani = animate_rotations({
    'piecewise Slerp': s.evaluate(np.linspace(s.grid[0], s.grid[-1], 100)),
})
[15]:
display_animation(ani, default_mode='loop')

Each section has its own constant angular velocity.

Slerp vs. Nlerp#

While Slerp interpolates along a great arc between two quaternions, it is also possible to interpolate along a straight line (in four-dimensional quaternion space) between those two quaternions. The resulting interpolant is not part of the unit hypersphere, i.e. the interpolated values are not unit quaternions. However, they can be normalized to become unit quaternions. This is called “normalized linear interpolation”, in short Nlerp. The resulting interpolant travels through the same quaternions as Slerp does, but it doesn’t do it with constant angular velocity.

[16]:
[17]:
def lerp(one, two, t):
    """Linear intERPolation."""
    one = np.asarray(one)
    two = np.asarray(two)
    return (1 - t) * one + t * two
[18]:
def nlerp(one, two, t):
    """Normalized Linear intERPolation.

    Linear interpolation in 4D quaternion space,
    normalizing the result.

    """
    if not np.isscalar(t):
        # If t is a list, return a list of unit quaternions
        return [nlerp(one, two, t) for t in t]
    *vector, scalar = lerp(one.xyzw, two.xyzw, t)
    return Quaternion(scalar, vector).normalized()

As a first example, we try an angle below 180 degrees …

[19]:
q1 = angles2quat(-60, 10, -10)
q2 = angles2quat(80, -35, -110)
[20]:
np.degrees(q1.rotation_to(q2).angle)
[20]:
174.5768498146622

… which we can also quickly check by means of the dot product:

[21]:
assert q1.dot(q2) > 0
[22]:
ani_times = np.linspace(0, 1, 50)
[23]:
ani = animate_rotations({
    'Slerp': slerp(q1, q2, ani_times),
    'Nlerp': nlerp(q1, q2, ani_times),
})
[24]:
display_animation(ani, default_mode='reflect')

Again, we plot some still images:

[25]:
plot_rotations({
    'Slerp': slerp(q1, q2, plot_times),
    'Nlerp': nlerp(q1, q2, plot_times),
}, figsize=(8, 3))
../_images/rotation_slerp_51_0.svg

The start and end values are (by definition) the same, the middle one is also the same (due to symmetry). And in between, there are very slight differences. Since the differences are barely visible, we can try a more extreme example:

[26]:
q3 = angles2quat(-170, 0, 45)
q4 = angles2quat(120, -90, -45)
[27]:
np.degrees(q3.rotation_to(q4).angle)
[27]:
268.27205892764954

Please note that this is a rotation by an angle of far more than 180 degrees!

[28]:
assert q3.dot(q4) < 0
[29]:
ani = animate_rotations({
    'Slerp': slerp(q3, q4, ani_times),
    'Nlerp': nlerp(q3, q4, ani_times),
})
[30]:
display_animation(ani, default_mode='reflect')
[31]:
plot_rotations({
    'Slerp': slerp(q3, q4, plot_times),
    'Nlerp': nlerp(q3, q4, plot_times),
}, figsize=(8, 3))
../_images/rotation_slerp_59_0.svg

Now the difference is clearly visible, but depending on the application you might want to limit your rotations to \(\pm 180\) degrees anyway, so this might not be relevant.