Model Matrix: Rotation - World vs. Local Origin
We introduce the mathematical foundation for rotation as linear transformations applied to model matrices in 3D computer graphics and focus on the distinction between rotating an object around an external point and rotating it around its own local origin. We derive the necessary transformations from two conceptually different but mathematically equivalent perspectives: Active transformations, which move the object in a fixed coordinate system, and passive transformations, which redefine the coordinate system around a fixed object. Additionally, we demonstrate the impact of matrix multiplication order, distinguish between world-space and local-space rotations, and conclude with performance considerations and examples from external libraries and game frameworks.
Introduction
The effect of a rotation matrix changes significantly depending on the order of matrix multiplication, as this determines the pivot point for the operation. This article addresses two essential scenarios:
- Rotating an object around an external point1.
- Rotating an object around its own local origin.
We will derive the mathematical solutions for both cases. The first scenario will be explored from two distinct but equivalent perspectives: as an active transformation that directly manipulates an object's vertices, and as a passive change of coordinates that reorients the coordinate system itself.
By examining the underlying matrix compositions, we will connect this theory to practical examples in common graphics libraries.
Rotation of A around B
The rotation of an object around another object can be seen as the rotation of around an origin defined by . For this to work, must be treated as the origin.
In the following, let be points represented by the position vectors and , respectively.
Obviously, the position vectors from the world origin (0, 0) to these points are:
We denote the vector pointing from to as . This vector is calculated as:
By subtracting 's position from 's, we obtain a direction vector which is a direction vector from - the new origin - to . Applying operations to this new vector is equivalent to performing them in a coordinate system where is the origin.
For this to work, three steps are necessary:
- Translation to Origin: The pivot point is moved to the world origin. This can also be understood as a passive transformation, where the world coordinate system is shifted to align its origin with .
- Rotation: The object is then rotated relative to , which is now the (temporary) origin of the world coordinate system.
- Translate Back: The initial translation is reversed to move the rotated object to its new position within the original coordinate system.
This is illustrated in Figure 1.
Plot-Code (Python)
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.ticker import MultipleLocator
from matplotlib.patches import Wedge
# plot layout
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-1, 8)
ax.set_ylim(-1, 8)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
ax.axhline(0, color='black', linewidth=1.5)
ax.axvline(0, color='black', linewidth=1.5)
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
theta = math.radians(20)
p_x = 6
p_y = 4
pc = 'blue'
r_x = 2
r_y = 3
rc = 'red'
# point
p = np.array([p_x, p_y])
p_len = np.linalg.norm(p)
# center of rotation
r = np.array([r_x, r_y])
ax.quiver(0, 0, r_x, r_y, angles='xy', scale_units='xy', scale=1, color=rc, width=0.005, linestyle='--',alpha=0.2)
# rotated p
pr = np.array([
((p_x - r[0]) * np.cos(theta) - (p_y - r[1]) * np.sin(theta)) + r[0],
((p_x - r[0]) * np.sin(theta) + (p_y - r[1]) * np.cos(theta)) + r[1]
])
# vectors from r to p, p'
ax.quiver(r_x, r_y, pr[0] - r_x, pr[1] - r_y, angles='xy', scale_units='xy', scale=1, color=rc, width=0.005, linestyle='--',alpha=0.2)
ax.quiver(r_x, r_y, p[0] - r_x, p[1] - r_y, angles='xy', scale_units='xy', scale=1, color=rc, width=0.005, linestyle='--',alpha=0.2)
# p - r
ax.quiver(0, 0, p_x - r_x, p_y - r_y, angles='xy', scale_units='xy', scale=1, color=pc, width=0.005, linestyle='--',alpha=0.2)
ax.quiver(0, 0, pr[0] - r_x, pr[1] - r_y, angles='xy', scale_units='xy', scale=1, color=pc, width=0.005, linestyle='--',alpha=0.2)
circle_p = plt.Circle((p_x, p_y), 0.08, color=pc, fill=True)
ax.add_patch(circle_p)
# p, p'
circle_p = plt.Circle((p_x, p_y), 0.08, color='blue', fill=True)
circle_pr = plt.Circle((pr[0], pr[1]), 0.08, color='blue', fill=True)
ax.add_patch(circle_p)
ax.add_patch(circle_pr)
circle_pt = plt.Circle((p_x - r_x, p_y - r_y), 0.08, color='blue', fill=True, alpha=0.2)
circle_prt = plt.Circle((pr[0] - r_x, pr[1] - r_y), 0.08, color='blue', fill=True, alpha=0.2)
ax.add_patch(circle_pt)
ax.add_patch(circle_prt)
#r
circle_r = plt.Circle((r_x, r_y), 0.08, color=rc, fill=True)
ax.add_patch(circle_r)
arc_radius = 4.2
arc = Arc((r[0], r[1]),
arc_radius,
arc_radius,
angle=0,
theta1=np.degrees(np.arctan2(p_y - r[1], p_x - r[0])),
theta2=20 + np.degrees(np.arctan2(p_y - r[1], p_x - r[0])),
edgecolor=rc)
ax.add_patch(arc)
arc_radius = 4.2
arc = Arc((0, 0),
arc_radius,
arc_radius,
angle=0,
theta1=np.degrees(np.arctan2(p_y - r[1], p_x - r[0])),
theta2=20 + np.degrees(np.arctan2(p_y - r[1], p_x - r[0])),
alpha=0.2,
edgecolor=pc)
ax.add_patch(arc)
# texts
ax.text(p_x + 0.2, p_y + 0.2, 'A', color=pc, fontsize=12)
ax.text(pr [0] - 0.6, pr[1] + 0.5 , r"$A' = R(\theta)(\vec{a} - \vec{b}) + \vec{b}$", color=pc, fontsize=12)
ax.text(p_x - r_x + 0.2, p_y - r_y + 0.2, r'$A_t = (a_x - b_x, a_y - b_y)$', color=pc, fontsize=12)
ax.text(pr[0] - r_x + 0.2, pr[1] - r_y + 0.2 , r"$A_t'$", color=pc, fontsize=12)
ax.text(0 + 1, 0 + 0.4, r'$\theta$', color=pc, fontsize=14, alpha=0.2)
ax.text(r_x + 0.2, r_y - 0.2, 'B ', color=rc, fontsize=12)
ax.text(r_x + 1, r_y + 0.4, r'$\theta$', color=rc, fontsize=14)
# axes through B
L = 4
ax.plot([r_x - L, r_x + L], [r_y, r_y], linestyle='-', linewidth=1.0, color='black', alpha=0.7)
ax.plot([r_x, r_x], [r_y - L, r_y + L], linestyle='-', linewidth=1.0, color='black', alpha=0.7)
ax.text(r_x + L + 0.05, r_y - 0.1, r'$x_B$', color='0.35', fontsize=11)
ax.text(r_x - 0.35, r_y + L + 0.05, r'$y_B$', color='0.35', fontsize=11)
plt.show()
Active Transformation
Let be the position vector of the point we want to rotate, and let be the position vector of the pivot point .
To rotate around , we first perform an affine transformation that translates into a new coordinate system where B is the origin. This is achieved by subtracting from :
The resulting vector, , now points from to . By performing this transformation, we ensure that subsequent operations, such as applying a rotation matrix , will rotate relative to - as if were the origin.
The calculation sequence, understanding the affine transformations as matrices, is as follows:
As usual, we interpret this from right to left:
A model matrix, , typically consists of a composition of scaling, rotation, and translation operations. We can express this as a single affine transformation matrix:
Here, is a 3x3 matrix representing the combined linear transformations (scaling and rotation), and is the translation vector.
To transform the object represented by this model matrix so that its pivot point is at the origin, we pre-multiply its model matrix by a translation matrix :
By applying the rotation and corresponding back-translation, we get the final composite matrix, M
When a local-space vector