This page was generated from doc/rotation/naive-4d-interpolation.ipynb. Interactive online version: Binder badge.

Naive 4D Quaternion Interpolation#

This method for interpolating rotations is normally not recommended. But it might still be interesting to try it out …

Since quaternions form a vector space (albeit a four-dimensional one), all methods for Euclidean splines can be applied. However, even though rotations can be represented by unit quaternions, which are a subset of all quaternions, this subset is not a Euclidean space. All unit quaternions form the unit hypersphere \(S^3\) (which is a curved space), and each point on this hypersphere uniquely corresponds to a rotation.

When we convert our desired rotation “control points” to quaternions and naively interpolate in 4D quaternion space, the interpolated quaternions are in general not unit quaternions, i.e. they are not part of the unit hypersphere and they don’t correspond to a rotation. In order to force them onto the unit hypersphere, we can normalize them, though, which projects them onto the unit hypersphere.

Note that this is a very crude form of interpolation and it might result in unexpected curve shapes. Especially the temporal behavior might be undesired.

If, for some application, more speed is essential, non-spherical quaternion splines will undoubtedly be faster than angle interpolation, while still free of axis bias and gimbal lock.

Shoemake [Sho85], section 5.4

Abandoning the unit sphere, one could work with the four-dimensional Euclidean space of arbitrary quaternions. How do standard interpolation methods applied there behave when mapped back to matrices? Note that we now have little guidance in picking the inverse image for a matrix, and that cusp-free \(\mathbf{R}^4\) paths do not always project to cusp-free \(S^3\) paths.

Shoemake [Sho85], section 6

[1]:
import numpy as np
[2]:
import splines
[3]:

As always, we use a few helper functions from helper.py:

[4]:
from helper import angles2quat, animate_rotations, display_animation
[5]:
rotations = [
    angles2quat(0, 0, 0),
    angles2quat(0, 0, 45),
    angles2quat(90, 90, 0),
    angles2quat(180, 0, 90),
]

We use xyzw coordinate order here (because it is more common), but since the 4D coordinates are independent, we could as well use wxyz order (or any order, for that matter) with identical results (apart from rounding errors).

However, for illustrating the non-normalized case, we rely on the implicit conversion from xyzw coordinates in the function animate_rotations().

[6]:
rotations_xyzw = [q.xyzw for q in rotations]

As an example we use splines.CatmullRom here, but any Euclidean spline could be used.

[7]:
s = splines.CatmullRom(rotations_xyzw, endconditions='closed')
[8]:
times = np.linspace(s.grid[0], s.grid[-1], 100)
[9]:
interpolated_xyzw = s.evaluate(times)
[10]:
normalized = [
    Quaternion(w, (x, y, z)).normalized()
    for x, y, z, w in interpolated_xyzw]

For comparison, we also create a splines.quaternion.CatmullRom instance:

[11]:
spherical_cr = splines.quaternion.CatmullRom(rotations, endconditions='closed')
[12]:
ani = animate_rotations({
    'normalized 4D interp.': normalized,
    'spherical interp.': spherical_cr.evaluate(times),
})
display_animation(ani, default_mode='loop')

In case you are wondering what would happen if you forget to normalize the results, let’s also show the non-normalized data:

[13]:
ani = animate_rotations({
    'normalized': normalized,
    'not normalized': interpolated_xyzw,
})
display_animation(ani, default_mode='loop')

Obviously, the non-normalized values are not pure rotations.

To get a different temporal behavior, let’s try using centripetal parameterization. Note that this guarantees the absence of cusps and self-intersections in the 4D curve, but this guarantee doesn’t extend to the projection onto the unit hypersphere.

[14]:
s2 = splines.CatmullRom(rotations_xyzw, alpha=0.5, endconditions='closed')
[15]:
times2 = np.linspace(s2.grid[0], s2.grid[-1], len(times))
[16]:
normalized2 = [
    Quaternion(w, (x, y, z)).normalized()
    for x, y, z, w in s2.evaluate(times2)]
[17]:
ani = animate_rotations({
    'uniform': normalized,
    'centripetal': normalized2,
})
display_animation(ani, default_mode='loop')

Let’s also try arc-length parameterization with the UnitSpeedAdapter:

[18]:
s3 = splines.UnitSpeedAdapter(s2)
times3 = np.linspace(s3.grid[0], s3.grid[-1], len(times))
[19]:
normalized3 = [
    Quaternion(w, (x, y, z)).normalized()
    for x, y, z, w in s3.evaluate(times3)]

The arc-length parameterized spline has a constant speed in 4D quaternion space, but that doesn’t mean it has a constant angular speed!

For comparison, we also create a rotation spline with constant angular speed:

[20]:
s4 = splines.UnitSpeedAdapter(
    splines.quaternion.CatmullRom(
        rotations, alpha=0.5, endconditions='closed'))
times4 = np.linspace(s4.grid[0], s4.grid[-1], len(times))
[21]:
ani = animate_rotations({
    'const. 4D speed': normalized3,
    'const. angular speed': s4.evaluate(times4),
})
display_animation(ani, default_mode='loop')

The difference is subtle, but it is definitely visible. More extreme examples can certainly be found.