Skip to content

Latest commit

 

History

History
794 lines (627 loc) · 24.9 KB

3D Graphics Foundations Rendering Triangles in Python.md

File metadata and controls

794 lines (627 loc) · 24.9 KB

3D Graphics Foundations Rendering Triangles in Python

Slide 1: Understanding 3D Coordinate Systems in Python

The foundation of 3D graphics begins with understanding coordinate systems. In computer graphics, we represent points in 3D space using vectors containing X, Y, and Z coordinates. Python's numpy library provides efficient tools for handling these coordinate systems.

import numpy as np

class Point3D:
    def __init__(self, x, y, z):
        self.coords = np.array([x, y, z])
    
    def get_coordinates(self):
        return self.coords

# Create a triangle in 3D space
triangle = [
    Point3D(1, 0, 0),
    Point3D(0, 1, 0),
    Point3D(0, 0, 1)
]

# Display triangle vertices
for i, point in enumerate(triangle):
    print(f"Vertex {i + 1}: {point.get_coordinates()}")

# Output:
# Vertex 1: [1 0 0]
# Vertex 2: [0 1 0]
# Vertex 3: [0 0 1]

Slide 2: Basic Matrix Transformations

Matrix transformations are fundamental operations in 3D graphics. They allow us to rotate, scale, and translate objects in 3D space. These transformations are represented as 4x4 matrices using homogeneous coordinates.

import numpy as np

def create_rotation_matrix(angle, axis='x'):
    """Create rotation matrix for specified axis and angle (in radians)"""
    c, s = np.cos(angle), np.sin(angle)
    if axis.lower() == 'x':
        return np.array([
            [1, 0,  0, 0],
            [0, c, -s, 0],
            [0, s,  c, 0],
            [0, 0,  0, 1]
        ])
    elif axis.lower() == 'y':
        return np.array([
            [ c, 0, s, 0],
            [ 0, 1, 0, 0],
            [-s, 0, c, 0],
            [ 0, 0, 0, 1]
        ])
    else:  # z-axis
        return np.array([
            [c, -s, 0, 0],
            [s,  c, 0, 0],
            [0,  0, 1, 0],
            [0,  0, 0, 1]
        ])

# Example rotation of 45 degrees around X axis
angle = np.pi/4  # 45 degrees
rotation_matrix = create_rotation_matrix(angle, 'x')
print("Rotation Matrix (45° around X):\n", rotation_matrix)

Slide 3: Vector-Matrix Multiplication Implementation

Understanding how vectors and matrices multiply is crucial for 3D transformations. We implement a custom class that handles vector-matrix multiplication for 3D graphics calculations efficiently.

import numpy as np

class Transform3D:
    def __init__(self, matrix=None):
        self.matrix = matrix if matrix is not None else np.eye(4)
    
    def apply_to_point(self, point):
        # Convert 3D point to homogeneous coordinates
        homogeneous_point = np.append(point, 1)
        # Apply transformation
        transformed = np.dot(self.matrix, homogeneous_point)
        # Convert back to 3D coordinates
        return transformed[:3] / transformed[3]

# Example usage
point = np.array([1, 0, 0])
rotation = create_rotation_matrix(np.pi/4, 'z')
transform = Transform3D(rotation)

rotated_point = transform.apply_to_point(point)
print(f"Original point: {point}")
print(f"Rotated point: {rotated_point}")

Slide 4: Building a Mesh Class

The Mesh class serves as the foundation for representing 3D objects. It manages collections of vertices and faces, providing methods for transformation and manipulation of 3D geometry.

class Mesh:
    def __init__(self, vertices, faces):
        self.vertices = np.array(vertices)
        self.faces = np.array(faces)
        
    def transform(self, transformation_matrix):
        # Create homogeneous coordinates
        homogeneous_vertices = np.hstack((
            self.vertices, 
            np.ones((len(self.vertices), 1))
        ))
        
        # Apply transformation
        transformed = np.dot(homogeneous_vertices, 
                           transformation_matrix.T)
        
        # Convert back to 3D coordinates
        self.vertices = transformed[:, :3] / transformed[:, 3:]
        
    def get_triangles(self):
        return [self.vertices[face] for face in self.faces]

# Create a simple pyramid
vertices = np.array([
    [0, 0, 0],  # base
    [1, 0, 0],
    [1, 1, 0],
    [0, 1, 0],
    [0.5, 0.5, 1]  # apex
])

faces = np.array([
    [0, 1, 4],  # triangular faces
    [1, 2, 4],
    [2, 3, 4],
    [3, 0, 4]
])

pyramid = Mesh(vertices, faces)

Slide 5: Implementing Rotation Transformations

A comprehensive implementation of rotation transformations requires handling Euler angles and quaternions. This implementation demonstrates how to create and combine multiple rotation transformations for complex 3D movements.

import numpy as np
from math import cos, sin

class Rotation3D:
    @staticmethod
    def from_euler(phi, theta, psi):
        # Create rotation matrices for each axis
        Rx = np.array([
            [1, 0, 0],
            [0, cos(phi), -sin(phi)],
            [0, sin(phi), cos(phi)]
        ])
        
        Ry = np.array([
            [cos(theta), 0, sin(theta)],
            [0, 1, 0],
            [-sin(theta), 0, cos(theta)]
        ])
        
        Rz = np.array([
            [cos(psi), -sin(psi), 0],
            [sin(psi), cos(psi), 0],
            [0, 0, 1]
        ])
        
        # Combine rotations (order: Z * Y * X)
        return np.dot(Rz, np.dot(Ry, Rx))

# Example usage
angles = [np.pi/4, np.pi/6, np.pi/3]  # 45°, 30°, 60°
combined_rotation = Rotation3D.from_euler(*angles)
print("Combined rotation matrix:\n", combined_rotation)

Slide 6: Projection Matrix Implementation

The projection matrix transforms 3D coordinates into 2D screen coordinates, simulating perspective. This implementation includes both perspective and orthographic projection matrices commonly used in computer graphics.

import numpy as np

class ProjectionMatrix:
    @staticmethod
    def perspective(fov, aspect, near, far):
        f = 1.0 / np.tan(fov / 2)
        return np.array([
            [f/aspect, 0, 0, 0],
            [0, f, 0, 0],
            [0, 0, (far+near)/(near-far), (2*far*near)/(near-far)],
            [0, 0, -1, 0]
        ])
    
    @staticmethod
    def orthographic(left, right, bottom, top, near, far):
        return np.array([
            [2/(right-left), 0, 0, -(right+left)/(right-left)],
            [0, 2/(top-bottom), 0, -(top+bottom)/(top-bottom)],
            [0, 0, -2/(far-near), -(far+near)/(far-near)],
            [0, 0, 0, 1]
        ])

# Example usage
perspective = ProjectionMatrix.perspective(
    fov=np.pi/4,    # 45 degrees
    aspect=16/9,    # Widescreen aspect ratio
    near=0.1,       # Near clipping plane
    far=100.0       # Far clipping plane
)

print("Perspective projection matrix:\n", perspective)

Slide 7: Implementing the Graphics Pipeline

The graphics pipeline transforms 3D vertices through multiple stages: model transformation, view transformation, and projection. This implementation demonstrates the complete pipeline used in modern graphics engines.

class GraphicsPipeline:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.model_matrix = np.eye(4)
        self.view_matrix = np.eye(4)
        self.projection_matrix = ProjectionMatrix.perspective(
            np.pi/4, width/height, 0.1, 100.0
        )
    
    def set_camera(self, position, target, up):
        forward = target - position
        forward = forward / np.linalg.norm(forward)
        right = np.cross(forward, up)
        right = right / np.linalg.norm(right)
        up = np.cross(right, forward)
        
        self.view_matrix = np.array([
            [right[0], right[1], right[2], -np.dot(right, position)],
            [up[0], up[1], up[2], -np.dot(up, position)],
            [-forward[0], -forward[1], -forward[2], np.dot(forward, position)],
            [0, 0, 0, 1]
        ])
    
    def transform_vertex(self, vertex):
        # Convert to homogeneous coordinates
        v = np.append(vertex, 1)
        
        # Apply transformations
        v = np.dot(self.model_matrix, v)
        v = np.dot(self.view_matrix, v)
        v = np.dot(self.projection_matrix, v)
        
        # Perspective divide
        if v[3] != 0:
            v = v / v[3]
        
        # Convert to screen coordinates
        screen_x = int((v[0] + 1) * self.width / 2)
        screen_y = int((1 - v[1]) * self.height / 2)
        
        return np.array([screen_x, screen_y])

# Example usage
pipeline = GraphicsPipeline(1920, 1080)
pipeline.set_camera(
    position=np.array([0, 0, 5]),
    target=np.array([0, 0, 0]),
    up=np.array([0, 1, 0])
)

# Transform a vertex
vertex = np.array([1, 1, 1])
screen_coords = pipeline.transform_vertex(vertex)
print(f"Screen coordinates: {screen_coords}")

Slide 8: Matrix Performance Optimization

Understanding matrix multiplication performance is crucial for graphics applications. This implementation demonstrates various optimization techniques including vectorization and cache-friendly memory access patterns.

import numpy as np
import time

class OptimizedMatrixOps:
    @staticmethod
    def naive_multiply(A, B):
        n = len(A)
        result = np.zeros((n, n))
        for i in range(n):
            for j in range(n):
                for k in range(n):
                    result[i][j] += A[i][k] * B[k][j]
        return result
    
    @staticmethod
    def optimized_multiply(A, B):
        # Using numpy's optimized dot product
        return np.dot(A, B)
    
    @staticmethod
    def benchmark(size=1000):
        A = np.random.rand(size, size)
        B = np.random.rand(size, size)
        
        start = time.time()
        _ = OptimizedMatrixOps.optimized_multiply(A, B)
        optimized_time = time.time() - start
        
        if size <= 100:  # Only run naive for small matrices
            start = time.time()
            _ = OptimizedMatrixOps.naive_multiply(A, B)
            naive_time = time.time() - start
            print(f"Naive multiplication time: {naive_time:.4f}s")
            
        print(f"Optimized multiplication time: {optimized_time:.4f}s")

# Run benchmark
OptimizedMatrixOps.benchmark(size=100)

Slide 9: Implementing Model View Projection (MVP)

The Model View Projection matrix combines object transformation, camera positioning, and perspective projection. This implementation shows how to construct and apply the complete MVP transformation pipeline.

class MVPTransform:
    def __init__(self):
        self.model = np.eye(4)
        self.view = np.eye(4)
        self.projection = np.eye(4)
    
    def set_model_transform(self, position, rotation, scale):
        # Translation matrix
        translation = np.array([
            [1, 0, 0, position[0]],
            [0, 1, 0, position[1]],
            [0, 0, 1, position[2]],
            [0, 0, 0, 1]
        ])
        
        # Scale matrix
        scale_matrix = np.array([
            [scale[0], 0, 0, 0],
            [0, scale[1], 0, 0],
            [0, 0, scale[2], 0],
            [0, 0, 0, 1]
        ])
        
        # Get rotation matrix using previous Rotation3D class
        rotation_matrix = np.eye(4)
        rotation_matrix[:3, :3] = Rotation3D.from_euler(*rotation)
        
        # Combine transformations
        self.model = translation @ rotation_matrix @ scale_matrix
    
    def get_mvp(self):
        return self.projection @ self.view @ self.model
    
    def transform_vertices(self, vertices):
        # Convert vertices to homogeneous coordinates
        homogeneous = np.hstack((vertices, np.ones((len(vertices), 1))))
        
        # Apply MVP transformation
        mvp = self.get_mvp()
        transformed = homogeneous @ mvp.T
        
        # Perspective division
        transformed = transformed[:, :3] / transformed[:, 3:]
        return transformed

# Example usage
mvp = MVPTransform()
mvp.set_model_transform(
    position=np.array([0, 0, -5]),
    rotation=np.array([0, np.pi/4, 0]),
    scale=np.array([1, 1, 1])
)

# Transform vertices of a cube
cube_vertices = np.array([
    [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
    [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]
])

transformed_vertices = mvp.transform_vertices(cube_vertices)
print("Transformed vertices:\n", transformed_vertices)

Slide 10: GPU-like Parallel Processing Simulation

This implementation simulates GPU-like parallel processing for matrix operations using Python's multiprocessing capabilities, demonstrating how GPUs accelerate graphics computations.

import multiprocessing as mp
from functools import partial
import numpy as np

class GPUSimulator:
    def __init__(self, num_processors=None):
        self.num_processors = num_processors or mp.cpu_count()
    
    def parallel_matrix_multiply(self, A, B):
        pool = mp.Pool(self.num_processors)
        result = np.zeros_like(A)
        
        def process_row(row_idx):
            return np.dot(A[row_idx], B)
        
        # Parallel processing of matrix rows
        results = pool.map(process_row, range(len(A)))
        pool.close()
        pool.join()
        
        return np.array(results)
    
    def batch_transform_vertices(self, vertices, transformation_matrix):
        # Split vertices into batches
        batch_size = len(vertices) // self.num_processors
        batches = [vertices[i:i + batch_size] for i in range(0, len(vertices), batch_size)]
        
        # Process batches in parallel
        pool = mp.Pool(self.num_processors)
        transform_func = partial(np.dot, b=transformation_matrix)
        results = pool.map(transform_func, batches)
        pool.close()
        pool.join()
        
        return np.vstack(results)

# Example usage
gpu_sim = GPUSimulator()
A = np.random.rand(1000, 1000)
B = np.random.rand(1000, 1000)

# Compare execution times
start = time.time()
result_parallel = gpu_sim.parallel_matrix_multiply(A, B)
parallel_time = time.time() - start

start = time.time()
result_numpy = np.dot(A, B)
numpy_time = time.time() - start

print(f"Parallel execution time: {parallel_time:.4f}s")
print(f"NumPy execution time: {numpy_time:.4f}s")

Slide 11: Real-time Animation Pipeline

This implementation demonstrates a complete animation pipeline for real-time 3D graphics, including interpolation between keyframes and smooth transformations for continuous motion.

import numpy as np
from dataclasses import dataclass
from typing import List
import time

@dataclass
class Keyframe:
    position: np.ndarray
    rotation: np.ndarray
    scale: np.ndarray
    time: float

class AnimationSystem:
    def __init__(self):
        self.keyframes: List[Keyframe] = []
        self.current_time = 0.0
    
    def add_keyframe(self, keyframe: Keyframe):
        self.keyframes.append(keyframe)
        # Sort keyframes by time
        self.keyframes.sort(key=lambda x: x.time)
    
    def interpolate(self, t: float) -> Keyframe:
        # Find surrounding keyframes
        next_idx = next((i for i, kf in enumerate(self.keyframes) 
                        if kf.time > t), len(self.keyframes))
        if next_idx == 0:
            return self.keyframes[0]
        if next_idx == len(self.keyframes):
            return self.keyframes[-1]
            
        prev_kf = self.keyframes[next_idx - 1]
        next_kf = self.keyframes[next_idx]
        
        # Calculate interpolation factor
        alpha = ((t - prev_kf.time) / 
                (next_kf.time - prev_kf.time))
        
        # Linear interpolation
        return Keyframe(
            position=prev_kf.position * (1-alpha) + next_kf.position * alpha,
            rotation=prev_kf.rotation * (1-alpha) + next_kf.rotation * alpha,
            scale=prev_kf.scale * (1-alpha) + next_kf.scale * alpha,
            time=t
        )
    
    def update(self, delta_time: float):
        self.current_time += delta_time
        return self.interpolate(self.current_time)

# Example usage
animation = AnimationSystem()

# Add keyframes for a simple rotation animation
animation.add_keyframe(Keyframe(
    position=np.array([0, 0, 0]),
    rotation=np.array([0, 0, 0]),
    scale=np.array([1, 1, 1]),
    time=0.0
))

animation.add_keyframe(Keyframe(
    position=np.array([0, 0, 0]),
    rotation=np.array([0, np.pi, 0]),
    scale=np.array([1, 1, 1]),
    time=2.0
))

# Simulate animation updates
for _ in range(5):
    frame = animation.update(0.5)
    print(f"Time: {animation.current_time:.1f}s, "
          f"Rotation: {frame.rotation}")

Slide 12: Optimized Triangle Rasterization

A crucial part of the graphics pipeline is converting 3D triangles into pixels. This implementation shows an efficient scanline algorithm for triangle rasterization with edge walking.

import numpy as np
from typing import Tuple, List

class Rasterizer:
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.framebuffer = np.zeros((height, width), dtype=np.float32)
        self.zbuffer = np.full((height, width), np.inf)
    
    def edge_function(self, a: np.ndarray, b: np.ndarray, 
                     c: np.ndarray) -> float:
        return ((c[0] - a[0]) * (b[1] - a[1]) - 
                (c[1] - a[1]) * (b[0] - a[0]))
    
    def rasterize_triangle(self, vertices: List[np.ndarray], 
                          color: float = 1.0):
        # Compute bounding box
        min_x = max(0, int(min(v[0] for v in vertices)))
        max_x = min(self.width - 1, int(max(v[0] for v in vertices)))
        min_y = max(0, int(min(v[1] for v in vertices)))
        max_y = min(self.height - 1, int(max(v[1] for v in vertices)))
        
        # Precompute edge functions
        area = self.edge_function(vertices[0], vertices[1], vertices[2])
        if area == 0:
            return
        
        # Rasterize
        for y in range(min_y, max_y + 1):
            for x in range(min_x, max_x + 1):
                point = np.array([x + 0.5, y + 0.5])
                
                # Compute barycentric coordinates
                w0 = self.edge_function(vertices[1], vertices[2], point)
                w1 = self.edge_function(vertices[2], vertices[0], point)
                w2 = self.edge_function(vertices[0], vertices[1], point)
                
                # Check if point is inside triangle
                if w0 >= 0 and w1 >= 0 and w2 >= 0:
                    # Normalize barycentric coordinates
                    w0 /= area
                    w1 /= area
                    w2 /= area
                    
                    # Interpolate z-value
                    z = (w0 * vertices[0][2] + 
                         w1 * vertices[1][2] + 
                         w2 * vertices[2][2])
                    
                    # Z-buffer test
                    if z < self.zbuffer[y, x]:
                        self.zbuffer[y, x] = z
                        self.framebuffer[y, x] = color

# Example usage
rasterizer = Rasterizer(800, 600)
triangle = [
    np.array([100, 100, 0]),
    np.array([700, 100, 0]),
    np.array([400, 500, 0])
]
rasterizer.rasterize_triangle(triangle)

print("Number of pixels rasterized:", 
      np.sum(rasterizer.framebuffer > 0))

Slide 13: Implementing Face Culling and Clipping

Face culling and clipping are essential optimizations in 3D graphics. This implementation demonstrates how to efficiently remove hidden faces and clip geometry against the view frustum.

import numpy as np
from dataclasses import dataclass
from typing import List, Tuple

@dataclass
class Plane:
    normal: np.ndarray
    distance: float

class FrustumClipper:
    def __init__(self):
        # Define view frustum planes (near, far, left, right, top, bottom)
        self.frustum_planes = [
            Plane(np.array([0, 0, 1]), -0.1),  # Near
            Plane(np.array([0, 0, -1]), 100),  # Far
            Plane(np.array([1, 0, 0]), 1),     # Left
            Plane(np.array([-1, 0, 0]), 1),    # Right
            Plane(np.array([0, 1, 0]), 1),     # Bottom
            Plane(np.array([0, -1, 0]), 1)     # Top
        ]
    
    def is_face_visible(self, vertices: np.ndarray) -> bool:
        # Calculate face normal using cross product
        v1 = vertices[1] - vertices[0]
        v2 = vertices[2] - vertices[0]
        normal = np.cross(v1, v2)
        
        # Check if face is facing camera (basic back-face culling)
        return np.dot(normal, vertices[0]) < 0
    
    def clip_triangle(self, vertices: np.ndarray) -> List[np.ndarray]:
        if not self.is_face_visible(vertices):
            return []
        
        current_vertices = vertices.tolist()
        
        # Clip against each frustum plane
        for plane in self.frustum_planes:
            if not current_vertices:
                return []
            
            next_vertices = []
            
            # Process each edge of the polygon
            for i in range(len(current_vertices)):
                current = np.array(current_vertices[i])
                next = np.array(current_vertices[(i + 1) % len(current_vertices)])
                
                current_inside = (np.dot(plane.normal, current) + plane.distance) > 0
                next_inside = (np.dot(plane.normal, next) + plane.distance) > 0
                
                if current_inside:
                    next_vertices.append(current)
                
                if current_inside != next_inside:
                    # Calculate intersection point
                    t = (-plane.distance - np.dot(plane.normal, current)) / \
                        np.dot(plane.normal, next - current)
                    intersection = current + t * (next - current)
                    next_vertices.append(intersection)
            
            current_vertices = next_vertices
        
        return current_vertices

# Example usage
clipper = FrustumClipper()

# Test triangle
triangle = np.array([
    [-0.5, -0.5, -1],
    [0.5, -0.5, -1],
    [0, 0.5, -1]
])

# Clip triangle
clipped_vertices = clipper.clip_triangle(triangle)
print(f"Original vertices: {len(triangle)}")
print(f"Clipped vertices: {len(clipped_vertices)}")

Slide 14: Scene Graph Implementation

A scene graph organizes 3D objects hierarchically, allowing for complex transformations and relationships between objects. This implementation shows how to build and traverse a scene graph efficiently.

from typing import Optional, List
import numpy as np

class SceneNode:
    def __init__(self, name: str):
        self.name = name
        self.local_transform = np.eye(4)
        self.world_transform = np.eye(4)
        self.parent: Optional[SceneNode] = None
        self.children: List[SceneNode] = []
        self.mesh: Optional[np.ndarray] = None
    
    def add_child(self, child: 'SceneNode'):
        child.parent = self
        self.children.append(child)
    
    def set_transform(self, translation: np.ndarray, 
                     rotation: np.ndarray, scale: np.ndarray):
        # Create transformation matrix
        T = np.eye(4)
        T[:3, 3] = translation
        
        R = np.eye(4)
        R[:3, :3] = Rotation3D.from_euler(*rotation)
        
        S = np.eye(4)
        np.fill_diagonal(S[:3, :3], scale)
        
        self.local_transform = T @ R @ S
        self.update_world_transform()
    
    def update_world_transform(self):
        if self.parent is None:
            self.world_transform = self.local_transform
        else:
            self.world_transform = (self.parent.world_transform @ 
                                  self.local_transform)
        
        # Update children
        for child in self.children:
            child.update_world_transform()
    
    def traverse(self, callback):
        callback(self)
        for child in self.children:
            child.traverse(callback)

# Example usage
def create_robot_arm():
    root = SceneNode("root")
    base = SceneNode("base")
    upper_arm = SceneNode("upper_arm")
    forearm = SceneNode("forearm")
    hand = SceneNode("hand")
    
    # Build hierarchy
    root.add_child(base)
    base.add_child(upper_arm)
    upper_arm.add_child(forearm)
    forearm.add_child(hand)
    
    # Set transforms
    base.set_transform(
        translation=np.array([0, 0, 0]),
        rotation=np.array([0, 0, 0]),
        scale=np.array([1, 1, 1])
    )
    
    upper_arm.set_transform(
        translation=np.array([0, 1, 0]),
        rotation=np.array([0, 0, np.pi/4]),
        scale=np.array([1, 2, 1])
    )
    
    return root

# Create and traverse scene graph
robot = create_robot_arm()
def print_node(node):
    print(f"Node: {node.name}")
    print(f"World transform:\n{node.world_transform}")

robot.traverse(print_node)

Slide 15: Additional Resources

Note: The URLs provided are examples and may not correspond to actual papers, as I cannot verify their existence.