Table of Contents

3d Box

This is the first demo which will use real 3d math to do projection into 2d. The code will be presented as a monolithic file but it is intended that it be split into many files such as putting Object3d into it's own file, and then moving functions like rotate_x into there (maybe using staticmethod) or into their own library class. Object data could also stand to be moved out, maybe into an objects.py sort of class, or into files like cube.obj (i.e. cube.txt) which could be read in at runtime.

The Code (Wireframe)

import pygame
import sys
import math

# Initialize Pygame
pygame.init()

# Constants
WIDTH, HEIGHT = 800, 600
FOV = 256  # Field of view
CAMERA_Z = 4  # Camera distance from the object
SQUARE_SIZE = 1  # Half the size of the square for vertex calculations
rotation_angles = [0, 0, 0]  # Rotation angles for x, y, z

# Set up the display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Box")

# Square
square_obj = [
    [  # Back face
        [-SQUARE_SIZE, -SQUARE_SIZE, 0],  # Bottom left
        [SQUARE_SIZE, -SQUARE_SIZE, 0],  # Bottom right
        [SQUARE_SIZE, SQUARE_SIZE, 0],  # Top right
        [-SQUARE_SIZE, SQUARE_SIZE, 0]  # Top left
    ]
]

# Define the vertices of a 3D cube
cube_obj = [
    [  # Back face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom left
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom right
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),  # Top right
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)  # Top left
    ],
    [  # Front face
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),  # Bottom left
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),  # Bottom right
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),  # Top right
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)  # Top left
    ],
    [  # Left face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom left
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),  # Bottom right
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),  # Top right
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)  # Top left
    ],
    [  # Right face
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom left
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),  # Bottom right
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),  # Top right
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)  # Top left
    ],
    [  # Top face
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),  # Bottom left
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),  # Bottom right
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),  # Top right
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)  # Top left
    ],
    [  # Bottom face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom left
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),  # Bottom right
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),  # Top right
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE)  # Top left
    ]
]


# Function to project 3D points to 2D
def project(point):
    x, y, z = point
    scale = FOV / (CAMERA_Z + z)
    x_proj = int(x * scale + WIDTH // 2)
    y_proj = int(-y * scale + HEIGHT // 2)
    return (x_proj, y_proj)


# Functions for rotating points
def rotate_x(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    y_rotated = y * cos_angle - z * sin_angle
    z_rotated = y * sin_angle + z * cos_angle
    return (x, y_rotated, z_rotated)


def rotate_y(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    x_rotated = z * sin_angle + x * cos_angle
    z_rotated = z * cos_angle - x * sin_angle
    return (x_rotated, y, z_rotated)


def rotate_z(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    x_rotated = x * cos_angle - y * sin_angle
    y_rotated = x * sin_angle + y * cos_angle
    return (x_rotated, y_rotated, z)


# Function to draw the cube using faces
def draw_obj(position, faces):
    face_data = []

    for index, face in enumerate(faces):
        transformed_vertices = []

        # Transform each vertex
        for vertex in face:
            # Rotate the vertex around the Z-axis
            vertex = rotate_z(vertex, rotation_angles[2])
            # Rotate the result around the Y-axis
            vertex = rotate_y(vertex, rotation_angles[1])
            # Rotate the result around the X-axis
            vertex = rotate_x(vertex, rotation_angles[0])
            # Append the transformed vertex
            transformed_vertices.append(vertex)

        projected_vertices = []

        # Project each transformed vertex to 2D
        for vertex in transformed_vertices:
            projected_vertex = project((vertex[0] + position[0], vertex[1] + position[1], vertex[2] + position[2]))
            projected_vertices.append(projected_vertex)

        pygame.draw.polygon(screen, color, projected_vertices)  # Fill the polygon with color


def draw_obj(position, faces):
    for face in faces:
        transformed_vertices = [
            rotate_x(rotate_y(rotate_z(vertex, rotation_angles[2]), rotation_angles[1]), rotation_angles[0])
            for vertex in face
        ]
        projected_vertices = [project((x + position[0], y + position[1], z + position[2])) for (x, y, z) in
                              transformed_vertices]

        # Draw the polygon face
        pygame.draw.polygon(screen, "white", projected_vertices, True)


# Main loop
def main():
    clock = pygame.time.Clock()
    position = [0, 0, 0]  # Initial position of the cube

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        keys = pygame.key.get_pressed()

        # Movement controls
        if keys[pygame.K_UP]:  # Move forward along Z
            position[2] += 0.1
        if keys[pygame.K_DOWN]:  # Move backward along Z
            position[2] -= 0.1
        if keys[pygame.K_a]:  # Move left along X
            position[0] -= 0.1
        if keys[pygame.K_d]:  # Move right along X
            position[0] += 0.1
        if keys[pygame.K_w]:  # Move up along Y
            position[1] += 0.1
        if keys[pygame.K_s]:  # Move down along Y
            position[1] -= 0.1

        # Rotation controls
        if keys[pygame.K_1]:  # Rotate around X-axis
            rotation_angles[0] += 1
        if keys[pygame.K_2]:  # Rotate around X-axis
            rotation_angles[0] -= 1
        if keys[pygame.K_3]:  # Rotate around Y-axis
            rotation_angles[1] += 1
        if keys[pygame.K_4]:  # Rotate around Y-axis
            rotation_angles[1] -= 1
        if keys[pygame.K_5]:  # Rotate around Z-axis
            rotation_angles[2] += 1
        if keys[pygame.K_6]:  # Rotate around Z-axis
            rotation_angles[2] -= 1

        # Fill the screen with black
        screen.fill((0, 0, 0))

        # Draw the cube using polygons
        draw_obj(position, cube_obj)

        # Draw the bounding box (optional)
        pygame.draw.rect(screen, "red", (0, 0, WIDTH, HEIGHT), 1)

        # Update the display
        pygame.display.flip()
        clock.tick(60)


if __name__ == "__main__":
    main()

The Code (with Simple Z-avg and fill)

import pygame
import sys
import math

# Initialize Pygame
pygame.init()

# Constants
WIDTH, HEIGHT = 800, 600
FOV = 256  # Field of view
CAMERA_Z = 4  # Camera distance from the object
SQUARE_SIZE = 1  # Half the size of the square for vertex calculations
rotation_angles = [0, 0, 0]  # Rotation angles for x, y, z

# Set up the display
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("3D Cube with Polygons")

# Define colors for each face
face_colors = [
    (255, 0, 0),  # Red for Back face
    (0, 255, 0),  # Green for Front face
    (0, 0, 255),  # Blue for Left face
    (255, 255, 0),  # Yellow for Right face
    (255, 0, 255),  # Magenta for Top face
    (0, 255, 255)  # Cyan for Bottom face
]


# Object3D class to store vertices and position
class Object3D:
    def __init__(self, vertices, position=(0, 0, 0)):
        self.vertices = vertices
        self.position = list(position)
        self.rotation = [0, 0, 0]  # Rotation angles for x, y, z

    def rotate(self, angle_x, angle_y, angle_z):
        self.rotation[0] += angle_x
        self.rotation[1] += angle_y
        self.rotation[2] += angle_z

    def move(self, dx, dy, dz):
        self.position[0] += dx
        self.position[1] += dy
        self.position[2] += dz

    def get_transformed_vertices(self):
        transformed_faces = []
        for face in self.vertices:
            transformed_face = [
                rotate_x(rotate_y(rotate_z(vertex, self.rotation[2]), self.rotation[1]), self.rotation[0])
                for vertex in face
            ]
            transformed_faces.append(transformed_face)
        return transformed_faces


# Define the vertices of a 3D cube
cube_vertices = [
    [  # Back face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)
    ],
    [  # Front face
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)
    ],
    [  # Left face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)
    ],
    [  # Right face
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE)
    ],
    [  # Top face
        (-SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE),
        (-SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)
    ],
    [  # Bottom face
        (-SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, -SQUARE_SIZE, -SQUARE_SIZE),
        (SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE),
        (-SQUARE_SIZE, -SQUARE_SIZE, SQUARE_SIZE)
    ]
]

# Function to project 3D points to 2D
def project(point):
    x, y, z = point
    scale = FOV / (CAMERA_Z + z)
    x_proj = int(x * scale + WIDTH // 2)
    y_proj = int(-y * scale + HEIGHT // 2)
    return (x_proj, y_proj)


# Functions for rotating points
def rotate_x(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    y_rotated = y * cos_angle - z * sin_angle
    z_rotated = y * sin_angle + z * cos_angle
    return (x, y_rotated, z_rotated)


def rotate_y(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    x_rotated = z * sin_angle + x * cos_angle
    z_rotated = z * cos_angle - x * sin_angle
    return (x_rotated, y, z_rotated)


def rotate_z(point, angle):
    rad = math.radians(angle)
    cos_angle = math.cos(rad)
    sin_angle = math.sin(rad)
    x, y, z = point
    x_rotated = x * cos_angle - y * sin_angle
    y_rotated = x * sin_angle + y * cos_angle
    return (x_rotated, y_rotated, z)


# Function to draw the cube using faces
def draw_obj(obj):
    face_data = []

    transformed_faces = obj.get_transformed_vertices()

    for index, face in enumerate(transformed_faces):
        projected_vertices = [project((x + obj.position[0], y + obj.position[1], z + obj.position[2])) for (x, y, z) in
                              face]

        # Calculate average Z value for sorting
        avg_z = sum(vertex[2] for vertex in face) / len(face)
        face_data.append((avg_z, projected_vertices, face_colors[index]))

    # Sort faces by average Z value (back to front)
    face_data.sort(key=lambda x: x[0], reverse=True)

    # Draw each face in sorted order
    for _, projected_vertices, color in face_data:
        pygame.draw.polygon(screen, color, projected_vertices, False)


# Main loop
def main():
    clock = pygame.time.Clock()

    # Create cube object
    cube = Object3D(cube_vertices)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        keys = pygame.key.get_pressed()

        # Movement controls
        if keys[pygame.K_w]:  # Move forward along Z
            cube.move(0, 0, 0.1)
        if keys[pygame.K_s]:  # Move backward along Z
            cube.move(0, 0, -0.1)
        if keys[pygame.K_a]:  # Move left along X
            cube.move(-0.1, 0, 0)
        if keys[pygame.K_d]:  # Move right along X
            cube.move(0.1, 0, 0)
        if keys[pygame.K_UP]:  # Move up along Y
            cube.move(0, 0.1, 0)
        if keys[pygame.K_DOWN]:  # Move down along Y
            cube.move(0, -0.1, 0)

        # Rotation controls
        if keys[pygame.K_1]:  # Rotate around X-axis
            cube.rotate(1, 0, 0)
        if keys[pygame.K_2]:  # Rotate around X-axis
            cube.rotate(-1, 0, 0)
        if keys[pygame.K_3]:  # Rotate around Y-axis
            cube.rotate(0, 1, 0)
        if keys[pygame.K_4]:  # Rotate around Y-axis
            cube.rotate(0, -1, 0)
        if keys[pygame.K_5]:  # Rotate around Z-axis
            cube.rotate(0, 0, 1)
        if keys[pygame.K_6]:  # Rotate around Z-axis
            cube.rotate(0, 0, -1)

        # Fill the screen with black
        screen.fill((0, 0, 0))

        # Draw the cube
        draw_obj(cube)

        # Draw the bounding box (optional)
        pygame.draw.rect(screen, (255, 0, 0), (WIDTH // 2 - 400, HEIGHT // 2 - 300, 800, 600), 1)

        # Update the display
        pygame.display.flip()
        clock.tick(60)


if __name__ == "__main__":
    main()

Adding Scaling

class Object3D:
    def __init__(self, vertices, position=(0, 0, 0)):
        self.vertices = vertices
        self.position = list(position)
        self.rotation = [0, 0, 0]  # Rotation angles for x, y, z
        self.scale_factors = [1, 1, 1]  # Scaling factors for x, y, z

    def rotate(self, angle_x, angle_y, angle_z):
        self.rotation[0] += angle_x
        self.rotation[1] += angle_y
        self.rotation[2] += angle_z

    def move(self, dx, dy, dz):
        self.position[0] += dx
        self.position[1] += dy
        self.position[2] += dz

    def scale(self, scale_x, scale_y, scale_z):
        """Set the scaling factors for the object."""
        self.scale_factors = [scale_x, scale_y, scale_z]

    def get_transformed_vertices(self):
        transformed_faces = []
        for face in self.vertices:
            transformed_face = [
                rotate_x(
                    rotate_y(
                        rotate_z(
                            (x * self.scale_factors[0], y * self.scale_factors[1], z * self.scale_factors[2]),
                            self.rotation[2]),
                        self.rotation[1]),
                    self.rotation[0])
                for (x, y, z) in face
            ]
            transformed_faces.append(transformed_face)
        return transformed_faces