GLM 4.5 Air

Complete Guide to Quaternion Interpolation

Learn the correct way to interpolate quaternions for smooth rotations. This complete guide covers SLERP implementation, edge cases, and optimization techniques for 3D graphics and game development.

Question

How to properly calculate an intermediate quaternion between two quaternions for rotation interpolation?

I’m working with quaternions that represent rotations, and I need to find an intermediate quaternion q3 such that q3 = q1 + (q2 - q1)*ratio where ratio is a float between 0 and 1.

I’ve tried several approaches:

  1. Naive linear interpolation:
cpp
q3 = {
    q1[0] + (q2[0] - q1[0])*ratio,
    q1[1] + (q2[1] - q1[1])*ratio,
    q1[2] + (q2[2] - q1[2])*ratio,
    q1[3] + (q2[3] - q1[3])*ratio,
};
  1. Using SLERP (Spherical Linear Interpolation):
cpp
vec4 quaternionSlerp(vec4 q1, vec4 q2, float t)
{
    float cosHalfTheta = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];

    if (std::fabs(cosHalfTheta) >= 1.0F) {
        return q1;
    }

    float halfTheta = std::acos(cosHalfTheta);
    float sinHalfTheta = std::sqrt(1.0F - cosHalfTheta*cosHalfTheta);

    if (std::fabs(sinHalfTheta) < 1e-4F) {
        return {
            q1[0] * 0.5F + q2[0] * 0.5F,
            q1[1] * 0.5F + q2[1] * 0.5F,
            q1[2] * 0.5F + q2[2] * 0.5F,
            q1[3] * 0.5F + q2[3] * 0.5F
        };
    }
    else {
        float ratioA = std::sin((1.0F - t) * halfTheta) / sinHalfTheta;
        float ratioB = std::sin(t * halfTheta) / sinHalfTheta;

        return {
            q1[0] * ratioA + q2[0] * ratioB,
            q1[1] * ratioA + q2[1] * ratioB,
            q1[2] * ratioA + q2[2] * ratioB,
            q1[3] * ratioA + q2[3] * ratioB
        };
    }
}
  1. Using quaternion multiplication and conjugate:
cpp
vec4 multiplyQuaternions(vec4 q1, vec4 q2)
{
    float w1 = q1[3], x1 = q1[0], y1 = q1[1], z1 = q1[2];
    float w2 = q2[3], x2 = q2[0], y2 = q2[1], z2 = q2[2];

    return {
        w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
        w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
        w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
        w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
    };
}

vec4 intermediateQuaternion(vec4 q1, vec4 q2, float ratio)
{
    float cosHalfTheta = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];
    float halfTheta = std::acos(cosHalfTheta);
    float sinHalfTheta = std::sqrt(1.0F - cosHalfTheta*cosHalfTheta);

    ratio = std::sin(ratio * halfTheta) / sinHalfTheta;

    vec4 qDifference = multiplyQuaternions(q1, { -q2[0], -q2[1], -q2[2], q2[3] });
    return multiplyQuaternions(q1, { qDifference[0]*ratio, qDifference[1]*ratio, qDifference[2]*ratio, qDifference[3]*ratio });
}

None of these approaches seem to produce the desired result. What is the correct way to calculate an intermediate quaternion between two quaternions for rotation interpolation?

GLM 4.5 Air

How to Properly Calculate an Intermediate Quaternion Between Two Quaternions

The correct way to calculate an intermediate quaternion between two quaternions for rotation interpolation is to use Spherical Linear Interpolation (SLERP), not simple linear interpolation. SLERP ensures the interpolated quaternion remains on the unit sphere and represents a valid rotation throughout the interpolation process.

Contents

What Are Quaternions and Why They’re Used for Rotations

Quaternions are a mathematical extension of complex numbers, consisting of one real component and three imaginary components: q=w+xi+yj+zkq = w + xi + yj + zk. When representing rotations, we typically use unit quaternions where w2+x2+y2+z2=1w^2 + x^2 + y^2 + z^2 = 1.

Quaternions offer several advantages over other rotation representations:

  • They avoid gimbal lock
  • They provide smooth interpolation
  • They’re computationally efficient for composition
  • They don’t suffer from singularities

A quaternion represents a rotation of 2θ2\theta degrees around an axis v=(x,y,z)\mathbf{v} = (x, y, z) where:

  • θ=arccos(w)\theta = \arccos(w)
  • The axis is normalized to v=(x,y,z)/1w2\mathbf{v} = (x, y, z) / \sqrt{1-w^2}

Why Linear Interpolation Fails for Quaternions

Your first approach using naive linear interpolation doesn’t work because quaternions lie on a 4D unit sphere. Linear interpolation in Euclidean space doesn’t preserve this constraint:

cpp
// This produces invalid rotation quaternions
q3 = {
    q1[0] + (q2[0] - q1[0])*ratio,
    q1[1] + (q2[1] - q1[1])*ratio,
    q1[2] + (q2[2] - q1[2])*ratio,
    q1[3] + (q2[3] - q1[3])*ratio,
};

The resulting quaternion won’t be normalized, leading to distorted rotations. Furthermore, the path taken by linear interpolation doesn’t follow the great circle (shortest path) on the quaternion sphere.

Proper SLERP Implementation

SLERP (Spherical Linear Interpolation) is the standard method for interpolating between quaternions. It follows the great circle arc between two points on a 4D sphere, ensuring constant angular velocity.

Here’s a corrected SLERP implementation:

cpp
vec4 quaternionSlerp(vec4 q1, vec4 q2, float t)
{
    // Ensure unit quaternions
    q1 = normalize(q1);
    q2 = normalize(q2);
    
    // Compute cosine of angle between quaternions
    float cosHalfTheta = dot(q1, q2);
    
    // If quaternions are very close, use linear interpolation to avoid division by zero
    if (fabs(cosHalfTheta) >= 1.0f - 1e-6f) {
        return normalize(mix(q1, q2, t));
    }
    
    // Ensure we take the shortest path
    if (cosHalfTheta < 0.0f) {
        q2 = -q2;
        cosHalfTheta = -cosHalfTheta;
    }
    
    // Compute interpolation parameters
    float halfTheta = acos(cosHalfTheta);
    float sinHalfTheta = sqrt(1.0f - cosHalfTheta * cosHalfTheta);
    float ratioA = sin((1.0f - t) * halfTheta) / sinHalfTheta;
    float ratioB = sin(t * halfTheta) / sinHalfTheta;
    
    // Perform interpolation
    return {
        q1[0] * ratioA + q2[0] * ratioB,
        q1[1] * ratioA + q2[1] * ratioB,
        q1[2] * ratioA + q2[2] * ratioB,
        q1[3] * ratioA + q2[3] * ratioB
    };
}

Handling Edge Cases in Quaternion Interpolation

Several edge cases need special attention when implementing SLERP:

  1. Identical Quaternions: When q1 and q2 represent the same rotation (or opposite rotations)
  2. Perpendicular Quaternions: When the angle between quaternions is 90°
  3. Very Close Quaternions: When numerically unstable calculations might occur

A robust implementation should handle these cases as shown in the code above. The key improvements over your original implementation are:

  • Normalizing input quaternions
  • Handling the double-cover property (q and -q represent the same rotation)
  • Using linear interpolation when quaternions are very close

SLERP vs. Other Interpolation Methods

While SLERP is the gold standard for quaternion interpolation, other methods exist for different use cases:

Method When to Use Pros Cons
SLERP Smooth rotation interpolation with constant angular velocity Follows shortest path, constant speed Computationally more expensive
LERP Fast interpolation when speed variations are acceptable Simple to implement, faster Non-constant speed, requires renormalization
NLERP Balanced performance and quality Better than LERP, faster than SLERP Still not perfectly constant speed
SQUAD Smooth interpolation of multiple quaternion curves Provides smooth derivatives More complex, requires additional data

For most applications, SLERP is the preferred method when quality is important.

Optimized SLERP Implementation

For performance-critical applications, here’s an optimized SLERP implementation:

cpp
vec4 fastSlerp(vec4 q1, vec4 q2, float t)
{
    // Ensure unit quaternions
    q1 = normalize(q1);
    q2 = normalize(q2);
    
    float cosTheta = dot(q1, q2);
    
    // Handle identical quaternions
    if (cosTheta > 0.9995f) {
        return normalize(mix(q1, q2, t));
    }
    
    // Ensure shortest path
    if (cosTheta < 0.0f) {
        q2 = -q2;
        cosTheta = -cosTheta;
    }
    
    // Use approximation for small angles
    if (cosTheta > 0.995f) {
        float theta = acos(cosTheta);
        float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
        float theta_t = t * theta;
        float sin_theta_t = sin(theta_t);
        
        float scale0 = sin(theta - theta_t) / sinTheta;
        float scale1 = sin_theta_t / sinTheta;
        
        return scale0 * q1 + scale1 * q2;
    }
    
    // Standard SLERP for most cases
    float theta = acos(cosTheta);
    float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
    float theta_t = t * theta;
    float sin_theta_t = sin(theta_t);
    
    float scale0 = sin(theta - theta_t) / sinTheta;
    float scale1 = sin_theta_t / sinTheta;
    
    return scale0 * q1 + scale1 * q2;
}

Practical Considerations

When implementing quaternion interpolation in your project, consider these best practices:

  1. Always normalize quaternions before interpolation: This ensures valid rotations
  2. Handle the double-cover property: Check if q2 should be negated for shortest path
  3. Consider performance: Use approximations when appropriate for your use case
  4. Test edge cases: Verify interpolation works for identical, opposite, and perpendicular quaternions
  5. Visual debugging: When possible, visualize interpolation to verify correctness

Your third approach using quaternion multiplication and conjugate doesn’t work because it’s attempting to apply linear interpolation in a non-linear space. Quaternion multiplication represents composition of rotations, not linear interpolation between them.

Conclusion

To properly calculate an intermediate quaternion between two quaternions:

  1. Use SLERP (Spherical Linear Interpolation) for accurate rotation interpolation
  2. Normalize your quaternions before interpolation
  3. Handle the double-cover property by checking if quaternions should be negated
  4. Consider edge cases like identical quaternions or very close ones
  5. Use approximations in performance-critical applications where appropriate

SLERP ensures your interpolated rotations follow the shortest path on the quaternion sphere with constant angular velocity, which is essential for smooth animation and camera movements in 3D applications.