Chapter 10: Three-Dimensional Linear Transformationsยถ

Introductionยถ

Everything weโ€™ve learned about 2D linear transformations extends beautifully to 3D!

Key Differences in 3Dยถ

  • Basis vectors: Three instead of two (รฎ, ฤต, kฬ‚)

  • Matrices: 3ร—3 instead of 2ร—2

  • Determinant: Measures volume scaling (not area!)

  • Rotations: Need to specify an axis to rotate around

The Big Pictureยถ

Just like 2D:

  • Columns of matrix = where basis vectors land

  • Transformations morph 3D space

  • Grid lines stay parallel and evenly spaced

But in 3D, weโ€™re morphing all of space, not just a plane!

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import seaborn as sns

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (12, 10)
np.set_printoptions(precision=3, suppress=True)

1. 3D Basis Vectorsยถ

In 3D, we have three standard basis vectors:

\[\begin{split} \hat{\imath} = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}, \quad \hat{\jmath} = \begin{bmatrix} 0 \\ 1 \\ 0 \end{bmatrix}, \quad \hat{k} = \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} \end{split}\]

Geometric picture:

  • รฎ points along the x-axis

  • ฤต points along the y-axis

  • kฬ‚ points along the z-axis

Any 3D vector can be expressed as:

\[\begin{split} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = x\hat{\imath} + y\hat{\jmath} + z\hat{k} \end{split}\]
def draw_3d_vector(ax, origin, vector, color='blue', label=None):
    """Draw a 3D vector as an arrow."""
    ax.quiver(origin[0], origin[1], origin[2],
             vector[0], vector[1], vector[2],
             color=color, arrow_length_ratio=0.15,
             linewidth=2.5, label=label)

def setup_3d_ax(ax, lim=(-2, 2)):
    """Setup 3D axis."""
    ax.set_xlim(lim)
    ax.set_ylim(lim)
    ax.set_zlim(lim)
    ax.set_xlabel('X', fontsize=12)
    ax.set_ylabel('Y', fontsize=12)
    ax.set_zlabel('Z', fontsize=12)
    ax.grid(True, alpha=0.3)

# Visualize 3D basis
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

i_hat = np.array([1, 0, 0])
j_hat = np.array([0, 1, 0])
k_hat = np.array([0, 0, 1])

draw_3d_vector(ax, np.zeros(3), i_hat, 'red', 'รฎ')
draw_3d_vector(ax, np.zeros(3), j_hat, 'green', 'ฤต')
draw_3d_vector(ax, np.zeros(3), k_hat, 'blue', 'kฬ‚')

setup_3d_ax(ax)
ax.legend(fontsize=11)
ax.set_title('3D Standard Basis Vectors', fontsize=14, fontweight='bold')
plt.show()

print('Standard 3D basis vectors:')
print(f'รฎ = {i_hat}')
print(f'ฤต = {j_hat}')
print(f'kฬ‚ = {k_hat}')

2. Reading 3ร—3 Matricesยถ

A 3ร—3 matrix represents a linear transformation in 3D:

\[\begin{split} A = \begin{bmatrix} a & b & c \\ d & e & f \\ g & h & i \end{bmatrix} \end{split}\]

How to read it:

  • First column = where รฎ lands

  • Second column = where ฤต lands

  • Third column = where kฬ‚ lands

Example:

\[\begin{split} \begin{bmatrix} 2 & 0 & 1 \\ 0 & 1 & 0 \\ 0 & 0 & 2 \end{bmatrix} \end{split}\]
  • รฎ โ†’ [2, 0, 0] (stretched by 2 in x)

  • ฤต โ†’ [0, 1, 0] (unchanged)

  • kฬ‚ โ†’ [1, 0, 2] (moved and stretched)

def draw_unit_cube(ax, color='cyan', alpha=0.1):
    """Draw a unit cube."""
    # Define vertices
    vertices = np.array([
        [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],  # bottom
        [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]   # top
    ])
    
    # Define faces
    faces = [
        [vertices[0], vertices[1], vertices[5], vertices[4]],  # front
        [vertices[2], vertices[3], vertices[7], vertices[6]],  # back
        [vertices[0], vertices[3], vertices[7], vertices[4]],  # left
        [vertices[1], vertices[2], vertices[6], vertices[5]],  # right
        [vertices[0], vertices[1], vertices[2], vertices[3]],  # bottom
        [vertices[4], vertices[5], vertices[6], vertices[7]]   # top
    ]
    
    cube = Poly3DCollection(faces, alpha=alpha, facecolor=color, edgecolor='black', linewidths=1)
    ax.add_collection3d(cube)

def visualize_3d_transformation(A, title):
    """Visualize how a 3x3 matrix transforms the unit cube."""
    fig = plt.figure(figsize=(14, 6))
    
    # Before transformation
    ax1 = fig.add_subplot(121, projection='3d')
    draw_unit_cube(ax1)
    draw_3d_vector(ax1, np.zeros(3), np.array([1, 0, 0]), 'red', 'รฎ')
    draw_3d_vector(ax1, np.zeros(3), np.array([0, 1, 0]), 'green', 'ฤต')
    draw_3d_vector(ax1, np.zeros(3), np.array([0, 0, 1]), 'blue', 'kฬ‚')
    setup_3d_ax(ax1, lim=(-1, 3))
    ax1.set_title('Before Transformation', fontsize=13, fontweight='bold')
    ax1.legend()
    
    # After transformation
    ax2 = fig.add_subplot(122, projection='3d')
    
    # Transform cube vertices
    vertices = np.array([
        [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
        [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]
    ]).T
    transformed = A @ vertices
    
    # Draw transformed cube
    t_verts = transformed.T
    faces = [
        [t_verts[0], t_verts[1], t_verts[5], t_verts[4]],
        [t_verts[2], t_verts[3], t_verts[7], t_verts[6]],
        [t_verts[0], t_verts[3], t_verts[7], t_verts[4]],
        [t_verts[1], t_verts[2], t_verts[6], t_verts[5]],
        [t_verts[0], t_verts[1], t_verts[2], t_verts[3]],
        [t_verts[4], t_verts[5], t_verts[6], t_verts[7]]
    ]
    cube = Poly3DCollection(faces, alpha=0.1, facecolor='orange', edgecolor='black', linewidths=1)
    ax2.add_collection3d(cube)
    
    # Draw transformed basis
    i_new = A @ np.array([1, 0, 0])
    j_new = A @ np.array([0, 1, 0])
    k_new = A @ np.array([0, 0, 1])
    draw_3d_vector(ax2, np.zeros(3), i_new, 'red', 'รฎ transformed')
    draw_3d_vector(ax2, np.zeros(3), j_new, 'green', 'ฤต transformed')
    draw_3d_vector(ax2, np.zeros(3), k_new, 'blue', 'kฬ‚ transformed')
    
    setup_3d_ax(ax2, lim=(-1, 3))
    ax2.set_title(f'After: {title}', fontsize=13, fontweight='bold')
    ax2.legend()
    
    plt.tight_layout()
    plt.show()

# Example transformation
A = np.array([[2, 0, 1],
              [0, 1, 0],
              [0, 0, 2]])

print(f"Matrix A:\n{A}\n")
print(f"รฎ โ†’ {A[:, 0]}")
print(f"ฤต โ†’ {A[:, 1]}")
print(f"kฬ‚ โ†’ {A[:, 2]}")

visualize_3d_transformation(A, "Scaling and Shearing")

3. Determinants in 3D: Volume Scalingยถ

In 2D, the determinant measured area scaling.
In 3D, the determinant measures volume scaling!

Key concept:

  • The unit cube has volume = 1

  • After transformation, it becomes a parallelepiped

  • det(A) = volume of that parallelepiped

Sign of determinant:

  • Positive: Orientation preserved (right-hand rule maintained)

  • Negative: Orientation flipped (like turning space inside-out)

  • Zero: Space squished to lower dimension (3D โ†’ plane or line)

\[ \text{det}(A) = \text{volume scaling factor} \]
def visualize_3d_determinant():
    """Show how determinant relates to volume scaling."""
    matrices = [
        ("Identity (det=1)", np.eye(3)),
        ("Scaling by 2 (det=8)", 2 * np.eye(3)),
        ("Rotation (det=1)", np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])),
        ("Squished (detโ‰ˆ0)", np.array([[1, 0, 0.5], [0, 1, 0.5], [0, 0, 0.1]]))
    ]
    
    for name, A in matrices:
        det = np.linalg.det(A)
        print(f"\n{'='*60}")
        print(f"{name}")
        print(f"{'='*60}")
        print(f"Matrix:\n{A}")
        print(f"\nDeterminant = {det:.6f}")
        print(f"Volume scaling = {abs(det):.2f}x")
        if det > 0:
            print("Orientation: Preserved โœ“")
        elif det < 0:
            print("Orientation: Flipped โ†ป")
        else:
            print("Dimension: Squished to lower!")

visualize_3d_determinant()

4. 3D Rotationsยถ

In 2D, rotation is simple: one angle around the origin.
In 3D, we need to specify which axis to rotate around!

Rotation around the z-axisยถ

\[\begin{split} R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \end{split}\]

Notice: This looks like 2D rotation with an extra โ€œ1โ€ at the bottom-right!

Rotation around the x-axisยถ

\[\begin{split} R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta \\ 0 & \sin\theta & \cos\theta \end{bmatrix} \end{split}\]

Rotation around the y-axisยถ

\[\begin{split} R_y(\theta) = \begin{bmatrix} \cos\theta & 0 & \sin\theta \\ 0 & 1 & 0 \\ -\sin\theta & 0 & \cos\theta \end{bmatrix} \end{split}\]

Right-hand rule: Curl fingers in rotation direction, thumb points along axis!

def rotation_matrix_x(theta):
    """Rotation around x-axis."""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[1, 0, 0],
                     [0, c, -s],
                     [0, s, c]])

def rotation_matrix_y(theta):
    """Rotation around y-axis."""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[c, 0, s],
                     [0, 1, 0],
                     [-s, 0, c]])

def rotation_matrix_z(theta):
    """Rotation around z-axis."""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[c, -s, 0],
                     [s, c, 0],
                     [0, 0, 1]])

# Demonstrate rotations
angle = np.pi / 4  # 45 degrees

print("Rotation Matrices (45ยฐ rotations):\n")
print("Around x-axis:")
print(rotation_matrix_x(angle))
print(f"\nDeterminant = {np.linalg.det(rotation_matrix_x(angle)):.6f} (preserves volume!)\n")

print("Around y-axis:")
print(rotation_matrix_y(angle))
print(f"\nDeterminant = {np.linalg.det(rotation_matrix_y(angle)):.6f}\n")

print("Around z-axis:")
print(rotation_matrix_z(angle))
print(f"\nDeterminant = {np.linalg.det(rotation_matrix_z(angle)):.6f}")

# Visualize rotation around z-axis
visualize_3d_transformation(rotation_matrix_z(np.pi/3), "60ยฐ Rotation around Z-axis")

5. Composite Transformations in 3Dยถ

Just like in 2D, we can combine transformations by multiplying matrices!

Example: Rotate around z-axis, then scale

\[ M = S \cdot R_z(\theta) \]

Remember: Read right to left! (First \(R_z\), then \(S\))

Cool application: Any 3D rotation can be decomposed into rotations around x, y, and z axes (Euler angles)!

# Composite transformation: rotate then scale
theta = np.pi / 4
R = rotation_matrix_z(theta)
S = np.diag([2, 1, 1.5])  # Scale x by 2, z by 1.5

# Composite
M = S @ R

print("Rotation matrix:")
print(R)
print("\nScaling matrix:")
print(S)
print("\nComposite M = S ร— R:")
print(M)
print(f"\nDeterminant of composite: {np.linalg.det(M):.6f}")

visualize_3d_transformation(M, "Rotate 45ยฐ then Scale")

# Test on a vector
v = np.array([1, 0, 0])
print(f"\nOriginal vector: {v}")
print(f"After rotation: {R @ v}")
print(f"After composite: {M @ v}")

6. Column Space and Rank in 3Dยถ

In 3D, the column space can have different dimensions:

Rank 3 (full rank):

  • Column space = all of 3D space

  • Transformation is reversible (invertible)

  • det(A) โ‰  0

Rank 2:

  • Column space = a 2D plane through origin

  • 3D space gets squished to a plane

  • det(A) = 0

Rank 1:

  • Column space = a line through origin

  • Everything squishes to a line

  • det(A) = 0

Rank 0:

  • Everything maps to origin

  • det(A) = 0

Key insight: rank = number of dimensions that โ€œsurviveโ€ the transformation!

def analyze_3d_rank():
    """Analyze rank for different 3D matrices."""
    matrices = [
        ("Full Rank (3)", np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]])),
        ("Rank 2 (Plane)", np.array([[1, 0, 2], [0, 1, 3], [0, 0, 0]])),
        ("Rank 1 (Line)", np.array([[1, 2, 3], [2, 4, 6], [3, 6, 9]])),
    ]
    
    for name, A in matrices:
        rank = np.linalg.matrix_rank(A)
        det = np.linalg.det(A)
        
        print(f"\n{'='*60}")
        print(f"{name}")
        print(f"{'='*60}")
        print(f"Matrix:\n{A}")
        print(f"\nRank: {rank}")
        print(f"Determinant: {det:.6f}")
        print(f"Column space dimension: {rank}D")
        
        if rank == 3:
            print("โœ“ Full rank - transformation is invertible!")
        elif rank == 2:
            print("โ†’ Squished to a plane")
        elif rank == 1:
            print("โ†’ Squished to a line")
        else:
            print("โ†’ Everything maps to origin")

analyze_3d_rank()

7. Applications: Computer Graphicsยถ

3D transformations are the foundation of computer graphics!

Graphics Pipeline:

  1. Model transformation: Position objects in the world

  2. View transformation: Position camera

  3. Projection transformation: 3D โ†’ 2D screen

Each step uses matrix multiplication!

Common operations:

  • Rotating objects (Rx, Ry, Rz)

  • Scaling objects (S)

  • Translating objects (using homogeneous coordinates)

  • Camera positioning

  • Perspective projection

Modern GPUs are optimized for matrix multiplication!

def demonstrate_3d_graphics():
    """Show how 3D graphics uses transformations."""
    print("3D Graphics Transformation Pipeline\n")
    
    # Define a simple 3D object (cube vertices)
    cube = np.array([
        [0, 1, 1, 0, 0, 1, 1, 0],
        [0, 0, 1, 1, 0, 0, 1, 1],
        [0, 0, 0, 0, 1, 1, 1, 1]
    ])
    
    print("Step 1: Model Transformation (scale and rotate)")
    scale = np.diag([1.5, 1, 1])
    rotate = rotation_matrix_y(np.pi / 6)
    model = rotate @ scale
    cube_model = model @ cube
    print(f"Model matrix:\n{model}\n")
    
    print("Step 2: View Transformation (position camera)")
    # Simple view: translate away from camera
    view = np.eye(3)  # Simplified (normally 4x4 with translation)
    cube_view = view @ cube_model
    print(f"View matrix:\n{view}\n")
    
    print("Step 3: Projection (3D โ†’ 2D)")
    # Orthographic projection (drop z-coordinate)
    projection = np.array([[1, 0, 0],
                           [0, 1, 0]])
    cube_screen = projection @ cube_view
    print(f"Projection matrix:\n{projection}\n")
    
    print(f"Final 2D screen coordinates:\n{cube_screen}\n")
    
    # Visualize the result
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
    
    # 3D view
    ax1 = plt.subplot(121, projection='3d')
    ax1.scatter(cube_model[0], cube_model[1], cube_model[2], c='blue', s=100)
    ax1.set_title('3D Object (after model transform)', fontweight='bold')
    ax1.set_xlabel('X')
    ax1.set_ylabel('Y')
    ax1.set_zlabel('Z')
    
    # 2D projection
    ax2.scatter(cube_screen[0], cube_screen[1], c='red', s=100)
    ax2.set_xlim(-2, 2)
    ax2.set_ylim(-2, 2)
    ax2.set_aspect('equal')
    ax2.set_title('2D Screen (after projection)', fontweight='bold')
    ax2.set_xlabel('Screen X')
    ax2.set_ylabel('Screen Y')
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

demonstrate_3d_graphics()

8. Cross Product Connectionยถ

The cross product in 3D is intimately connected to determinants!

\[\begin{split} \vec{v} \times \vec{w} = \text{det}\begin{bmatrix} \hat{\imath} & \hat{\jmath} & \hat{k} \\ v_1 & v_2 & v_3 \\ w_1 & w_2 & w_3 \end{bmatrix} \end{split}\]

Geometric meaning:

  • Result is perpendicular to both v and w

  • Magnitude = area of parallelogram spanned by v and w

  • Direction follows right-hand rule

Volume interpretation: The scalar triple product gives the volume of a parallelepiped:

\[\begin{split} \vec{u} \cdot (\vec{v} \times \vec{w}) = \text{det}\begin{bmatrix} u_1 & v_1 & w_1 \\ u_2 & v_2 & w_2 \\ u_3 & v_3 & w_3 \end{bmatrix} \end{split}\]
def visualize_cross_product_3d():
    """Demonstrate cross product as perpendicular vector."""
    v = np.array([1, 0, 0.5])
    w = np.array([0, 1, 0.3])
    
    # Compute cross product
    cross = np.cross(v, w)
    
    # Verify perpendicularity
    dot_v = np.dot(cross, v)
    dot_w = np.dot(cross, w)
    
    print("Cross Product in 3D\n")
    print(f"v = {v}")
    print(f"w = {w}")
    print(f"\nv ร— w = {cross}")
    print(f"\nMagnitude |v ร— w| = {np.linalg.norm(cross):.4f}")
    print(f"\nVerifying perpendicularity:")
    print(f"(v ร— w) ยท v = {dot_v:.10f} โ‰ˆ 0 โœ“")
    print(f"(v ร— w) ยท w = {dot_w:.10f} โ‰ˆ 0 โœ“")
    
    # Visualize
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    draw_3d_vector(ax, np.zeros(3), v, 'blue', 'v')
    draw_3d_vector(ax, np.zeros(3), w, 'green', 'w')
    draw_3d_vector(ax, np.zeros(3), cross, 'red', 'v ร— w (perpendicular!)')
    
    setup_3d_ax(ax, lim=(-1, 2))
    ax.legend(fontsize=11)
    ax.set_title('Cross Product: Perpendicular to Both Vectors', 
                fontsize=14, fontweight='bold')
    plt.show()

visualize_cross_product_3d()

9. Summary and Key Takeawaysยถ

3D vs 2Dยถ

Concept

2D

3D

Basis vectors

รฎ, ฤต

รฎ, ฤต, kฬ‚

Matrix size

2ร—2

3ร—3

Determinant

Area scaling

Volume scaling

Rotation

One angle

Axis + angle

Full rank

2

3

Key Insightsยถ

  1. Reading matrices: Columns = where basis vectors land

  2. Determinant: Volume scaling factor (can be negative!)

  3. Rotations: Need to specify an axis (Rx, Ry, Rz)

  4. Rank: Dimensions that survive transformation

  5. Applications: Computer graphics, robotics, physics simulations

Mental Modelยถ

Think of 3D transformations as morphing all of 3D space:

  • Grid lines stay parallel and evenly spaced

  • Origin stays fixed

  • Everything is determined by where รฎ, ฤต, kฬ‚ land

Next Stepsยถ

  • Practice with rotation matrices

  • Explore quaternions (better for animations!)

  • Study homogeneous coordinates (4ร—4 matrices for translation)

  • Learn about perspective projection

Remember: The geometric intuition from 2D extends beautifully to 3D and beyond!

10. Practice Exercisesยถ

Exercise 1: 3D Transformationsยถ

For the matrix:

\[\begin{split} A = \begin{bmatrix} 2 & 0 & 1 \\ 1 & 3 & 0 \\ 0 & 1 & 2 \end{bmatrix} \end{split}\]

a) Where do รฎ, ฤต, and kฬ‚ land?
b) What is the determinant?
c) Is this transformation invertible?
d) Apply it to the vector [1, 0, 1]

# Your code here
A = np.array([[2, 0, 1],
              [1, 3, 0],
              [0, 1, 2]])

print("a) Basis vectors after transformation:")
print(f"   รฎ โ†’ {A[:, 0]}")
print(f"   ฤต โ†’ {A[:, 1]}")
print(f"   kฬ‚ โ†’ {A[:, 2]}")

det_A = np.linalg.det(A)
print(f"\nb) Determinant = {det_A:.6f}")
print(f"   Volume scaling factor: {det_A:.2f}x")

print(f"\nc) Invertible? {det_A != 0}")
if det_A != 0:
    print(f"   Inverse exists! โœ“")

v = np.array([1, 0, 1])
Av = A @ v
print(f"\nd) A ร— [1, 0, 1] = {Av}")

Exercise 2: Rotationsยถ

Create a transformation that:

  1. Rotates 90ยฐ around the z-axis

  2. Then rotates 45ยฐ around the x-axis

Apply it to the vector [1, 1, 0]

# Your code here
# Step 1: Rotate 90ยฐ around z-axis
R_z = rotation_matrix_z(np.pi / 2)

# Step 2: Rotate 45ยฐ around x-axis
R_x = rotation_matrix_x(np.pi / 4)

# Composite (remember: right to left!)
M = R_x @ R_z

v = np.array([1, 1, 0])
result = M @ v

print("Rotation around z (90ยฐ):")
print(R_z)
print("\nRotation around x (45ยฐ):")
print(R_x)
print("\nComposite M = Rx ร— Rz:")
print(M)
print(f"\nOriginal vector: {v}")
print(f"After transformations: {result}")
print(f"\nMagnitude preserved? {np.isclose(np.linalg.norm(v), np.linalg.norm(result))}")

Exercise 3: Rank and Column Spaceยถ

Determine the rank and describe the column space for:

\[\begin{split} B = \begin{bmatrix} 1 & 2 & 3 \\ 0 & 0 & 0 \\ 2 & 4 & 6 \end{bmatrix} \end{split}\]
# Your code here
B = np.array([[1, 2, 3],
              [0, 0, 0],
              [2, 4, 6]])

rank = np.linalg.matrix_rank(B)
det_B = np.linalg.det(B)

print(f"Matrix B:\n{B}")
print(f"\nRank: {rank}")
print(f"Determinant: {det_B:.10f}")

if rank == 1:
    print("\nColumn space: A line through the origin")
    print("All three columns are parallel!")
    print(f"Column 1: {B[:, 0]}")
    print(f"Column 2: {B[:, 1]} = 2 ร— column 1")
    print(f"Column 3: {B[:, 2]} = 3 ร— column 1")
elif rank == 2:
    print("\nColumn space: A plane through the origin")
else:
    print("\nColumn space: All of 3D space")

Further Readingยถ

  • Quaternions: Better than Euler angles for smooth rotations

  • Homogeneous coordinates: 4ร—4 matrices for translation

  • Gimbal lock: Problem with Euler angles

  • Perspective projection: How 3D becomes 2D on screen

  • Robotics: Forward and inverse kinematics using 3D transforms

Next: Eigenvalues and eigenvectors - the natural axes of transformations!