Interpolation

A quaternion represents an orientation; it defines a coordinate system relative to another. If we have two orientations, we can consider the orientation of the same object represented in both coordinate systems.

What if we want to generate an orientation that is halfway between them, for some definition of halfway? Or even better, consider an arbitrary interpolation between two orientations, so that we can watch an object move from one orientation to another. This would allow us to see an object smoothly moving from one orientation to another.

This is one more trick we can play with quaternions that we cannot with matrices. Linearly-interpolating the components of matrices does not create anything that resembles an inbetween transformation. However, linearly interpolating a pair of quaternions does. As long as you normalize the results.

The Interpolation tutorial demonstrates this. The Q, W, E, R, T, Y, and U keys cause the ship to interpolate to a new orientation. Each key corresponds to a particular orientation, and the Q key is the initial orientation.

We can see that there are some pretty reasonable looking transitions. The transition from Q to W, for example. However, there are some other transitions that do not look so good; the Q to E transition. What exactly is going on?

The Long Path

Unit quaternions represent orientations, but they are also vector directions. Specifically, directions in a four-dimensional space. Being unit vectors, they represent points on a 4D sphere of radius one. Therefore, the path between two orientations can be considered to be simply moving from one direction to another on the surface of the 4D sphere.

While unit quaternions do represent orientations, a quaternion is not a unique representation of an orientation. That is, there are multiple quaternions that represent the same orientation. Well, there are two.

The conjugate of a quaternion, its inverse orientation, is the negation of the vector part of the quaternion. If you negate all four components however, you get something quite different: the same orientation as before. Negating a quaternion does not affect its orientation.

While the two quaternions represent the same orientation, they are not the same as far as interpolation is concerned. Consider a two-dimensional case:

Figure 8.5. Interpolation Directions

Interpolation Directions

If the angle between the two quaternions is greater than 90°, then the interpolation between them will take the long path between the two orientations. Which is what we see in the Q to E transition. The orientation R is the negation of E; if you try to interpolate between them, nothing changes. The Q to R transition looks much better behaved.

This can be detected easily enough. If the 4-vector dot product between the two quaternions is less than zero, then the long path will be taken. If you want to prevent the long path from being used, simply negate one of the quaternions before interpolating if you detect this. Similarly, if you want to force the long path, then ensure that the angle is greater than 90° by negating a quaternion if the dot product is greater than zero.

Interpolation Speed

There is another problem. Notice how fast the Q to E interpolation is. It starts off slow, then rapidly spins around, then slows down towards the end. Why does this happen?

The linear interpolation code looks like this:

Example 8.5. Quaternion Linear Interpolation

glm::fquat Lerp(const glm::fquat &v0, const glm::fquat &v1, float alpha)
{
    glm::vec4 start = Vectorize(v0);
    glm::vec4 end = Vectorize(v1);
    glm::vec4 interp = glm::mix(start, end, alpha);
    interp = glm::normalize(interp);
    return glm::fquat(interp.w, interp.x, interp.y, interp.z);
}

Note

GLM's quaternion support does something unusual. The W component is given first to the fquat constructor. Be aware of that when looking through the code.

The Vectorize function simply takes a quaternion and returns a vec4; this is necessary because GLM fquat do not support many of the operations that GLM vec4's do. In this case, the glm::mix function, which performs component-wise linear interpolation.

Each component of the vector is interpolated separately from the rest. The quaternion for Q is (0.7071f, 0.7071f, 0.0f, 0.0f), while the quaternion for E is (-0.4895f, -0.7892f, -0.3700f, -0.02514f). In order for the first componet of Q to get to E's first component, it will have to go through zero.

When the alpha is around 0.5, half-way through the movement, the resultant vector before normalization is very small. But the vector itself is not what provides the orientation; the direction of the 4D vector is. Which is why it moves very fast in the middle: the direction is changing rapidly.

In order to get smooth interpolation, we need to interpolate based on the direction of the vectors. That is, we interpolate along the angle between the two vectors. This kind of interpolation is called spherical linear interpolation or slerp.

To see the difference this makes, press the SpaceBar; this toggles between regular linear interpolation and slerp. The slerp version is much smoother.

The code for slerp is rather complex:

Example 8.6. Spherical Linear Interpolation

glm::fquat Slerp(const glm::fquat &v0, const glm::fquat &v1, float alpha)
{
    float dot = glm::dot(v0, v1);
    
    const float DOT_THRESHOLD = 0.9995f;
    if (dot > DOT_THRESHOLD)
        return Lerp(v0, v1, alpha);
    
    glm::clamp(dot, -1.0f, 1.0f);
    float theta_0 = acosf(dot);
    float theta = theta_0*alpha;
    
    glm::fquat v2 = v1 - v0*dot;
    v2 = glm::normalize(v2);
    
    return v0*cos(theta) + v2*sin(theta);
}