GLM 4.5 Air

SLERP for Quaternions: Complete Implementation Guide

Master quaternion interpolation with SLERP. Learn the correct implementation for smooth 3D rotations, handling shortest paths and avoiding common pitfalls in rotation interpolation.

Question

How do I calculate an intermediate quaternion between two quaternions using a ratio parameter?

I’m working with quaternions that represent rotations, and I need to find an intermediate quaternion q3 between two quaternions q1 and q2 based on a ratio parameter (a float between 0 and 1).

I initially tried a naive linear interpolation approach:

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,
};

Then I tried using SLERP (Spherical Linear Interpolation):

cpp
vec4 quaternionSlerp(vec4 q1, vec4 q2, float t)
{
    // Based on http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm
    float cosHalfTheta = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];

    // if q1=q2 or q1=-q2 then theta = 0 and we can return q1
    if (std::fabs(cosHalfTheta) >= 1.0F) {
        return q1;
    }

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

    // If theta = 180 degrees then result is not fully defined
    // We could rotate around any axis normal to q1 or q2
    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 {
        // Default quaternion calculation
        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
        };
    }
}

I also tried using quaternion multiplication and conjugation:

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,  // X
        w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,  // Y
        w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,  // Z
        w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2   // W
    };
}

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 using a ratio parameter?

GLM 4.5 Air

How to Calculate an Intermediate Quaternion Between Two Quaternions Using a Ratio Parameter

Brief Answer:
The correct way to calculate an intermediate quaternion between two quaternions using a ratio parameter is to use Spherical Linear Interpolation (SLERP). Unlike naive linear interpolation which doesn’t maintain the unit quaternion property, SLERP provides constant angular velocity and shortest path interpolation. Your SLERP implementation is mostly correct but needs to handle the shortest path by negating one quaternion when the dot product is negative, ensuring you take the shortest rotational path.

Contents

Understanding Quaternion Interpolation

Quaternions represent rotations in 3D space, and interpolating between them requires special consideration because they exist on a 4D unit sphere. Unlike simple vectors, quaternions have specific mathematical properties that must be preserved during interpolation.

Why Linear Interpolation Doesn’t Work

Your first approach using 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,
};

This approach has two major issues:

  1. It doesn’t preserve the unit length property of quaternions
  2. It doesn’t provide constant angular velocity, resulting in uneven motion

The interpolated quaternion would need to be renormalized, and even then, the motion wouldn’t be smooth in terms of angular velocity.

The Geometry of Quaternion Space

Quaternions representing rotations lie on a 4D hypersphere. To interpolate between two points on this hypersphere with constant angular velocity, we need to follow the great arc connecting them - this is exactly what SLERP does.


Proper SLERP Implementation

Your SLERP implementation is close to correct but missing one crucial element: ensuring the shortest path. Here’s a complete implementation:

cpp
vec4 quaternionSlerp(vec4 q1, vec4 q2, float t)
{
    // Ensure quaternions are normalized
    q1 = normalize(q1);
    q2 = normalize(q2);
    
    // Calculate cosine of angle between quaternions
    float cosTheta = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];
    
    // If quaternions are very close, use linear interpolation to avoid division by small numbers
    if (cosTheta > 0.9995f) {
        float inv_t = 1.0f - t;
        return normalize({
            q1[0] * inv_t + q2[0] * t,
            q1[1] * inv_t + q2[1] * t,
            q1[2] * inv_t + q2[2] * t,
            q1[3] * inv_t + q2[3] * t
        });
    }
    
    // Ensure we take the shortest path
    if (cosTheta < 0.0f) {
        q2 = {-q2[0], -q2[1], -q2[2], -q2[3]};
        cosTheta = -cosTheta;
    }
    
    // Calculate interpolation parameters
    float theta = acos(cosTheta);
    float sinTheta = sin(theta);
    float w1 = sin((1.0f - t) * theta) / sinTheta;
    float w2 = sin(t * theta) / sinTheta;
    
    // Interpolate
    return {
        q1[0] * w1 + q2[0] * w2,
        q1[1] * w1 + q2[1] * w2,
        q1[2] * w1 + q2[2] * w2,
        q1[3] * w1 + q2[3] * w2
    };
}

The key addition is the check for negative dot product, which allows us to take the shortest rotational path by negating one quaternion when necessary.


Common Issues and Solutions

Issue 1: Not Handling the Shortest Path

When the dot product between two quaternions is negative, it means the angle between them is greater than 180 degrees. In this case, you should negate one quaternion to ensure you’re taking the shortest rotational path.

Issue 2: Quaternion Normalization

Quaternions must be unit quaternions to represent valid rotations. Always normalize your quaternions before interpolation.

Issue 3: Numerical Stability

When the angle between quaternions is very small (or zero), the SLERP formula can become numerically unstable. In these cases, linear interpolation with normalization (NLERP) is a better choice.

Issue 4: Extrapolation Beyond [0,1]

Your ratio parameter should typically be between 0 and 1. If you need to extrapolate beyond this range, note that the behavior may not be what you expect.


Alternative Interpolation Methods

Normalized Linear Interpolation (NLERP)

For performance-critical applications where perfect constant velocity isn’t essential, you can use NLERP:

cpp
vec4 quaternionNlerp(vec4 q1, vec4 q2, float t)
{
    // Ensure shortest path
    float dot = q1[3]*q2[3] + q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2];
    if (dot < 0.0f) {
        q2 = {-q2[0], -q2[1], -q2[2], -q2[3]};
    }
    
    // Linear interpolation
    vec4 result = {
        q1[0] * (1.0f - t) + q2[0] * t,
        q1[1] * (1.0f - t) + q2[1] * t,
        q1[2] * (1.0f - t) + q2[2] * t,
        q1[3] * (1.0f - t) + q2[3] * t
    };
    
    // Normalize result
    return normalize(result);
}

Spherical Cubic Interpolation (SCERP)

For interpolating through multiple quaternions with smooth transitions, you can use SCERP, which builds upon SLERP.

Squad (Spherical Quadrangle Interpolation)

For more complex interpolation curves through multiple control points, Squad provides smooth transitions between SLERP segments.


Practical Example and Code

Here’s a complete example showing how to use SLERP for rotation interpolation:

cpp
#include <cmath>
#include <array>

// Quaternion structure
struct Quaternion {
    float x, y, z, w;
};

// Normalize a quaternion
Quaternion normalize(Quaternion q) {
    float length = sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
    if (length == 0.0f) return {0, 0, 0, 1}; // Return identity quaternion if length is zero
    return {q.x/length, q.y/length, q.z/length, q.w/length};
}

// SLERP implementation
Quaternion quaternionSlerp(Quaternion q1, Quaternion q2, float t) {
    // Ensure quaternions are normalized
    q1 = normalize(q1);
    q2 = normalize(q2);
    
    // Calculate cosine of angle between quaternions
    float cosTheta = q1.w*q2.w + q1.x*q2.x + q1.y*q2.y + q1.z*q2.z;
    
    // If quaternions are very close, use linear interpolation
    if (cosTheta > 0.9995f) {
        float inv_t = 1.0f - t;
        return normalize({
            q1.x * inv_t + q2.x * t,
            q1.y * inv_t + q2.y * t,
            q1.z * inv_t + q2.z * t,
            q1.w * inv_t + q2.w * t
        });
    }
    
    // Ensure we take the shortest path
    if (cosTheta < 0.0f) {
        q2 = {-q2.x, -q2.y, -q2.z, -q2.w};
        cosTheta = -cosTheta;
    }
    
    // Calculate interpolation parameters
    float theta = acos(cosTheta);
    float sinTheta = sin(theta);
    float w1 = sin((1.0f - t) * theta) / sinTheta;
    float w2 = sin(t * theta) / sinTheta;
    
    // Interpolate
    return {
        q1.x * w1 + q2.x * w2,
        q1.y * w1 + q2.y * w2,
        q1.z * w1 + q2.z * w2,
        q1.w * w1 + q2.w * w2
    };
}

// Example usage
void exampleUsage() {
    // Create two quaternions representing rotations
    Quaternion q1 = {0.0f, 0.0f, 0.0f, 1.0f}; // Identity quaternion (no rotation)
    Quaternion q2 = {0.0f, 0.7071f, 0.0f, 0.7071f}; // 90-degree rotation around Y axis
    
    // Interpolate between them with ratio = 0.5
    Quaternion intermediate = quaternionSlerp(q1, q2, 0.5f);
    
    // intermediate now represents a 45-degree rotation around Y axis
}

Applications and Use Cases

Quaternion interpolation is widely used in:

  1. Computer Animation: Smoothly rotating objects between keyframes
  2. Game Development: Character animations, camera movements, and object rotations
  3. Robotics: Smooth joint movements and orientation control
  4. Flight Simulation: Smooth aircraft orientation changes
  5. Virtual Reality: Smooth viewpoint transitions and object manipulations

The choice between SLERP, NLERP, or other interpolation methods depends on your specific requirements for smoothness, performance, and accuracy.


Conclusion

  1. SLERP is the correct method for interpolating between quaternions with constant angular velocity and maintaining the shortest rotational path.
  2. Always normalize your quaternions before interpolation to ensure they remain valid rotation representations.
  3. Check for negative dot products and negate one quaternion when necessary to ensure you’re taking the shortest path.
  4. Consider NLERP for performance-critical applications where perfect constant velocity isn’t essential.
  5. Handle edge cases where quaternions are nearly identical to avoid numerical instability.

If your current SLERP implementation isn’t producing the desired results, double-check that you’re handling the shortest path correctly and that your quaternions are properly normalized before interpolation.