Smooth Rotations in Three.js Using Quaternion Slerp

In 3D game development and animation, smoothly rotating objects from one orientation to another is a common requirement. This article guides you through using the Quaternion.slerp (Spherical Linear Interpolation) method in Three.js to achieve seamless, natural-looking rotational transitions. You will learn why quaternions are preferred over Euler angles, how the slerp mathematical concept works in Three.js, and how to implement it using two practical coding approaches.

Why Use Quaternions and Slerp?

When rotating 3D objects, developers often use Euler angles (X, Y, and Z rotations). However, Euler angles suffer from gimbal lock—a state where two rotational axes align, causing a loss of one degree of freedom and resulting in erratic, jerky movement.

Quaternions represent rotations using four-dimensional numbers (\(x, y, z, w\)). They completely avoid gimbal lock. Slerp (Spherical Linear Interpolation) is a method that interpolates between two quaternions along the shortest path on a 4D sphere at a constant speed, resulting in perfectly smooth rotation.


Method 1: Absolute Interpolation (From Start to End)

Use this method when you want to transition an object from a specific starting rotation to a specific target rotation over a defined period of time.

Three.js provides the slerpQuaternions(qa, qb, t) method for this purpose, where t is a normalized value between 0 (start) and 1 (end).

Step-by-Step Implementation

  1. Define the Quaternions: Create a start quaternion, a target quaternion, and a third quaternion to hold the interpolated result.
  2. Track Time/Progress: Increment an interpolation factor (\(t\)) in your animation loop.
  3. Apply and Assign: Use slerpQuaternions to calculate the rotation at step \(t\) and apply it to your 3D mesh.
import * as THREE from 'three';

// 1. Create the mesh
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 2. Define rotations
const startRotation = new THREE.Quaternion(); // Default identity (no rotation)
const targetRotation = new THREE.Quaternion().setFromAxisAngle(
    new THREE.Vector3(0, 1, 0), // Rotate around Y-axis
    Math.PI / 2                 // 90 degrees in radians
);

let t = 0; // Interpolation progress (0 to 1)
const duration = 2; // Duration of transition in seconds
const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);

    const delta = clock.getDelta();
    if (t < 1) {
        t += delta / duration; // Advance progress based on elapsed time
        t = Math.min(t, 1);    // Clamp t to a maximum of 1

        // Interpolate and apply directly to the mesh
        mesh.quaternion.slerpQuaternions(startRotation, targetRotation, t);
    }

    renderer.render(scene, camera);
}
animate();

Method 2: Dynamic Tracking (Constant Easing)

Use this method when your target rotation is constantly changing (for example, a camera tracking a moving player, or a character turning to face the mouse cursor).

Instead of tracking absolute time, you call the .slerp() method on the object’s current quaternion directly in the loop, using a constant fractional step (damping factor).

Step-by-Step Implementation

  1. Calculate Target: Determine the desired target quaternion in each frame.
  2. Slerp Incrementally: Call mesh.quaternion.slerp(target, alpha) where alpha is a small percentage (e.g., 0.05 to 0.1). This acts as a dampening factor, causing the rotation to smoothly “catch up” to the target.
const targetQuaternion = new THREE.Quaternion();
const easingFactor = 0.05; // Adjust between 0 and 1 for speed (lower is smoother)

// Event listener to change target rotation on click
window.addEventListener('click', () => {
    // Generate a random rotation target
    targetQuaternion.setFromEuler(new THREE.Euler(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        0
    ));
});

function animate() {
    requestAnimationFrame(animate);

    // Smoothly rotate the mesh towards the target rotation
    mesh.quaternion.slerp(targetQuaternion, easingFactor);

    renderer.render(scene, camera);
}
animate();

Key Differences Summary

Use Case Method to Use Benefits
Fixed Timeline Transitions (e.g., opening a door, UI transitions) slerpQuaternions(start, end, t) Linear, predictable timing; stops processing once \(t = 1\).
Real-time Tracking (e.g., character aiming, camera tracking) current.slerp(target, step) Easy to implement; naturally slows down as it nears the target (organic deceleration).