Rotations as a Special Case of Vector Transformations
Rotations, as a class of linear vector transformations1, represent rigid body transformations that change the direction of a vector within a given plane.
Efficient and accurate handling of such transformations poses challenges not only for game and animation engines, but also for simulations and real-time systems, where the limitations of floating-point arithmetic and operation throughput can negatively impact the computational outcomes ([📖She97], [📖Kui99]).
This work introduces vector rotation with a focus on the relations between its two- and three-dimensional formulations.
We begin in the two-dimensional plane by deriving classical rotation matrices from linear combinations in orthogonal bases.
We then generalize this approach by introducing Rodrigues' rotation formula, which provides a compact expression for rotation around arbitrary axes in three-dimensional space. We verify that the standard rotation matrices emerge as special cases from this general form.
Finally, we illustrate the application of vector rotation in the context of embedded coordinate systems within large scene graphs, demonstrating how transformations propagate across hierarchical structures in real-time environments such as video games.
Vector Rotation as a Linear Combination
Given an angle and an orthonormal basis in three-dimensional space, rotating a vector around one of the cardinal axes can be computed with a rotation matrix .
The following matrices define rotations about the , and axes, respectively:
The matrix directly arises from rotating a vector in the two-dimensional -plane. This connection will be established in the following.
In an orthonormal basis with the standard basis vectors , , any vector of unit length can be expressed as
This follows directly by when writing as a linear combination of the two linear independent vectors :
Substituting with , , we have:
or, as a matrix multiplication
(Observe how represent the identity matrix ).
In Figure 1, the angle represents the amount of rotation of around the origin2.
Plot-Code (Python)
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Arc, FancyArrowPatch
fig, ax = plt.subplots(figsize=(6, 6))
# Plot setup
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
ax.set_xticks([-1, 0, 1])
ax.set_yticks([-1, 0, 1])
ax.set_aspect('equal')
ax.grid(True, linestyle=':', linewidth=0.5)
for spine in ax.spines.values():
spine.set_visible(False)
ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_visible(True)
ax.spines['left'].set_visible(True)
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.text(1.05, 0.02, r"$x$", fontsize=14, va='bottom')
ax.text(0.02, 1.05, r"$y$", fontsize=14, ha='left')
# Vecors
origin = np.array([0, 0])
theta = np.radians(45)
cos_theta = np.cos(theta)
sin_theta = np.sin(theta)
opposite = np.array([0, np.sin(theta)])
v1 = np.array([np.cos(theta), np.sin(theta)])
v2 = np.array([1, 0])
ax.quiver(*origin, *v1, angles='xy', scale_units='xy', scale=1, color='r')
ax.quiver(*origin, *v2, angles='xy', scale_units='xy', scale=1, alpha = 0.5, color='r')
offset = 0.01
ax.text(v1[0] - 0.5, v1[1] -0.3, r"$\vec{v'}$", fontsize=12, color='r')
ax.text(v2[0] - 0.2, v2[1] -0.12, r"$\vec{v}$", fontsize=12, color='r')
# COSINE
ax.plot([0, cos_theta], [-0.02, -0.02], color='black', linestyle='--', linewidth=1)
ax.text(cos_theta / 2 - 0.05, -0.1, r"$\cos(\Theta)$", fontsize=10)
# SINE
start = np.array([cos_theta, 0])
end = start + opposite
ax.plot([start[0], end[0]], [start[1], end[1]], color='black', linewidth=1, linestyle='--')
ax.text(cos_theta + 0.05, sin_theta / 2 - 0.05, r"$\sin(\Theta)$", fontsize=10)
# Unit Circle
circle = plt.Circle((0, 0), 1, color='black', linewidth=0.2, fill=False, transform=ax.transData, linestyle='--')
ax.add_patch(circle)
arrow = FancyArrowPatch(
posA=(0.5, 0),
posB=(np.cos(theta)*0.5, np.sin(theta)*0.51),
connectionstyle="arc3,rad=0.2",
arrowstyle='->',
color='blue',
linestyle='--',
mutation_scale=15,
lw=1.5
)
ax.add_patch(arrow)
ax.text(0.5, 0.2, r"$\Theta$", color='blue', fontsize=16)
plt.show()
We claim that
holds for any orthonormal basis, not necessarily the standard basis (which turns out to be just a special case once we have proven the claim).
We require to ensure that has unit length. This condition is immediately satisfied by choosing , as derived in the following proof.
Expressing the linear combination with cosine and sine
Let , , , , .
Claim: The linear combination can be written as and yields a normalized vector :
Proof:
We compute the dot products , to relate the cosines of the angles (between and ) and (between and ), respectively:
Since , we find
We now solve for and . Since , we have
Since and , we have
Solving analogously for , we obtain
We have shown that
and substitute into the linear combination:
From we can derive
and finally rewrite the linear combination as
Dividing by yields the normalized vector . We therefore receive
Verifying the normalization
Under the given assumptions, we verify that , confirming that is normalized:
In order for to hold, it is required that , as given by the assumption.
Substituting and by and , respectively, we confirm that
Determining the angle θ between the vectors
Claim: is the angle between .
This follows trivially since we have already shown that both and are unit vectors. By definition of the dot product, we have:
which is illustrated in Figure 1.
Generalization to arbitrary lengths
One particular result we get from this - and which will be useful when applying rotations in around arbitrary axes - is the fact that, for any perpendicular vectors , where , the linear combination
yields a vector satisfying
Following the previous proof, we now treat as vectors with arbitrary, but equal length, resulting in the (normalized) linear combination
To cancel out the denominators on the right-hand side, we multiply the equation by 3, yielding
Thus, we obtain a vector with the same magnitude as and .
Rotation around arbitrary points
When rotating a point by a given angle, the transformation depends on the chosen center of rotation.
Figure 2 illustrates the point rotated by . Here, is rotated around the origin of the coordinate plane, resulting in , which can be obtained via the matrix-vector product
where4
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(False)
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)
ax.quiver(0, 0, p_x, p_y, angles='xy', scale_units='xy', scale=1, color=pc, width=0.005, linestyle='--',alpha=0.2)
# 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 * np.cos(theta) - p_y * np.sin(theta),
p_x * np.sin(theta) + p_y * np.cos(theta)
])
ax.quiver(0, 0, pr[0], pr[1], 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)
#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((0, 0),
arc_radius,
arc_radius,
angle=0,
theta1=np.degrees(np.arctan2(p_y, p_x)),
theta2=20 + np.degrees(np.arctan2(p_y, p_x)),
edgecolor=pc)
ax.add_patch(arc)
# texts
ax.text(p_x + 0.2, p_y + 0.2, 'p', color=pc, fontsize=12)
ax.text(pr [0] + 0.2, pr[1] + 0.2 , 'p\'', color=pc, fontsize=12)
ax.text(1, 1, r'$\theta$', color=pc, fontsize=14)
ax.text(r_x - 0.2, r_y + 0.2, 'r ', color=rc, fontsize=12)
plt.show()
Choosing a different center of rotation
Figure 3 illustrates the rotation around . is computed such that
- the rotation center becomes the origin of all points in the coordinate plane
- rotation is applied
- the rotated point is translated back.
Using , we get the expression:

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(False)
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)
ax.quiver(0, 0, p_x, p_y, angles='xy', scale_units='xy', scale=1, color=pc, width=0.005, linestyle='--',alpha=0.2)
# 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 * np.cos(theta) - p_y * np.sin(theta),
p_x * np.sin(theta) + p_y * np.cos(theta)
])
ax.quiver(0, 0, pr[0], pr[1], 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)
#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((0, 0),
arc_radius,
arc_radius,
angle=0,
theta1=np.degrees(np.arctan2(p_y, p_x)),
theta2=20 + np.degrees(np.arctan2(p_y, p_x)),
edgecolor=pc)
ax.add_patch(arc)
# texts
ax.text(p_x + 0.2, p_y + 0.2, 'p', color=pc, fontsize=12)
ax.text(pr [0] + 0.2, pr[1] + 0.2 , 'p\'', color=pc, fontsize=12)
ax.text(1, 1, r'$\theta$', color=pc, fontsize=14)
ax.text(r_x - 0.2, r_y + 0.2, 'r ', color=rc, fontsize=12)
plt.show()
Constructing an orthogonal basis for linear combination
As an alternative to explicitly translating a point to the origin, applying rotation and translating it back, we can directly construct an orthogonal basis around and express the rotated point as a linear combination, as shown in the previous section:
Here, is the vector perpendicular to with the same magnitude, i.e. .
To obtain , we present two practical methods:
-
Using the vector cross product:
The vector cross product yields a vector orthogonal to both and .is unknown and therefor has to be computed.
Since the vector cross product is an operation inherently defined in , we need to temporarily extend our vector by a third component: For , we write .
For , we choose a unit vector along the -axis, , which is perpendicular to our 2-dimensional plane. We then calculate
Dropping the third component in the resulting vector, we end up with , which is perpendicular to , as verified by their dot product:
In fact, the resulting vector is simply rotated by counterclockwise, which will be formally verified with the next method.
-
Using the rotation matrix:
By substituting () into the rotation matrix , we get:Multiplying with , we obtain the perpendicular vector:
which also confirms the claim above: represents rotated by ccw.
In both cases, can be used for finalizing the linear combination as
Without delving into the complexity of the involved operations, it should be evident that translating, rotating by , then translating back is more efficient when operating on a large number of vectors, as it avoids computing an orthogonal basis for each case. However, constructing such a basis becomes necessary when rotations around arbitrary axes must be performed, a topic we will address in the next section.
3D Rotation about an Arbitrary Axis
Rodrigues' Rotation Formula
Rotation of vectors about an arbitrary vector in three dimensions involves a combination of vector projection and the use of a locally defined two-dimensional orthogonal basis. The general method for computing such rotations is also known as the Rodrigues formula5:
or, in its commonly used compact form
We will now proceed to derive the formula, with a special focus on obtaining which represents a coordinate axis in a locally defined orthogonal basis with equal-length axes.
In Figure 4, the vector is rotated about the axis . Since we do not care about the magnitude of , but rather its direction, we normalize the vector and obtain , which will be the center of rotation for in the following derivation.

Plot-Code (Python)
import matplotlib.pyplot as plt
import numpy as np
import math
def arc(ax, center, v1, v2, theta_start, theta_end, r=1.0, steps=100, **kwargs):
thetas = np.linspace(theta_start, theta_end, steps)
arc_points = np.array([
center + r * (np.cos(t) * v1 + np.sin(t) * v2) for t in thetas
])
if kwargs.get('arrow', True):
theta = thetas[-1]
tangent = np.cos(theta)*v2 - np.sin(theta)*v1
tangent = tangent / np.linalg.norm(tangent)
arrow_end = arc_points[-1]
arrow_start = arrow_end - 0.01 * tangent
ax.quiver(*arrow_start, *tangent, length=0.01,
arrow_length_ratio=20, linewidth=1, color=kwargs.get('color', 'black'))
ax.plot(arc_points[:, 0], arc_points[:, 1], arc_points[:, 2], **kwargs)
def rodrigues(theta, v, n):
vn = v / np.linalg.norm(v)
nn = n / np.linalg.norm(n)
vpar = np.dot(nn, v) * nn
vperp = v - vpar
return vpar + (np.cos(theta) * (vperp)) + (np.sin(theta) * np.cross(nn, v))
####################
## CANVAS
####################
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='3d')
ax.set_xlim([0, 6])
ax.set_ylim([0, 6])
ax.set_zlim([0, 6])
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.quiver(0, 0, 0, 1, 0, 0, color='grey', length=6, arrow_length_ratio=0.04)
ax.quiver(0, 0, 0, 0, 1, 0, color='grey', length=6, arrow_length_ratio=0.04)
ax.quiver(0, 0, 0, 0, 0, 1, color='grey', length=6, arrow_length_ratio=0.04)
ax.view_init(elev=10, azim=-45)
####################
## Parameters
####################
origin = np.array([0, 1, 2])
v = np.array([0, 4, 3])
n = np.array([0, 6, 0])
nn = n / np.linalg.norm(n)
#v
ax.quiver(*origin, *v, color='black', arrow_length_ratio=0.05)
ax.text(*(v + [0, -2.0, -1.2] + origin), r'$\vec{v}$', color='black', fontsize=12, horizontalalignment='left')
# v parallel
vpar = np.dot(v, nn) * nn
ax.quiver(*(origin + nn), *(vpar - nn), color='green', linewidth=1, alpha=0.7, arrow_length_ratio=0.1)
ax.text(*(vpar + [0, -1.2, 0.2] + origin), r'$\vec{v}_{\parallel}$', color='green', fontsize=12, horizontalalignment='left')
#v rotated - demo
vdemo = rodrigues(math.pi / 2, v, n)
ax.quiver(*origin, *vdemo, color='black', linewidth=1, alpha=0.7, arrow_length_ratio=0.05)
ax.text(*(vdemo + [0, -2.4, -0] + origin), r'$\vec{v}_{90^\circ}$', color='black', fontsize=12, horizontalalignment='left')
#v rotated
vrot = rodrigues(math.pi / 4, v, n)
ax.quiver(*origin, *vrot, color='black', linewidth=1, alpha=0.7, arrow_length_ratio=0.05)
ax.text(*(vrot + [0, -1.4, -0.2] + origin), r"$\vec{v}'$", color='black', fontsize=12, horizontalalignment='left')
# v perp
vperp = v - vpar
ax.quiver(*(vpar + origin), *vperp, color='blue', linewidth=1, alpha=0.7, arrow_length_ratio=0.15)
ax.text(*(vpar +[-0.4, 0, 1.2] + origin), r'$\vec{v}_{\perp}$', color='blue', fontsize=12, horizontalalignment='left')
#w
w = np.cross(nn, v)
ax.quiver(*(vpar + origin), *w, color='red', linewidth=1, alpha=0.7, arrow_length_ratio=0.1)
ax.text(*(vpar +[2, 0, 0.08] + origin), r'$\vec{w}$', color='red', fontsize=12, horizontalalignment='left')
# v perp 45 degrees
vperp45 = np.cos(math.pi/4) * vperp + np.sin(math.pi/4) * w
ax.quiver(*(vpar + origin), *vperp45, color='blue', linewidth=0.5, alpha=0.7, arrow_length_ratio=0.15)
ax.text(*(vpar + vperp45 + [0, -0.6, -0.8] + origin), r"$\vec{v}'_{\perp}$", color='blue', fontsize=12, horizontalalignment='left')
#n unit n
ax.quiver(*(origin + vpar), *(n - vpar), color='green', linewidth=1, arrow_length_ratio=0.15)
ax.text(*(origin + vpar + [0, 0.8, 0.15]), r"$\vec{n}$", color='green', fontsize=12, horizontalalignment='left')
ax.quiver(*origin, *nn, color='green', linewidth=1, arrow_length_ratio=0.3)
ax.text(*(origin + nn + [0, -0.6, -0.4]), r"$\hat{n}$", color='green', fontsize=12, horizontalalignment='left')
arc(ax, center=origin + vpar, v1=vperp, v2=w,
theta_start=0, theta_end=np.pi/4, r=1,
color='purple', alpha=0.5, linewidth=1, linestyle='dashed')
ax.text(*(vrot + [0, -1.1, 1.2] + origin), r'$45^\circ$', color='purple', fontsize=12, horizontalalignment='left')
plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
plt.show()
Clearly, rotating one vector about another vector in 3D must preserve at least one component of the vector that never changes, specifically the one aligned with the axis of rotation. To illustrate this, consider the special case of rotation about one of the standard basis vectors in :
For example, rotation of a point in the Euclidean plane corresponds to rotating the point in three-dimensional space about the -axis: Here, the -component always stays unchanged, e.g.
Analogous reasoning applies to rotation about the -axis and -axis, where the respective components remain constant.
Applying the rules of vector projection in an orthonormal basis - or an orthogonal basis with equal axis lengths, as we will see shortly - helps us isolate the component of a vector that remains unchanged under rotation. Illustrated in Figure 5 is the orthogonal projection of a vector onto another vector , which yields the component of parallel to .
Plot-Code (Python)
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.ticker import MultipleLocator
# plot layout
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(0, 6)
ax.set_ylim(-1, 4)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(False)
ax.axhline(0, color='black', linewidth=1.0)
ax.axvline(0, color='black', linewidth=1.0)
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
origin = np.array([0, 0])
a = np.array([4, 3])
b = np.array([5, 0])
bn = b / np.linalg.norm(b)
apar = np.dot(a, bn) * bn
aperp = a - apar
ax.quiver(*origin, *a, angles='xy', scale_units='xy', scale=1, color='red')
ax.quiver(*origin, *b, angles='xy', scale_units='xy', scale=1, color='blue')
ax.quiver(*origin, *apar, angles='xy', scale_units='xy', scale=1, color='red')
ax.quiver(*(origin + apar), *aperp, angles='xy', scale_units='xy', width=0.005, scale=1, color='red')
ax.quiver(*origin, *bn, angles='xy', scale_units='xy', scale=1, color='blue')
ax.text(*(apar + aperp - [-0.2, 1.5]), r'$\vec{a}_{\perp} = \vec{a} - \vec{a}_{\parallel}$', color='red', fontsize=12)
ax.text(*(a - [2.5, 1.5]), r'$\vec{a}$', color='red', fontsize=12)
ax.text(*(bn - [0.5, 0.5]), r'$\hat{b}$', color='blue', fontsize=12)
ax.text(*(apar - apar/2 - [0.5 , 0.5]), r'$\hat{a}_{\parallel} = \frac{\vec{a} \cdot \vec{b}}{|\vec{b}|^2}\cdot\vec{b}$', color='red', fontsize=12)
ax.text(*(b - [0.5, 0.5]), r'$\vec{b}$', color='blue', fontsize=12)
plt.show()
Here, represents the -component of vector in the direction of , while the orthogonal (or "sifted out") -component is given by :
It might at first seem confusing that both the parallel and the perpendicular component change under rotation in two dimensions, whereas we previously stated that the projection parallel to the rotation axis remains constant. This raises the question: Where is the rotation axis located in the Euclidean space? Obviously, there is none, as Foley et al. point out6.
This apparent discrepancy resolves once we explicitly construct the rotation axis outside the plane, effectively embedding the plane into a higher dimensional-space.
As demonstrated above, a 2D rotation can be represented in 3D by considering vectors lying in an -plane, perpendicular to the -axis (see Figure 6).
Plot-Code (Python)
import matplotlib.pyplot as plt
import numpy as np
import math
#################
# Canvas
#################
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='3d')
ax.set_xlim([-12, 12])
ax.set_ylim([-12, 12])
ax.set_zlim([-12, 12])
ax.set_xticks(np.arange(-12, 13, 2))
ax.set_yticks(np.arange(-12, 13, 2))
ax.set_zticks(np.arange(-12, 13, 2))
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
origin = np.array([0, 0, 0])
ax.quiver(*origin, 1, 0, 0, color='red', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, -1, 0, 0, color='red', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 1, 0, color='green', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, -1, 0, color='green', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 0, 1, color='blue', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 0, -1, color='blue', length=12, arrow_length_ratio=0.06)
# data
x = np.linspace(-12, 12, 2)
y = np.linspace(-12, 12, 2)
x, y = np.meshgrid(x, y)
z = np.zeros_like(x)
ax.plot_surface(x, y, z, color='lightblue', alpha=0.4)
a = np.array([12, -8, 0])
b = np.array([0, -8, 0])
bn = b / np.linalg.norm(b)
apar = np.dot(a, bn) * bn
aperp = a - apar
ax.quiver(*origin, *a, color='black', length=1, arrow_length_ratio=0.06)
ax.quiver(*origin, *b, color='black', length=1, arrow_length_ratio=0.06)
ax.quiver(*apar, *(aperp), color='black', length=1, arrow_length_ratio=0.06)
ax.text(*origin + [0, -4, 1], r'$\vec{a}_{\parallel}$', color='black', fontsize=12)
ax.text(*origin + [14, 0, 0.5], r'$\vec{a}$', color='black', fontsize=12)
ax.text(*origin + [14, 0, 3.5], r'$\vec{a}_{\perp}$', color='black', fontsize=12)
ax.view_init(elev=10, azim=45)
plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
plt.show()
Here, the orthogonal projection - the parallel component - of any rotated vector onto the rotation axis is always - which corresponds exactly to the situation in two-dimensional rotation. This follows directly from the dot product.
However, once we tilt the plane slightly, such that vectors take the form with , the parallel components of these vectors become nonzero and remain fixed under rotation (see Figure 7).
Plot-Code (Python)
import matplotlib.pyplot as plt
import numpy as np
import math
#################
# Canvas
#################
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='3d')
ax.set_xlim([-12, 12])
ax.set_ylim([-12, 12])
ax.set_zlim([-12, 12])
ax.set_xticks(np.arange(-12, 13, 2))
ax.set_yticks(np.arange(-12, 13, 2))
ax.set_zticks(np.arange(-12, 13, 2))
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
origin = np.array([0, 0, 0])
ax.quiver(*origin, 1, 0, 0, color='red', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, -1, 0, 0, color='red', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 1, 0, color='green', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, -1, 0, color='green', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 0, 1, color='blue', length=12, arrow_length_ratio=0.06)
ax.quiver(*origin, 0, 0, -1, color='blue', length=12, arrow_length_ratio=0.06)
# data
tilt = 0.5
x = np.linspace(-12, 12, 10)
y = np.linspace(-12, 12, 10)
x, y = np.meshgrid(x, y)
z = tilt * x
ax.plot_surface(x, y, z, alpha=0.3, color='lightblue')
a = np.array([12, -8, 12 * tilt])
b = np.array([0, -8, 0])
bn = b / np.linalg.norm(b)
apar = np.dot(a, bn) * bn
aperp = a - apar
z = np.array([0, 0, 1])
zn = z / np.linalg.norm(z)
azpar = np.dot(a, zn) * zn
azperp = a - azpar
ax.quiver(*origin, *z, color='black', length=6, arrow_length_ratio=0.06)
ax.quiver(*origin, *azpar, color='black', length=1, arrow_length_ratio=0.15)
ax.quiver(*azpar, *azperp, color='black', length=1, arrow_length_ratio=0.06)
ax.quiver(*origin, *a, color='black', alpha=0.5, length=1, arrow_length_ratio=0.06)
ax.quiver(*origin, *b, color='black', alpha=0.5, length=1, arrow_length_ratio=0.06)
ax.quiver(*apar, *(aperp), color='black', alpha=0.5, length=1, arrow_length_ratio=0.06)
ax.text(*origin + [0, 1, 3], r'$\vec{a}_{z\parallel}$', color='black', fontsize=12)
ax.view_init(elev=20, azim=85)
plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
plt.show()
Informally speaking, the "fixed projection" of a rotated vector in two dimensions is not absent, its just that it's not "visible" within the plane of rotation.
Knowing that the vector-components parallel to the rotation axis stay fixed and only the orthogonal projection changes, we conclude:
-
is the parallel component of onto the normalized rotation axis . can be computed as
This is the part that never changes during rotation. Thus, the rotated vector is the vector sum of the parallel projection and the orthogonal projection after rotation.
-
is the perpendicular component of and lies in a two-dimensional subspace perpendicular to . This component , also called the "rejection" of from ([📖Len16, p. 29]), is obtained by subtracting the parallel component from :
-
While the parallel component of stays fixed under rotation, the perpendicular component in respect to the projection changes. It can be expressed by the linear combination
Here, the locally defined two-dimensional orthogonal basis is given by and the vector . For the linear combination to ensure that the resulting vector has the same length as , the vector must have the same magnitude as and correspond to a counterclockwise rotation of . The existence of such a vector was shown in the previous section. We can obtain the orthogonal basis by computing via
Since and , the length of is verified with the vector cross product
Deriving the Rotation Matrices
We now derive as the result of rotating the vector by an angle about the rotation axis using the locally defined two-dimensional orthogonal basis, with coordinate axes and .
which leads to the commonly used compact form of the Rodrigues formula:
Solving componentwise, we obtain
Using a specific rotation axis, we verify that follows directly from the Rodrigues formula. We rotate by around the -axis of the orthonormal standard basis in .
Hence, substituting by into the formula, we derive :