<jupyter_start><jupyter_text>This notebook is part of https://github.com/AudioSceneDescriptionFormat/splines, see also https://splines.readthedocs.io/.[back to rotation splines](index.ipynb)  Spherical Linear Interpolation (Slerp)The term "Slerp" for "**s**pherical **l**inear int**erp**olation"(a.k.a. "great arc in-betweening")has been coined byShoemake (1985)(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](https://en.wikipedia.org/wiki/SlerpQuaternion_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 (1985)also provides an alternative formulation(attributed to <NAME>):\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-dimensionalinner 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 ...<jupyter_code>def slerp(one, two, t):
    return (two * one.inverse())**t * one<jupyter_output><empty_output><jupyter_text>... but for your convenience an implementation is also provided in[splines.quaternion.slerp()](../python-module/splines.quaternion.rstsplines.quaternion.slerp).  DerivationBefore 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 interpolatingfrom 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 anglefrom $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)](quaternions.ipynbRelative-Rotation-(Global-Frame-of-Reference))$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)](quaternions.ipynbRelative-Rotation-(Local-Frame-of-Reference))$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 rotationand invert the parameter by using $1 - t$,leading to the two further alternatives mentioned above.  Visualization First, let's import [NumPy](https://numpy.org/) ...<jupyter_code>import numpy as np<jupyter_output><empty_output><jupyter_text>... and a few helper functions from[helper.py](helper.py):<jupyter_code>from helper import angles2quat, animate_rotations, display_animation
q1 = angles2quat(45, -20, -60)
q2 = angles2quat(-45, 20, 30)<jupyter_output><empty_output><jupyter_text>The angle between the two quaternions:<jupyter_code>np.degrees(np.arccos(q1.dot(q2)) * 2)
ani_times = np.linspace(0, 1, 50)
ani = animate_rotations({
    'slerp(q1, q2)': slerp(q1, q2, ani_times),
    'slerp(q1, -q2)': slerp(q1, -q2, ani_times),
}, figsize=(5, 2))
display_animation(ani, default_mode='reflect')<jupyter_output><empty_output><jupyter_text>`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:<jupyter_code>from helper import plot_rotations
plot_times = np.linspace(0, 1, 9)
plot_rotations({
    'slerp(q1, q2)': slerp(q1, q2, plot_times),
    'slerp(q1, -q2)': slerp(q1, -q2, plot_times),
}, figsize=(8, 3))<jupyter_output><empty_output><jupyter_text>Piecewise SlerpThe class[PiecewiseSlerp](../python-module/splines.quaternion.rstsplines.quaternion.PiecewiseSlerp)provides a rotation spline that consists of Slerp sectionsbetween the given quaternions.<jupyter_code>from splines.quaternion import PiecewiseSlerp
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)
ani = animate_rotations({
    'piecewise Slerp': s.evaluate(np.linspace(s.grid[0], s.grid[-1], 100)),
}, figsize=(3, 2))
display_animation(ani, default_mode='loop')<jupyter_output><empty_output><jupyter_text>Each section has a constant rotation angleand a constant angular velocity.  Slerp vs. NlerpWhile *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 "**n**ormalized **l**inear int**erp**olation",in short *Nlerp*.The resulting interpolant travels through the same quaternions as Slerp does,but it doesn't do it with constant angular velocity.<jupyter_code>from splines.quaternion import Quaternion
def lerp(one, two, t):
    """Linear interpolation.
    
    t can go from 0 to 1.
    
    """
    return (1 - t) * one + t * two
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).normalized()
q1 = angles2quat(-60, 10, -10)
q2 = angles2quat(80, -35, -110)
assert q1.dot(q2) > 0
np.degrees(np.arccos(q1.dot(q2)) * 2)
ani_times = np.linspace(0, 1, 50)
ani = animate_rotations({
    'Slerp': slerp(q1, q2, ani_times),
    'Nlerp': [nlerp(q1, q2, t) for t in ani_times],
}, figsize=(5, 2))
display_animation(ani, default_mode='reflect')<jupyter_output><empty_output><jupyter_text>Some still images:<jupyter_code>plot_rotations({
    'Slerp': slerp(q1, q2, plot_times),
    'Nlerp': [nlerp(q1, q2, t) for t in plot_times],
}, figsize=(8, 3))<jupyter_output><empty_output><jupyter_text>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:<jupyter_code>q3 = angles2quat(-170, 0, 45)
q4 = angles2quat(120, -90, -45)
assert q3.dot(q4) < 0
np.degrees(np.arccos(q3.dot(q4)) * 2)<jupyter_output><empty_output><jupyter_text>Please note that this is a rotationby an angle of far more than 180 degrees!<jupyter_code>ani = animate_rotations({
    'Slerp': slerp(q3, q4, ani_times),
    'Nlerp': [nlerp(q3, q4, t) for t in ani_times],
}, figsize=(5, 2))
display_animation(ani, default_mode='reflect')
plot_rotations({
    'Slerp': slerp(q3, q4, plot_times),
    'Nlerp': [nlerp(q3, q4, t) for t in plot_times],
}, figsize=(8, 3))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15180241985780216, 'fraction_numerical': 0.03430210802045653, 'mean_word_length': 5.37867939538584, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 43, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '569252', 'license': 'MIT', 'n_tokens_mistral': 3048, 'n_tokens_neox': 2886, 'n_words': 780, 'path': 'doc/rotation/slerp.ipynb', 'repo_name': 'AudioSceneDescriptionFormat/splines', 'size': '8022'}