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 [Sho85] (section 3.3). 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*}

[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 is quite easy to implement …

[1]:
def slerp(one, two, t):
    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 \, \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
[4]:
q1 = angles2quat(45, -20, -60)
q2 = angles2quat(-45, 20, 30)

The angle between the two quaternions:

[5]:
np.degrees(np.arccos(q1.dot(q2)) * 2)
[5]:
123.9513586527906
[6]:
ani_times = np.linspace(0, 1, 50)
[7]:
ani = animate_rotations({
    'slerp(q1, q2)': slerp(q1, q2, ani_times),
    'slerp(q1, -q2)': slerp(q1, -q2, ani_times),
}, figsize=(6, 3))
[8]:
display_animation(ani, default_mode='reflect')

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.

Let’s create some still images:

[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_28_0.svg

Piecewise Slerp§

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

[12]:
from splines.quaternion import PiecewiseSlerp
[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)),
}, figsize=(3, 3))
[15]:
display_animation(ani, default_mode='loop')

Each section has a constant rotation angle and a 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]:
from splines.quaternion import Quaternion
[17]:
def lerp(one, two, t):
    """Linear interpolation.

    t can go from 0 to 1.

    """
    return (1 - t) * one + t * two
[18]:
def nlerp(one, two, t):
    """Normalized linear interpolation.

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

    t can go from 0 to 1.

    """
    one = np.array(one.xyzw)
    two = np.array(two.xyzw)
    *vector, scalar = lerp(one, two, t)
    return Quaternion(scalar, vector).normalize()
[19]:
q1 = angles2quat(-60, 10, -10)
q2 = angles2quat(80, -35, -110)
[20]:
assert q1.dot(q2) > 0
[21]:
np.degrees(np.arccos(q1.dot(q2)) * 2)
[21]:
174.5768498146622
[22]:
ani_times = np.linspace(0, 1, 50)
[23]:
ani = animate_rotations({
    'Slerp': slerp(q1, q2, ani_times),
    'Nlerp': [nlerp(q1, q2, t) for t in ani_times],
}, figsize=(6, 3))
[24]:
display_animation(ani, default_mode='reflect')

Some still images:

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

Start and end are (by definition) the same, the middle 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]:
assert q3.dot(q4) < 0
[28]:
np.degrees(np.arccos(q3.dot(q4)) * 2)
[28]:
268.2720589276495

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

[29]:
ani = animate_rotations({
    'Slerp': slerp(q3, q4, ani_times),
    'Nlerp': [nlerp(q3, q4, t) for t in ani_times],
}, figsize=(6, 3))
[30]:
display_animation(ani, default_mode='reflect')
[31]:
plot_rotations({
    'Slerp': slerp(q3, q4, plot_times),
    'Nlerp': [nlerp(q3, q4, t) for t in plot_times],
}, figsize=(8, 3))
../_images/rotation_slerp_55_0.svg