Rotating in 3D is subtle. Euler angles are intuitive but suffer gimbal lock; quaternions rotate smoothly without it and interpolate cleanly. Learn when to use each and how to think about quaternions.
Euler angles describe a 3D rotation as three separate turns — pitch, yaw, roll — which is intuitive and human-readable. Their flaw is gimbal lock: at certain orientations two axes align and you lose a degree of freedom, causing sudden flips and jerky rotation. Euler angles are fine for input and display but treacherous for accumulating rotations over time.
Euler angles: rotation as three sequential turns.
pitch (X) nod up/down
yaw (Y) turn left/right
roll (Z) tilt sideways
Intuitive — but GIMBAL LOCK:
rotate pitch by 90° and the yaw + roll axes ALIGN -> you lose an axis of
freedom -> the object suddenly flips / can't rotate a certain way.
Order-dependent too (XYZ ≠ ZYX). Great for reading/authoring a rotation,
bad for composing rotations over time. That's what quaternions fix.A quaternion is a four-number representation of a 3D rotation (x, y, z, w) that has no gimbal lock and composes and interpolates cleanly. You rarely manipulate the raw numbers by hand — you create one from an axis and angle and let the library combine them. The mental model: a quaternion encodes "rotate this much around this axis," robustly.
import numpy as np
def quat_from_axis_angle(axis, angle):
axis = np.array(axis) / np.linalg.norm(axis) # unit axis
s = np.sin(angle / 2)
return np.array([axis[0]*s, axis[1]*s, axis[2]*s, np.cos(angle/2)]) # (x,y,z,w)
# rotate 90° around the Y axis:
q = quat_from_axis_angle((0, 1, 0), np.radians(90))
print(q) # ~[0, 0.707, 0, 0.707]
# you build quaternions from axis+angle and let the engine combine/apply them.
# no gimbal lock, and they interpolate smoothly (next).Combining rotations is quaternion multiplication (order matters, like matrices), and applying a quaternion to a vector rotates it. Engines expose this as multiplying quaternions and transforming vectors, so you accumulate an object’s spin frame after frame without the drift and lock of Euler angles. This robustness is why every 3D engine stores orientation as a quaternion internally.
Working with quaternions (via your engine's math type):
combine two rotations: q_total = q2 * q1 (apply q1 then q2; order matters)
rotate a vector: v_rotated = q * v * q_conjugate (engine does this for you)
identity (no rotation): (0, 0, 0, 1)
Because they compose without gimbal lock or drift, engines store an object's
orientation as a QUATERNION and accumulate spin each frame by multiplying in a
small delta rotation. You author in Euler, but the engine rotates in quaternions.Interpolating between two orientations should follow the shortest arc at constant angular speed — that is spherical linear interpolation, slerp. Lerping Euler angles gives wobble and wrong paths; slerping quaternions gives clean, natural turns. Use slerp to smoothly aim a turret, blend animation poses, or ease a camera’s orientation toward a target.
# slerp: shortest-arc, constant-speed blend between two orientations.
# in an engine you call it directly:
# result = start_quat.slerp(end_quat, t) # t in [0, 1]
# why not just lerp Euler angles?
# turning from yaw 350° to yaw 10° by lerping goes the LONG way (350->10),
# and combined-axis Euler lerps wobble. Slerp takes the short arc, smoothly.
# uses: smoothly aim a turret at a target, blend two animation poses,
# ease a camera's look direction. Slerp is the rotation equivalent of lerp.
print("slerp = the 'lerp' for rotations — shortest arc, constant speed")