The Quaternion
3 min read•1 page
Definition
A quaternion is a 4D complex number used to represent 3D orientation. Unlike Euler angles (XYZ rotation) which suffer from Gimbal Lock, Quaternions provide a robust, lock-free way to represent and interpolate rotations.
python
1import math23# --- 2D Rotation with Python's native complex numbers ---4# Rotate 2D point (1, 0) by 45 degrees5point_2d = 1.0 + 0.0j6angle_2d = math.radians(45)78# Rotation factor: e^(i*theta) = cos(theta) + i*sin(theta)9rotation_2d = complex(math.cos(angle_2d), math.sin(angle_2d))10rotated_2d = point_2d * rotation_2d11print(f"2D Rotated: ({rotated_2d.real:.3f}, {rotated_2d.imag:.3f})")1213# --- 3D Rotation with Quaternions (Hypercomplex Numbers) ---14# Extends complex numbers to 4D with three imaginary units: i, j, k15# satisfying: i² = j² = k² = ijk = -116# q = w + xi + yj + zk17q = Quaternion(w=1.0, x=0.0, y=0.0, z=0.0) # Identity rotation18is_unit = math.isclose(q.w**2 + q.x**2 + q.y**2 + q.z**2, 1.0)19print(f"Is unit quaternion: {is_unit}")
Relationship to Complex Numbers:
Just as standard 2D complex numbers
Multiplying a 3D point by a quaternion via the sandwich product
Just as standard 2D complex numbers
z = a + bi (with i² = -1) rotate points in 2D by multiplication, quaternions extend this to 4D space as hypercomplex numbers of the form q = w + xi + yj + zk. Hamilton's fundamental formula defines the relationship between the three imaginary units:i² = j² = k² = ijk = -1Multiplying a 3D point by a quaternion via the sandwich product
q * v * q⁻¹ performs a clean, gimbal-lock-free 3D rotation.Rotate X
0.00Rotate Y
45.00Rotate Z
0.003 min read•1 page
Data structure
At its core, a quaternion is stored in memory as an ordered list of four double-precision floating-point numbers (x, y, z, and w).
python
1import math23# Define axis and angle4axis = Vector3(0, 1, 0)5angle_rad = math.radians(45)67# Create Quaternion from axis and angle8quat = Quaternion.from_axis_angle(axis, angle_rad)910# Retrieve components11w = quat.w12x = quat.x13y = quat.y14z = quat.z1516print(f"Quaternion components: x={x:.3f}, y={y:.3f}, z={z:.3f}, w={w:.3f}")1718# --- Sine/Cosine component calculation ---19# A rotation by angle theta around a unit axis (ux, uy, uz) maps to:20# w = cos(theta / 2)21# (x, y, z) = (ux, uy, uz) * sin(theta / 2)22theta_half = angle_rad / 2.023w_manual = math.cos(theta_half)24s_half = math.sin(theta_half)25x_manual = axis.x * s_half26y_manual = axis.y * s_half27z_manual = axis.z * s_half2829print(f"Manual components: x={x_manual:.3f}, y={y_manual:.3f}, z={z_manual:.3f}, w={w_manual:.3f}")
Trigonometric Vector & Scalar Split:
For a rotation of angle
• Real scalar part:
• Imaginary vector part:
For a rotation of angle
θ around a normalized axis u = (ux, uy, uz), the quaternion components are defined by:• Real scalar part:
w = cos(θ / 2)• Imaginary vector part:
(x, y, z) = u * sin(θ / 2)(x,y,z,w) values are very unintuitive to read. They represent this trigonometric interplay of the axis and rotation angle.
4 min read•1 page
Applying Rotation:
Quaternions are easily constructed from an Axis and an Angle, and then applied to transform vectors and points without the artifacts of Euler angles.
python
1import math234def rotate_vector(vec: Vector3, axis: Vector3, angle_degrees: float) -> Vector3:5 # Convert angle to radians6 angle_rad = math.radians(angle_degrees)78 # Create the rotation quaternion9 q = Quaternion.from_axis_angle(axis.normalized(), angle_rad)1011 # Apply rotation directly to vector12 return q.rotate(vec)1314# --- Manual Conjugation Formula: q * v * q* ---15# Translates point/vector rotation using raw quaternion multiplication16def rotate_vector_manual(vec: Vector3, q: Quaternion) -> Vector3:17 # 1. Represent vector as a "pure" quaternion (w = 0)18 v_pure = Quaternion(w=0.0, x=vec.x, y=vec.y, z=vec.z)1920 # 2. Get conjugate of the rotation quaternion21 q_conj = Quaternion(w=q.w, x=-q.x, y=-q.y, z=-q.z)2223 # 3. Rotate via sandwich product: v' = q * v * q*24 rotated_pure = q * v_pure * q_conj2526 # 4. Extract 3D coordinates from vector part (x, y, z)27 return Vector3(rotated_pure.x, rotated_pure.y, rotated_pure.z)2829# Example usage:30v = Vector3(2.0, 0.0, 0.0)31rot_axis = Vector3(0.0, 1.0, 1.0)32result = rotate_vector(v, rot_axis, 90.0)
The Conjugation (Sandwich) Formula:
Mathematically, to rotate a 3D point or vector
where
Mathematically, to rotate a 3D point or vector
v by quaternion q, we represent v as a pure imaginary quaternion (0, vx, vy, vz). We then compute the sandwich product:v' = q * v * q*where
q* is the conjugate of q. The rotated vector is extracted from the imaginary part of the result.Angle (deg)
90.002 min read•1 page
Slerp (Interpolation):
Spherical Linear Interpolation smoothly blends between two quaternion orientations. Unlike Euler angle interpolation (which can wobble or experience Gimbal Lock), Slerp takes the shortest, most direct angular path with a constant speed.
python
1import math23def interpolate_rotation(quat_a: Quaternion, quat_b: Quaternion, t: float) -> Quaternion:4 # Spherical Linear Interpolation (Slerp) between quat_a and quat_b5 # t is the interpolation parameter between 0.0 and 1.06 return Quaternion.slerp(quat_a, quat_b, t)78# Example usage:9q_a = Quaternion.from_axis_angle(Vector3(0, 0, 1), 0.0)10q_b = Quaternion.from_axis_angle(Vector3(0, 0, 1), math.pi / 2.0)11q_interp = interpolate_rotation(q_a, q_b, 0.5)
Interpolate (t)
0.502 min read•1 page
Multiplication (Combine):
To combine two rotations, you multiply their quaternions. Order matters! `Q1 * Q2` is not the same as `Q2 * Q1`. In most 3D math libraries (like Three.js), `A.multiply(B)` applies rotation B after rotation A.
python
1import math23def combine_rotations(quat_first: Quaternion, quat_second: Quaternion) -> Quaternion:4 # Combine rotations by multiplying quaternions.5 # Order matters: quat_second * quat_first represents applying quat_first, then quat_second.6 return quat_second * quat_first78# Example usage:9q_rot_z = Quaternion.from_axis_angle(Vector3(0, 0, 1), math.radians(45))10q_rot_x = Quaternion.from_axis_angle(Vector3(1, 0, 0), math.radians(45))1112# Apply Z rotation first, then X rotation13q_combined = combine_rotations(q_rot_z, q_rot_x)
Sequence
0.002 min read•1 page
Inverse (Conjugate):
The Inverse of a quaternion perfectly reverses its rotation. If you rotate an object by `Q` and then by `Q.invert()`, the object returns exactly to its starting orientation (the Identity). Note: For unit/normalized quaternions, the Inverse is mathematically equal to its Conjugate.
python
1import math23def reverse_rotation(q: Quaternion) -> Quaternion:4 # For a unit quaternion, the inverse is equivalent to its conjugate.5 # Returns a new quaternion representing the reverse rotation.6 return Quaternion(w=q.w, x=-q.x, y=-q.y, z=-q.z)78# Example usage:9q = Quaternion.from_axis_angle(Vector3(1, 0, 0), math.radians(45))10q_inv = reverse_rotation(q)1112# Multiplying Q by Q_inv yields the Identity quaternion (no rotation)13q_identity = q * q_inv
Sequence
0.002 min read•1 page
Normalize:
Only Unit Quaternions (magnitude exactly equal to 1) represent valid pure rotations. After many math operations, floating-point drift can un-normalize them, causing unwanted scaling artifacts. Always Normalize your quaternions if doing heavy continuous math!
python
1import math23def fix_drift(q: Quaternion) -> Quaternion:4 # Creates a copy of the quaternion and normalizes it.5 # Normalizing scales the magnitude back to 1.0, removing floating-point drift scaling errors.6 mag = math.sqrt(q.w**2 + q.x**2 + q.y**2 + q.z**2)7 if mag == 0:8 return Quaternion(1.0, 0.0, 0.0, 0.0)9 return Quaternion(w=q.w/mag, x=q.x/mag, y=q.y/mag, z=q.z/mag)1011# Example usage:12# Create an unnormalized quaternion (with scale drift)13q_drifted = Quaternion(1.5, 0.0, 0.0, 0.0)14q_clean = fix_drift(q_drifted)
State
0.002 min read•1 page
Dot(Quaternion):
Calculates the 4D dot product between two quaternions, representing their orientation similarity.
python
1def quaternion_dot(q1: Quaternion, q2: Quaternion) -> float:2 # 4D Dot Product of components3 return q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z45# Example usage:6q_a = Quaternion(1.0, 0.0, 0.0, 0.0) # Identity7q_b = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(90))8dot_val = quaternion_dot(q_a, q_b) # Returns ~0.707 (cos(45°))
Mathematical Property:
For two normalized/unit quaternions, their dot product equals the cosine of half the angle between their orientations:
If
For two normalized/unit quaternions, their dot product equals the cosine of half the angle between their orientations:
q1 · q2 = cos(θ / 2)If
|q1 · q2| = 1.0, the orientations are identical. If q1 · q2 = 0.0, the rotations are 180° apart. Note that q and -q represent the exact same physical orientation.Rotate Y (deg)
0.002 min read•1 page
AngleTo(Quaternion):
Calculates the angular difference between two quaternion rotations in radians.
python
1import math23def quaternion_angle_to(q1: Quaternion, q2: Quaternion) -> float:4 # 4D dot product5 dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z67 # Restrict to absolute dot product for shortest angular path8 abs_dot = min(1.0, abs(dot))910 # Angle formula: theta = 2 * acos(|q1 · q2|)11 return 2.0 * math.acos(abs_dot)1213# Example usage:14q_a = Quaternion(1.0, 0.0, 0.0, 0.0) # Identity15q_b = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(90))16angle = quaternion_angle_to(q_a, q_b) # Returns ~1.5708 radians (90°)
Shortest Arc Angle:
Because
Because
q and -q represent the same spatial rotation, taking the absolute value of the dot product before calculating acos ensures we get the shortest angular distance (between 0 and π radians).Angle Y (deg)
0.002 min read•1 page
RotationDifference(Quaternion, Quaternion):
Calculates the relative quaternion rotation required to transform orientation A into orientation B.
python
1def rotation_difference(q_from: Quaternion, q_to: Quaternion) -> Quaternion:2 # Compute relative rotation: q_diff = q_to * q_from_inverse3 # For unit quaternions, inverse is the conjugate4 q_from_conj = Quaternion(w=q_from.w, x=-q_from.x, y=-q_from.y, z=-q_from.z)56 # Multiply to find difference rotation7 return q_to * q_from_conj89# Example usage:10q_start = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(30))11q_target = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(90))12q_diff = rotation_difference(q_start, q_target) # Returns 60° rotation around Y
Algebraic Relationship:
To find the rotation
Since both are unit quaternions, we can optimize by multiplying
To find the rotation
q_diff that maps orientation A to B, we satisfy the equation:q_diff * q_A = q_B => q_diff = q_B * q_A⁻¹Since both are unit quaternions, we can optimize by multiplying
q_B with the conjugate q_A* directly.Change Target
45.003 min read•1 page
Align Vectors:
To align one vector directly onto another, we find the shortest-arc rotation between them. The rotation axis is the cross product of the two vectors, and the angle is the arccosine of their dot product.
python
1import math23def align_vectors(v_from: Vector3, v_to: Vector3) -> Quaternion:4 # Ensure vectors are normalized5 v1 = v_from.normalized()6 v2 = v_to.normalized()78 # Calculate dot product and cross product axis9 dot = v1.dot(v2)10 axis = v1.cross(v2)1112 # Handle collinear opposite vectors (180 deg)13 if dot < -0.999999:14 # Find orthogonal vector to v1 as axis15 orthogonal = Vector3(1, 0, 0) if abs(v1.x) < 0.8 else Vector3(0, 1, 0)16 axis = v1.cross(orthogonal).normalized()17 return Quaternion.from_axis_angle(axis, math.pi)1819 # Handle collinear same vectors (0 deg)20 if dot > 0.999999:21 return Quaternion(1.0, 0.0, 0.0, 0.0) # Identity2223 # Calculate half-angle components directly for stable math24 s = math.sqrt((1.0 + dot) * 2.0)25 inv_s = 1.0 / s2627 w = s * 0.528 x = axis.x * inv_s29 y = axis.y * inv_s30 z = axis.z * inv_s3132 quat = Quaternion(w, x, y, z)33 return quat.normalized()
Target Y
1.50Target Z
1.003 min read•1 page
Extraction (To Euler):
Since raw quaternion values `(x, y, z, w)` are difficult to visualize, we often extract Euler Angles (Pitch, Yaw, Roll) or the Axis/Angle representation from them to display in user interfaces or debug output.
python
1import math23def extract_rotation_axis_angle(q: Quaternion):4 # Extracts the axis of rotation and the angle (in radians) from the quaternion.5 # Returns a tuple: (angle_rad, axis_vector)6 # For q = w + xi + yj + zk:7 # angle = 2 * acos(w)8 # axis = (x, y, z) / sin(angle / 2)9 half_angle_sin = math.sqrt(max(0.0, 1.0 - q.w**2))10 if half_angle_sin < 0.0001:11 # Identity rotation, axis can be anything12 return 0.0, Vector3(0.0, 1.0, 0.0)13 angle_rad = 2.0 * math.acos(q.w)14 axis_vec = Vector3(q.x / half_angle_sin, q.y / half_angle_sin, q.z / half_angle_sin)15 return angle_rad, axis_vec1617# Example usage:18q = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(45))19angle, axis = extract_rotation_axis_angle(q)2021print(f"Extracted angle: {math.degrees(angle):.1f}° around axis: {axis.x:.1f}, {axis.y:.1f}, {axis.z:.1f}")
Pitch (X)
45.00Yaw (Y)
45.00