Initializing 3D Canvas...

The Quaternion

3 min read1 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 math
2
3# --- 2D Rotation with Python's native complex numbers ---
4# Rotate 2D point (1, 0) by 45 degrees
5point_2d = 1.0 + 0.0j
6angle_2d = math.radians(45)
7
8# 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_2d
11print(f"2D Rotated: ({rotated_2d.real:.3f}, {rotated_2d.imag:.3f})")
12
13# --- 3D Rotation with Quaternions (Hypercomplex Numbers) ---
14# Extends complex numbers to 4D with three imaginary units: i, j, k
15# satisfying: i² = j² = k² = ijk = -1
16# q = w + xi + yj + zk
17q = Quaternion(w=1.0, x=0.0, y=0.0, z=0.0) # Identity rotation
18is_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 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 = -1
Multiplying a 3D point by a quaternion via the sandwich product q * v * q⁻¹ performs a clean, gimbal-lock-free 3D rotation.
Rotate X
0.00
Rotate Y
45.00
Rotate Z
0.00
3 min read1 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 math
2
3# Define axis and angle
4axis = Vector3(0, 1, 0)
5angle_rad = math.radians(45)
6
7# Create Quaternion from axis and angle
8quat = Quaternion.from_axis_angle(axis, angle_rad)
9
10# Retrieve components
11w = quat.w
12x = quat.x
13y = quat.y
14z = quat.z
15
16print(f"Quaternion components: x={x:.3f}, y={y:.3f}, z={z:.3f}, w={w:.3f}")
17
18# --- 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.0
23w_manual = math.cos(theta_half)
24s_half = math.sin(theta_half)
25x_manual = axis.x * s_half
26y_manual = axis.y * s_half
27z_manual = axis.z * s_half
28
29print(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 θ 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 read1 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 math
2
3
4def rotate_vector(vec: Vector3, axis: Vector3, angle_degrees: float) -> Vector3:
5 # Convert angle to radians
6 angle_rad = math.radians(angle_degrees)
7
8 # Create the rotation quaternion
9 q = Quaternion.from_axis_angle(axis.normalized(), angle_rad)
10
11 # Apply rotation directly to vector
12 return q.rotate(vec)
13
14# --- Manual Conjugation Formula: q * v * q* ---
15# Translates point/vector rotation using raw quaternion multiplication
16def 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)
19
20 # 2. Get conjugate of the rotation quaternion
21 q_conj = Quaternion(w=q.w, x=-q.x, y=-q.y, z=-q.z)
22
23 # 3. Rotate via sandwich product: v' = q * v * q*
24 rotated_pure = q * v_pure * q_conj
25
26 # 4. Extract 3D coordinates from vector part (x, y, z)
27 return Vector3(rotated_pure.x, rotated_pure.y, rotated_pure.z)
28
29# 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 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.00
2 min read1 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 math
2
3def interpolate_rotation(quat_a: Quaternion, quat_b: Quaternion, t: float) -> Quaternion:
4 # Spherical Linear Interpolation (Slerp) between quat_a and quat_b
5 # t is the interpolation parameter between 0.0 and 1.0
6 return Quaternion.slerp(quat_a, quat_b, t)
7
8# 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.50
2 min read1 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 math
2
3def 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_first
7
8# 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))
11
12# Apply Z rotation first, then X rotation
13q_combined = combine_rotations(q_rot_z, q_rot_x)
Sequence
0.00
2 min read1 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 math
2
3def 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)
7
8# Example usage:
9q = Quaternion.from_axis_angle(Vector3(1, 0, 0), math.radians(45))
10q_inv = reverse_rotation(q)
11
12# Multiplying Q by Q_inv yields the Identity quaternion (no rotation)
13q_identity = q * q_inv
Sequence
0.00
2 min read1 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 math
2
3def 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)
10
11# 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.00
2 min read1 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 components
3 return q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z
4
5# Example usage:
6q_a = Quaternion(1.0, 0.0, 0.0, 0.0) # Identity
7q_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:
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.00
2 min read1 page

AngleTo(Quaternion):

Calculates the angular difference between two quaternion rotations in radians.
python
1import math
2
3def quaternion_angle_to(q1: Quaternion, q2: Quaternion) -> float:
4 # 4D dot product
5 dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z
6
7 # Restrict to absolute dot product for shortest angular path
8 abs_dot = min(1.0, abs(dot))
9
10 # Angle formula: theta = 2 * acos(|q1 · q2|)
11 return 2.0 * math.acos(abs_dot)
12
13# Example usage:
14q_a = Quaternion(1.0, 0.0, 0.0, 0.0) # Identity
15q_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 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.00
2 min read1 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_inverse
3 # For unit quaternions, inverse is the conjugate
4 q_from_conj = Quaternion(w=q_from.w, x=-q_from.x, y=-q_from.y, z=-q_from.z)
5
6 # Multiply to find difference rotation
7 return q_to * q_from_conj
8
9# 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 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.00
3 min read1 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 math
2
3def align_vectors(v_from: Vector3, v_to: Vector3) -> Quaternion:
4 # Ensure vectors are normalized
5 v1 = v_from.normalized()
6 v2 = v_to.normalized()
7
8 # Calculate dot product and cross product axis
9 dot = v1.dot(v2)
10 axis = v1.cross(v2)
11
12 # Handle collinear opposite vectors (180 deg)
13 if dot < -0.999999:
14 # Find orthogonal vector to v1 as axis
15 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)
18
19 # Handle collinear same vectors (0 deg)
20 if dot > 0.999999:
21 return Quaternion(1.0, 0.0, 0.0, 0.0) # Identity
22
23 # Calculate half-angle components directly for stable math
24 s = math.sqrt((1.0 + dot) * 2.0)
25 inv_s = 1.0 / s
26
27 w = s * 0.5
28 x = axis.x * inv_s
29 y = axis.y * inv_s
30 z = axis.z * inv_s
31
32 quat = Quaternion(w, x, y, z)
33 return quat.normalized()
Target Y
1.50
Target Z
1.00
3 min read1 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 math
2
3def 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 anything
12 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_vec
16
17# Example usage:
18q = Quaternion.from_axis_angle(Vector3(0, 1, 0), math.radians(45))
19angle, axis = extract_rotation_axis_angle(q)
20
21print(f"Extracted angle: {math.degrees(angle):.1f}° around axis: {axis.x:.1f}, {axis.y:.1f}, {axis.z:.1f}")
Pitch (X)
45.00
Yaw (Y)
45.00