Quaternion Rotations for 3D Games
The Gimbal Lock Problem
Euler angles represent 3D rotation as three independent rotations around the x, y, and z axes, typically called pitch, yaw, and roll. This is intuitive for humans: yaw turns left and right, pitch looks up and down, roll tilts sideways. The problem is that these three rotations are applied in sequence, and when two axes align, you lose one degree of freedom.
The classic example: pitch an object 90 degrees upward (rotate around the x-axis). Now the local z-axis points in the same direction the y-axis originally pointed. Yaw (rotation around the original y-axis) and roll (rotation around the local z-axis) now produce the same rotation. You can no longer independently control rotation around two of the three original directions. The system "locks" two axes together, making certain orientations unreachable without snapping through discontinuities.
In practice, gimbal lock manifests as sudden orientation jumps when a camera or character passes through certain angles. A flight camera that looks straight up suddenly flips 180 degrees. A character turning smoothly suddenly snaps to a different orientation. An interpolation between two Euler angle orientations takes a wild spiral path instead of the shortest arc. These problems are not bugs in the engine; they are fundamental mathematical limitations of representing 3D rotation as three independent angle values.
Quaternions avoid gimbal lock entirely because they represent rotation as a single operation around a single axis, not as three sequential rotations around fixed axes. There is no sequential application that can cause axis alignment. Every possible 3D orientation has a unique, smooth quaternion representation (technically two representations, differing only in sign, but both produce the same rotation).
How Quaternions Work
A quaternion has four components: x, y, z, and w. The x, y, and z components form a 3D vector that represents the axis of rotation scaled by the sine of half the rotation angle. The w component stores the cosine of half the rotation angle. A rotation of angle theta around a unit axis (ax, ay, az) produces the quaternion: x = ax * sin(theta/2), y = ay * sin(theta/2), z = az * sin(theta/2), w = cos(theta/2).
The identity quaternion (no rotation) is (0, 0, 0, 1): zero rotation around any axis, with w = cos(0) = 1. A 180-degree rotation around the y-axis is (0, 1, 0, 0): the axis is (0, 1, 0), sin(90) = 1 for the xyz components, and cos(90) = 0 for w. A 90-degree rotation around the z-axis is (0, 0, 0.707, 0.707): sin(45) = 0.707 for the z component, cos(45) = 0.707 for w.
Quaternion multiplication combines two rotations into one. If quaternion A rotates by 30 degrees around the y-axis and quaternion B rotates by 45 degrees around the x-axis, the product A * B represents the combined rotation that applies B first, then A. Multiplication is not commutative (A * B does not equal B * A), which correctly reflects the fact that rotation order matters in 3D.
A unit quaternion (magnitude 1) represents a valid rotation. All rotation quaternions must be unit quaternions. After repeated multiplications, floating-point error can cause the magnitude to drift from 1.0, which introduces scaling artifacts. Periodically renormalizing (dividing each component by the quaternion's magnitude) prevents this drift. Most engine implementations normalize automatically after operations.
Slerp: Smooth Rotation Interpolation
Slerp (spherical linear interpolation) is the primary reason game developers use quaternions. Given two quaternion orientations and a parameter t between 0 and 1, slerp produces the rotation that is t percent of the way from the first orientation to the second, following the shortest arc on the rotation hypersphere. The result is constant angular velocity: the rotation speed does not accelerate or decelerate unless you apply an easing function to the t parameter.
Linear interpolation (lerp) between two Euler angle sets produces unpredictable results. The interpolated path may spiral, take the long way around, or pass through gimbal lock orientations. Slerp between two quaternions always takes the shortest path, produces no spiraling, and maintains constant speed. This makes slerp ideal for camera transitions, character turning animations, turret tracking, and any system that smoothly transitions between two 3D orientations.
Both Three.js and Babylon.js provide slerp methods on their quaternion classes. In Three.js: quaternion.slerpQuaternions(startQuat, endQuat, t) or the static THREE.Quaternion.slerp(qa, qb, result, t). In Babylon.js: BABYLON.Quaternion.Slerp(qa, qb, t). The t parameter typically comes from a timer normalized to the 0-1 range, or from an easing function for non-linear interpolation.
For very small angles (less than about 1 degree), slerp's internal sine division can lose precision. Normalized lerp (nlerp), which linearly interpolates the components and then normalizes the result, produces nearly identical results for small angles and is cheaper to compute. Many engines use nlerp when the angle between quaternions is below a threshold and switch to slerp for larger angles.
Converting Between Representations
Euler to quaternion: Game designers and level editors typically work in Euler angles (degrees of pitch, yaw, roll) because they are intuitive. The engine converts these to quaternions internally. In Three.js, setting object.rotation.set(pitch, yaw, roll) converts to quaternion automatically when the engine computes the world matrix. You can also construct a quaternion from Euler angles directly: quaternion.setFromEuler(new THREE.Euler(x, y, z, order)) where order specifies the rotation sequence (YXZ is common for games).
Quaternion to Euler: Extracting Euler angles from a quaternion is sometimes needed for display or serialization. In Three.js: euler.setFromQuaternion(quaternion, order). Be aware that the conversion is not unique: a single quaternion orientation can be described by multiple Euler angle combinations, and the function returns one canonical representation. The extracted angles may not match the angles you originally set, even though they represent the same orientation.
Quaternion to matrix: Quaternions can be converted to 3x3 or 4x4 rotation matrices for use in shader uniforms or matrix multiplication. This conversion is straightforward and engines do it automatically when computing the model matrix from an object's transform. In Three.js: matrix.makeRotationFromQuaternion(quaternion).
Axis-angle to quaternion: If you have a rotation axis (a unit vector) and an angle, converting to a quaternion is direct: x = axis.x * sin(angle/2), y = axis.y * sin(angle/2), z = axis.z * sin(angle/2), w = cos(angle/2). In Three.js: quaternion.setFromAxisAngle(axis, angle). This is useful for creating rotations from physics (torque around an axis) or procedural animation (rotate around a computed axis).
Practical Usage in Web Games
Camera orbit controls: When the player drags to orbit the camera around a target, store the camera orientation as a quaternion. Each mouse drag increment creates a small rotation quaternion from the drag delta, and you multiply it with the current orientation quaternion. The camera always follows the shortest path and never hits gimbal lock, regardless of how the player drags.
Smooth character turning: When a character needs to face a new direction, compute the target quaternion using lookAt or manual axis-angle construction. Each frame, slerp the character's current orientation toward the target orientation with a t value proportional to delta time. A t value of 5 * deltaTime produces a responsive but smooth turn. Higher values turn faster; lower values create lazy, sweeping turns.
Animation blending: Skeletal animation systems store bone rotations as quaternions. Blending two animation clips (like transitioning from walk to run) slerps each bone's quaternion between the two clip values. This produces smooth, natural-looking transitions without the popping or distortion that Euler angle blending would cause.
Combining rotations: To apply rotation A followed by rotation B, multiply B * A (note the reversed order). To undo rotation A, multiply by the conjugate of A (negate x, y, and z, keep w). To find the rotation from orientation A to orientation B, multiply B by the conjugate of A. These operations let you compute relative rotations, delta rotations between frames, and accumulated rotations over time.
Rotating a vector: To rotate a vector v by quaternion q, compute q * v * q_conjugate, where v is treated as a quaternion (v.x, v.y, v.z, 0). This gives the rotated vector. In practice, use the engine's method (vector.applyQuaternion(quaternion) in Three.js) rather than implementing the multiplication yourself.
Use quaternions whenever you interpolate between 3D orientations or compound rotations over time. Use Euler angles for authoring and display. Let the engine convert between them. Slerp produces smooth, predictable rotation transitions that Euler interpolation cannot match.