cube.c icon indicating copy to clipboard operation
cube.c copied to clipboard

Add comments

Open camdenorrb opened this issue 2 years ago • 9 comments

Me bad at math, please explain some of the variables and link to equations, thxz :cat:

camdenorrb avatar Sep 08 '22 10:09 camdenorrb

Example: why does xp = (int)(width / 2 + horizontalOffset + K1 * ooz * x * 2); work??

camdenorrb avatar Sep 08 '22 10:09 camdenorrb

hi, newbie programmer but pro math person here! I'll explain everything as best as I can, keep in mind I am new to C-lang 😿.

CodeByAidan avatar Oct 07 '22 23:10 CodeByAidan

Ok so we are going to be discussing really difficult areas topics on Linear Algebra, Calculus, and rotations in Euclidean space. 😃

Have you lost me? Yes? Good! :D

Projections

Ok so in the program it is using a projection, you have a 3D shape, and you have all the vertices right, and you project it onto a 2D plane.

Image of an example of perspective projection (you have a 3D shape, and then you see the black dot? you have the point there, it projects to the blue box... image

Ok so now it's time for linear algebra to be applied:

What is a projection matrix and why is it being used so much?

In Linear Algebra, a projection is a linear transformation. It maps the point ( x, y, z ) in three-dimensional space to the point ( x, y, 0 ) is an orthogonal projection onto the xy-plane. ( 3D -> 2D )

image

You can find how this works at this website: http://matrixmultiplication.xyz/

How do we make a cube?

image We will be getting all the vertices and they are being projected via the projection matrix.

Anyways if you would like to see a following translation of this code with some better organization, and output that is clean, all in python (pygame):

import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
import numpy as np
from math import *

WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLACK = (0, 0, 0)

WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))

scale = 100

circle_pos = [WIDTH/2, HEIGHT/2]  # x, y

angle = 0

points = [np.matrix([-1, -1, 1])]

points.append(np.matrix([1, -1, 1]))
points.append(np.matrix([1,  1, 1]))
points.append(np.matrix([-1, 1, 1]))
points.append(np.matrix([-1, -1, -1]))
points.append(np.matrix([1, -1, -1]))
points.append(np.matrix([1, 1, -1]))
points.append(np.matrix([-1, 1, -1]))


projection_matrix = np.matrix([
    [1, 0, 0],
    [0, 1, 0]
])  # type: ignore


projected_points = [
    [n, n] for n in range(len(points))
]


def connect_points(i, j, points):
    pygame.draw.line(
        screen, BLACK, (points[i][0], points[i][1]), (points[j][0], points[j][1]))


clock = pygame.time.Clock()
while True:

    clock.tick(60)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            pygame.quit()
            exit()

    # update stuff

    rotation_z = np.matrix([
        [cos(angle), -sin(angle), 0],
        [sin(angle), cos(angle), 0],
        [0, 0, 1],
    ])  # type: ignore

    rotation_y = np.matrix([
        [cos(angle), 0, sin(angle)],
        [0, 1, 0],
        [-sin(angle), 0, cos(angle)],
    ])  # type: ignore

    rotation_x = np.matrix([
        [1, 0, 0],
        [0, cos(angle), -sin(angle)],
        [0, sin(angle), cos(angle)],
    ])  # type: ignore
    angle += 0.01

    screen.fill(WHITE)
    for i, point in enumerate(points):
        rotated2d = np.dot(rotation_z, point.reshape((3, 1)))
        rotated2d = np.dot(rotation_y, rotated2d)
        rotated2d = np.dot(rotation_x, rotated2d)

        projected2d = np.dot(projection_matrix, rotated2d)

        x = int(projected2d[0][0] * scale) + circle_pos[0]
        y = int(projected2d[1][0] * scale) + circle_pos[1]

        projected_points[i] = [x, y]  # type: ignore
        pygame.draw.circle(screen, RED, (x, y), 5)
    for p in range(4):
        connect_points(p, (p+1) % 4, projected_points)
        connect_points(p+4, ((p+1) % 4) + 4, projected_points)
        connect_points(p, (p+4), projected_points)

    pygame.display.update()

CodeByAidan avatar Oct 08 '22 00:10 CodeByAidan

Woah @livxy thank you so much 🔥

camdenorrb avatar Oct 08 '22 00:10 camdenorrb

thx bro

cldhfleks2 avatar Nov 07 '22 02:11 cldhfleks2

gotchu <3

CodeByAidan avatar Nov 07 '22 14:11 CodeByAidan

Ok so we are going to be discussing really difficult areas topics on Linear Algebra, Calculus, and rotations in Euclidean space. 😃

Have you lost me? Yes? Good! :D

Projections

Ok so in the program it is using a projection, you have a 3D shape, and you have all the vertices right, and you project it onto a 2D plane.

Image of an example of perspective projection (you have a 3D shape, and then you see the black dot? you have the point there, it projects to the blue box... image

Ok so now it's time for linear algebra to be applied:

What is a projection matrix and why is it being used so much?

In Linear Algebra, a projection is a linear transformation. It maps the point ( x, y, z ) in three-dimensional space to the point ( x, y, 0 ) is an orthogonal projection onto the xy-plane. ( 3D -> 2D )

image

You can find how this works at this website: http://matrixmultiplication.xyz/

How do we make a cube?

image We will be getting all the vertices and they are being projected via the projection matrix.

Anyways if you would like to see a following translation of this code with some better organization, and output that is clean, all in python (pygame):

import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
import numpy as np
from math import *

WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLACK = (0, 0, 0)

WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))

scale = 100

circle_pos = [WIDTH/2, HEIGHT/2]  # x, y

angle = 0

points = [np.matrix([-1, -1, 1])]

points.append(np.matrix([1, -1, 1]))
points.append(np.matrix([1,  1, 1]))
points.append(np.matrix([-1, 1, 1]))
points.append(np.matrix([-1, -1, -1]))
points.append(np.matrix([1, -1, -1]))
points.append(np.matrix([1, 1, -1]))
points.append(np.matrix([-1, 1, -1]))


projection_matrix = np.matrix([
    [1, 0, 0],
    [0, 1, 0]
])  # type: ignore


projected_points = [
    [n, n] for n in range(len(points))
]


def connect_points(i, j, points):
    pygame.draw.line(
        screen, BLACK, (points[i][0], points[i][1]), (points[j][0], points[j][1]))


clock = pygame.time.Clock()
while True:

    clock.tick(60)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            pygame.quit()
            exit()

    # update stuff

    rotation_z = np.matrix([
        [cos(angle), -sin(angle), 0],
        [sin(angle), cos(angle), 0],
        [0, 0, 1],
    ])  # type: ignore

    rotation_y = np.matrix([
        [cos(angle), 0, sin(angle)],
        [0, 1, 0],
        [-sin(angle), 0, cos(angle)],
    ])  # type: ignore

    rotation_x = np.matrix([
        [1, 0, 0],
        [0, cos(angle), -sin(angle)],
        [0, sin(angle), cos(angle)],
    ])  # type: ignore
    angle += 0.01

    screen.fill(WHITE)
    for i, point in enumerate(points):
        rotated2d = np.dot(rotation_z, point.reshape((3, 1)))
        rotated2d = np.dot(rotation_y, rotated2d)
        rotated2d = np.dot(rotation_x, rotated2d)

        projected2d = np.dot(projection_matrix, rotated2d)

        x = int(projected2d[0][0] * scale) + circle_pos[0]
        y = int(projected2d[1][0] * scale) + circle_pos[1]

        projected_points[i] = [x, y]  # type: ignore
        pygame.draw.circle(screen, RED, (x, y), 5)
    for p in range(4):
        connect_points(p, (p+1) % 4, projected_points)
        connect_points(p+4, ((p+1) % 4) + 4, projected_points)
        connect_points(p, (p+4), projected_points)

    pygame.display.update()

Hello @CodeByAidan,

I am not able to understand the maths used here, could you please help me?(I have just finished 10th standard from an ICSE school)

Basically I wanted to print my name in 3D on the terminal... But for getting the initial logic, I settled for a sphere and I am still confused on what to do.

Regards, Tree-t

tree-t avatar Jun 17 '24 08:06 tree-t

Hey @tree-t! Sorry for my poor explanation, and honestly was pretty cringe, looking back at it. The code I currently have which surprisingly, still works, is a bit different from what you want. I completely understand your goal. So, I'll try to explain everything from scratch, with a bit of a reformatted revision of my code:

Click to expand
from math import cos, sin
from typing import List, Tuple

import numpy as np
import pygame

# Define colors
WHITE: Tuple[int, int, int] = (255, 255, 255)
RED: Tuple[int, int, int] = (255, 0, 0)
BLACK: Tuple[int, int, int] = (0, 0, 0)

# Define screen size
WIDTH, HEIGHT = 800, 600
screen: pygame.Surface = pygame.display.set_mode((WIDTH, HEIGHT))

# Define scale and circle position
scale = 100
circle_pos: List[float] = [WIDTH / 2, HEIGHT / 2]  # x, y

# Define initial angle
angle = 0

# Define points for 3D cube
points: List[np.matrix] = [
    np.matrix([-1, -1, 1]),
    np.matrix([1, -1, 1]),
    np.matrix([1, 1, 1]),
    np.matrix([-1, 1, 1]),
    np.matrix([-1, -1, -1]),
    np.matrix([1, -1, -1]),
    np.matrix([1, 1, -1]),
    np.matrix([-1, 1, -1]),
]

# Define projection matrix
projection_matrix = np.matrix([[1, 0, 0], [0, 1, 0]])

# Initialize projected points
projected_points: List[List[int]] = [[n, n] for n in range(len(points))]


# Function to connect points
def connect_points(i, j, points) -> None:
    pygame.draw.line(
        screen, BLACK, (points[i][0], points[i][1]), (points[j][0], points[j][1])
    )


# Initialize clock
clock = pygame.time.Clock()

# Main loop
while True:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT or (
            event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
        ):
            pygame.quit()
            exit()

    # Update angle
    angle += 0.01

    # Define rotation matrices
    cos_angle: float = cos(angle)
    sin_angle: float = sin(angle)
    rotation_z = np.matrix(
        [[cos_angle, -sin_angle, 0], [sin_angle, cos_angle, 0], [0, 0, 1]]
    )

    rotation_y = np.matrix(
        [[cos_angle, 0, sin_angle], [0, 1, 0], [-sin_angle, 0, cos_angle]]
    )

    rotation_x = np.matrix(
        [[1, 0, 0], [0, cos_angle, -sin_angle], [0, sin_angle, cos_angle]]
    )

    # Clear screen
    screen.fill(WHITE)

    # Draw points and lines
    for i, point in enumerate(points):
        # Rotate points in 3D space
        rotated2d = rotation_z @ point.reshape((3, 1))
        rotated2d = rotation_y @ rotated2d
        rotated2d = rotation_x @ rotated2d

        # Project points from 3D to 2D
        projected2d = projection_matrix @ rotated2d

        # Scale and translate points
        x: float = int(projected2d[0][0] * scale) + circle_pos[0]
        y: float = int(projected2d[1][0] * scale) + circle_pos[1]

        # Update projected points
        projected_points[i] = [x, y]

        # Draw points
        pygame.draw.circle(screen, RED, (x, y), 5)

    # Connect points with lines
    for p in range(4):
        connect_points(p, (p + 1) % 4, projected_points)
        connect_points(p + 4, ((p + 1) % 4) + 4, projected_points)
        connect_points(p, (p + 4), projected_points)

    # Update display
    pygame.display.update()

    # Limit frame rate
    clock.tick(60)

Simple Relatable Explanation

This code is like a magic trick where we take a cube (like a dice), spin it around in a 3D space (like tossing it in the air), and then show it on your computer screen (which is a 2D space, like a piece of paper).

The cube is defined by 8 points, like the 8 corners of a room. We use some math (trigonometry) to spin these points around in 3D space. This is done using something called rotation matrices. If you've ever played with spinning tops, it's a bit like that, but in three different directions.

After we've spun the cube, we need to show it on the screen. But the screen is flat (2D), while our cube is 3D. So, we need to convert our 3D points to 2D. This is done using a projection matrix. It's like casting a shadow of the cube onto the screen.

Finally, we adjust the size and position of the cube on the screen (scaling and translation), and then draw it. We also connect the points with lines to form the edges of the cube.

Explanation of the Code

This code is for a 3D cube rotation simulation using Pygame and Numpy. It involves some mathematical concepts like matrices, vectors, and trigonometry. Let's break it down:

Importing Libraries

from math import cos, sin
from typing import List, Tuple
import numpy as np
import pygame

The code starts by importing necessary libraries. math for cosine and sine functions, typing for type annotations, numpy for matrix operations, and pygame for creating the graphical interface.

Defining Constants

WHITE: Tuple[int, int, int] = (255, 255, 255)
RED: Tuple[int, int, int] = (255, 0, 0)
BLACK: Tuple[int, int, int] = (0, 0, 0)
WIDTH, HEIGHT = 800, 600
screen: pygame.Surface = pygame.display.set_mode((WIDTH, HEIGHT))
scale = 100
circle_pos: List[float] = [WIDTH / 2, HEIGHT / 2]
angle = 0

Here, the code defines some constants like colors, screen size, scale factor, initial position of the cube, and initial rotation angle.

Defining 3D Points

points: List[np.matrix] = [
    np.matrix([-1, -1, 1]),
    np.matrix([1, -1, 1]),
    np.matrix([1, 1, 1]),
    np.matrix([-1, 1, 1]),
    np.matrix([-1, -1, -1]),
    np.matrix([1, -1, -1]),
    np.matrix([1, 1, -1]),
    np.matrix([-1, 1, -1]),
]

This part defines the 8 points of a cube in a 3D space.

Projection Matrix

projection_matrix = np.matrix([[1, 0, 0], [0, 1, 0]])

The projection matrix is used to convert 3D points to 2D points. This is necessary because we can only display 2D graphics on the screen.

Main Loop

while True:
    ...

This is the main loop where the cube is drawn and updated on the screen. It handles user events, updates the rotation angle, calculates the rotation matrices, clears the screen, draws the points and lines, and updates the display.

Rotation Matrices

cos_angle: float = cos(angle)
sin_angle: float = sin(angle)
rotation_z = np.matrix(
    [[cos_angle, -sin_angle, 0], [sin_angle, cos_angle, 0], [0, 0, 1]]
)
rotation_y = np.matrix(
    [[cos_angle, 0, sin_angle], [0, 1, 0], [-sin_angle, 0, cos_angle]]
)
rotation_x = np.matrix(
    [[1, 0, 0], [0, cos_angle, -sin_angle], [0, sin_angle, cos_angle]]
)

These are the rotation matrices for rotating the cube around the x, y, and z axes. The rotation is done by multiplying the points of the cube with these matrices.

Drawing Points and Lines

for i, point in enumerate(points):
    ...

This loop goes through each point of the cube, rotates it, projects it to 2D, scales and translates it, and then draws it on the screen.

for p in range(4):
    connect_points(p, (p + 1) % 4, projected_points)
    connect_points(p + 4, ((p + 1) % 4) + 4, projected_points)
    connect_points(p, (p + 4), projected_points)

This loop connects the points with lines to form the edges of the cube.

Updating the Display

pygame.display.update()
clock.tick(60)

Finally, the display is updated, and the frame rate is limited to 60 frames per second.

Explanation (in math)

The math behind the 3D rotation involves linear algebra and trigonometry. Here's a brief explanation:

  1. 3D Points: The cube is represented by 8 points in a 3D space. Each point is a vector with three components (x, y, z). Learn more about 3D points

  2. Rotation Matrices: The rotation of points in 3D space is achieved by multiplying the point vectors with rotation matrices. The rotation matrices for the x, y, and z axes are as follows:

    • Rotation around the x-axis:

    Learn more about rotation matrices

    • Rotation around the y-axis:

    • Rotation around the z-axis:

    Here, θ is the rotation angle.

  3. Projection: The 3D points are projected onto a 2D plane (the screen) using a projection matrix. The simplest form of projection is orthographic projection, which ignores the z-component of the points. Learn more about projection. The projection matrix for this is:

  1. Scaling and Translation: The projected points are then scaled and translated to fit on the screen. The scaling is done by multiplying the points with a scale factor, and the translation is done by adding a translation vector to the points. Learn more about scaling and translation

  2. Drawing: Finally, the points are drawn on the screen, and lines are drawn between them to form the edges of the cube. Learn more about drawing in Pygame

Conversion from cube.c to cube.py through terminal

I did convert the C code, so it works in the terminal like so:

import math
import os

# Define the initial rotation angles
A, B, C = 0, 0, 0

# Define the width of the cube
cubeWidth = 20

# Define the width and height of the terminal window
width, height = 160, 44

# Initialize the z-buffer and the display buffer
zBuffer = [0] * (width * height)
buffer = [" "] * (width * height)

# Define the ASCII character to use for the background
backgroundASCIICode = "."

# Define the distance from the camera to the cube
distanceFromCam = 100

# Define the horizontal offset of the cube
horizontalOffset = 0

# Define a constant used in the projection calculations
K1 = 40

# Define the speed at which the cube rotates
incrementSpeed = 0.9


def calculateX(i: int, j: int, k: int) -> float:
    """
    Calculate the X coordinate in 3D space.

    Parameters:
        i (int): The X coordinate of the point.
        j (int): The Y coordinate of the point.
        k (int): The Z coordinate of the point.

    Returns:
        float: The calculated X coordinate.
    """
    return (
        j * math.sin(A) * math.sin(B) * math.cos(C)
        - k * math.cos(A) * math.sin(B) * math.cos(C)
        + j * math.cos(A) * math.sin(C)
        + k * math.sin(A) * math.sin(C)
        + i * math.cos(B) * math.cos(C)
    )


def calculateY(i: int, j: int, k: int) -> float:
    """
    Calculate the Y coordinate in 3D space.

    Parameters:
        i (int): The X coordinate of the point.
        j (int): The Y coordinate of the point.
        k (int): The Z coordinate of the point.

    Returns:
        float: The calculated Y coordinate.
    """
    return (
        j * math.cos(A) * math.cos(C)
        + k * math.sin(A) * math.cos(C)
        - j * math.sin(A) * math.sin(B) * math.sin(C)
        + k * math.cos(A) * math.sin(B) * math.sin(C)
        - i * math.cos(B) * math.sin(C)
    )


def calculateZ(i: int, j: int, k: int) -> float:
    """
    Calculate the Z coordinate in 3D space.

    Parameters:
        i (int): The X coordinate of the point.
        j (int): The Y coordinate of the point.
        k (int): The Z coordinate of the point.

    Returns:
        float: The calculated Z coordinate.
    """
    return (
        k * math.cos(A) * math.cos(B) - j * math.sin(A) * math.cos(B) + i * math.sin(B)
    )


def calculateForSurface(cubeX: int, cubeY: int, cubeZ: int, ch: str) -> None:
    """
    Calculate the coordinates for a surface of a cube and update the buffer.

    Parameters:
        cubeX (int): The X coordinate of the cube.
        cubeY (int): The Y coordinate of the cube.
        cubeZ (int): The Z coordinate of the cube.
        ch (str): The character to be displayed at the calculated position.
    """
    global buffer, zBuffer, width, height
    x: float = calculateX(cubeX, cubeY, cubeZ)
    y: float = calculateY(cubeX, cubeY, cubeZ)
    z: float = calculateZ(cubeX, cubeY, cubeZ) + distanceFromCam

    ooz: float = 1 / z

    xp = int(width / 2 + horizontalOffset + K1 * ooz * x * 2)
    yp = int(height / 2 + K1 * ooz * y)

    idx: int = xp + yp * width
    if 0 <= idx < width * height and ooz > zBuffer[idx]:
        zBuffer[idx] = ooz
        buffer[idx] = ch


def render() -> None:
    """
    Render the buffer to the console.
    """
    global buffer, zBuffer
    os.system("cls" if os.name == "nt" else "clear")
    for k in range(width * height):
        if k % width:
            print(buffer[k], end="")
        else:
            print()


while True:
    # Initialize the display buffer and the z-buffer
    buffer: list[str] = [" "] * (width * height)
    zBuffer: list[int] = [0] * (width * height)

    # Define the width of the cube and its horizontal offset
    cubeWidth = 20
    horizontalOffset: int = -2 * cubeWidth

    # For each point on the cube's surface, calculate its 2D coordinates and update the display buffer and the z-buffer
    cubeX = -cubeWidth
    while cubeX < cubeWidth:
        cubeY = -cubeWidth
        while cubeY < cubeWidth:
            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")
            calculateForSurface(cubeWidth, cubeY, cubeX, "$")
            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")
            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")
            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")
            calculateForSurface(cubeX, cubeWidth, cubeY, "+")
            cubeY += incrementSpeed
        cubeX += incrementSpeed

    # Repeat the above steps for a smaller cube
    cubeWidth = 10
    horizontalOffset = 1 * cubeWidth

    cubeX = -cubeWidth
    while cubeX < cubeWidth:
        cubeY = -cubeWidth
        while cubeY < cubeWidth:
            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")
            calculateForSurface(cubeWidth, cubeY, cubeX, "$")
            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")
            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")
            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")
            calculateForSurface(cubeX, cubeWidth, cubeY, "+")
            cubeY += incrementSpeed
        cubeX += incrementSpeed

    # Repeat the above steps for an even smaller cube
    cubeWidth = 5
    horizontalOffset = 8 * cubeWidth

    cubeX = -cubeWidth
    while cubeX < cubeWidth:
        cubeY = -cubeWidth
        while cubeY < cubeWidth:
            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")
            calculateForSurface(cubeWidth, cubeY, cubeX, "$")
            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")
            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")
            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")
            calculateForSurface(cubeX, cubeWidth, cubeY, "+")
            cubeY += incrementSpeed
        cubeX += incrementSpeed

    # Render the current state of the display buffer to the terminal
    render()

    # Increment the rotation angles, causing the cubes to rotate
    A += 0.05
    B += 0.05
    C += 0.01

This should at least give you a good idea on where to start, based on the logic.

Please ask me anything, here to help!

CodeByAidan avatar Jun 17 '24 15:06 CodeByAidan

Hey @tree-t! Sorry for my poor explanation, and honestly was pretty cringe, looking back at it. The code I currently have which surprisingly, still works, is a bit different from what you want. I completely understand your goal. So, I'll try to explain everything from scratch, with a bit of a reformatted revision of my code:

Click to expand

from math import cos, sin

from typing import List, Tuple



import numpy as np

import pygame



# Define colors

WHITE: Tuple[int, int, int] = (255, 255, 255)

RED: Tuple[int, int, int] = (255, 0, 0)

BLACK: Tuple[int, int, int] = (0, 0, 0)



# Define screen size

WIDTH, HEIGHT = 800, 600

screen: pygame.Surface = pygame.display.set_mode((WIDTH, HEIGHT))



# Define scale and circle position

scale = 100

circle_pos: List[float] = [WIDTH / 2, HEIGHT / 2]  # x, y



# Define initial angle

angle = 0



# Define points for 3D cube

points: List[np.matrix] = [

    np.matrix([-1, -1, 1]),

    np.matrix([1, -1, 1]),

    np.matrix([1, 1, 1]),

    np.matrix([-1, 1, 1]),

    np.matrix([-1, -1, -1]),

    np.matrix([1, -1, -1]),

    np.matrix([1, 1, -1]),

    np.matrix([-1, 1, -1]),

]



# Define projection matrix

projection_matrix = np.matrix([[1, 0, 0], [0, 1, 0]])



# Initialize projected points

projected_points: List[List[int]] = [[n, n] for n in range(len(points))]





# Function to connect points

def connect_points(i, j, points) -> None:

    pygame.draw.line(

        screen, BLACK, (points[i][0], points[i][1]), (points[j][0], points[j][1])

    )





# Initialize clock

clock = pygame.time.Clock()



# Main loop

while True:

    # Handle events

    for event in pygame.event.get():

        if event.type == pygame.QUIT or (

            event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE

        ):

            pygame.quit()

            exit()



    # Update angle

    angle += 0.01



    # Define rotation matrices

    cos_angle: float = cos(angle)

    sin_angle: float = sin(angle)

    rotation_z = np.matrix(

        [[cos_angle, -sin_angle, 0], [sin_angle, cos_angle, 0], [0, 0, 1]]

    )



    rotation_y = np.matrix(

        [[cos_angle, 0, sin_angle], [0, 1, 0], [-sin_angle, 0, cos_angle]]

    )



    rotation_x = np.matrix(

        [[1, 0, 0], [0, cos_angle, -sin_angle], [0, sin_angle, cos_angle]]

    )



    # Clear screen

    screen.fill(WHITE)



    # Draw points and lines

    for i, point in enumerate(points):

        # Rotate points in 3D space

        rotated2d = rotation_z @ point.reshape((3, 1))

        rotated2d = rotation_y @ rotated2d

        rotated2d = rotation_x @ rotated2d



        # Project points from 3D to 2D

        projected2d = projection_matrix @ rotated2d



        # Scale and translate points

        x: float = int(projected2d[0][0] * scale) + circle_pos[0]

        y: float = int(projected2d[1][0] * scale) + circle_pos[1]



        # Update projected points

        projected_points[i] = [x, y]



        # Draw points

        pygame.draw.circle(screen, RED, (x, y), 5)



    # Connect points with lines

    for p in range(4):

        connect_points(p, (p + 1) % 4, projected_points)

        connect_points(p + 4, ((p + 1) % 4) + 4, projected_points)

        connect_points(p, (p + 4), projected_points)



    # Update display

    pygame.display.update()



    # Limit frame rate

    clock.tick(60)

Simple Relatable Explanation

This code is like a magic trick where we take a cube (like a dice), spin it around in a 3D space (like tossing it in the air), and then show it on your computer screen (which is a 2D space, like a piece of paper).

The cube is defined by 8 points, like the 8 corners of a room. We use some math (trigonometry) to spin these points around in 3D space. This is done using something called rotation matrices. If you've ever played with spinning tops, it's a bit like that, but in three different directions.

After we've spun the cube, we need to show it on the screen. But the screen is flat (2D), while our cube is 3D. So, we need to convert our 3D points to 2D. This is done using a projection matrix. It's like casting a shadow of the cube onto the screen.

Finally, we adjust the size and position of the cube on the screen (scaling and translation), and then draw it. We also connect the points with lines to form the edges of the cube.

Explanation of the Code

This code is for a 3D cube rotation simulation using Pygame and Numpy. It involves some mathematical concepts like matrices, vectors, and trigonometry. Let's break it down:

Importing Libraries


from math import cos, sin

from typing import List, Tuple

import numpy as np

import pygame

The code starts by importing necessary libraries. math for cosine and sine functions, typing for type annotations, numpy for matrix operations, and pygame for creating the graphical interface.

Defining Constants


WHITE: Tuple[int, int, int] = (255, 255, 255)

RED: Tuple[int, int, int] = (255, 0, 0)

BLACK: Tuple[int, int, int] = (0, 0, 0)

WIDTH, HEIGHT = 800, 600

screen: pygame.Surface = pygame.display.set_mode((WIDTH, HEIGHT))

scale = 100

circle_pos: List[float] = [WIDTH / 2, HEIGHT / 2]

angle = 0

Here, the code defines some constants like colors, screen size, scale factor, initial position of the cube, and initial rotation angle.

Defining 3D Points


points: List[np.matrix] = [

    np.matrix([-1, -1, 1]),

    np.matrix([1, -1, 1]),

    np.matrix([1, 1, 1]),

    np.matrix([-1, 1, 1]),

    np.matrix([-1, -1, -1]),

    np.matrix([1, -1, -1]),

    np.matrix([1, 1, -1]),

    np.matrix([-1, 1, -1]),

]

This part defines the 8 points of a cube in a 3D space.

Projection Matrix


projection_matrix = np.matrix([[1, 0, 0], [0, 1, 0]])

The projection matrix is used to convert 3D points to 2D points. This is necessary because we can only display 2D graphics on the screen.

Main Loop


while True:

    ...

This is the main loop where the cube is drawn and updated on the screen. It handles user events, updates the rotation angle, calculates the rotation matrices, clears the screen, draws the points and lines, and updates the display.

Rotation Matrices


cos_angle: float = cos(angle)

sin_angle: float = sin(angle)

rotation_z = np.matrix(

    [[cos_angle, -sin_angle, 0], [sin_angle, cos_angle, 0], [0, 0, 1]]

)

rotation_y = np.matrix(

    [[cos_angle, 0, sin_angle], [0, 1, 0], [-sin_angle, 0, cos_angle]]

)

rotation_x = np.matrix(

    [[1, 0, 0], [0, cos_angle, -sin_angle], [0, sin_angle, cos_angle]]

)

These are the rotation matrices for rotating the cube around the x, y, and z axes. The rotation is done by multiplying the points of the cube with these matrices.

Drawing Points and Lines


for i, point in enumerate(points):

    ...

This loop goes through each point of the cube, rotates it, projects it to 2D, scales and translates it, and then draws it on the screen.


for p in range(4):

    connect_points(p, (p + 1) % 4, projected_points)

    connect_points(p + 4, ((p + 1) % 4) + 4, projected_points)

    connect_points(p, (p + 4), projected_points)

This loop connects the points with lines to form the edges of the cube.

Updating the Display


pygame.display.update()

clock.tick(60)

Finally, the display is updated, and the frame rate is limited to 60 frames per second.

Explanation (in math)

The math behind the 3D rotation involves linear algebra and trigonometry. Here's a brief explanation:

  1. 3D Points: The cube is represented by 8 points in a 3D space. Each point is a vector with three components (x, y, z). Learn more about 3D points

  2. Rotation Matrices: The rotation of points in 3D space is achieved by multiplying the point vectors with rotation matrices. The rotation matrices for the x, y, and z axes are as follows:

    • Rotation around the x-axis:

    Learn more about rotation matrices

    • Rotation around the y-axis:

    • Rotation around the z-axis:

    Here, θ is the rotation angle.

  3. Projection: The 3D points are projected onto a 2D plane (the screen) using a projection matrix. The simplest form of projection is orthographic projection, which ignores the z-component of the points. Learn more about projection. The projection matrix for this is:

  1. Scaling and Translation: The projected points are then scaled and translated to fit on the screen. The scaling is done by multiplying the points with a scale factor, and the translation is done by adding a translation vector to the points. Learn more about scaling and translation

  2. Drawing: Finally, the points are drawn on the screen, and lines are drawn between them to form the edges of the cube. Learn more about drawing in Pygame

Conversion from cube.c to cube.py through terminal

I did convert the C code, so it works in the terminal like so:


import math

import os



# Define the initial rotation angles

A, B, C = 0, 0, 0



# Define the width of the cube

cubeWidth = 20



# Define the width and height of the terminal window

width, height = 160, 44



# Initialize the z-buffer and the display buffer

zBuffer = [0] * (width * height)

buffer = [" "] * (width * height)



# Define the ASCII character to use for the background

backgroundASCIICode = "."



# Define the distance from the camera to the cube

distanceFromCam = 100



# Define the horizontal offset of the cube

horizontalOffset = 0



# Define a constant used in the projection calculations

K1 = 40



# Define the speed at which the cube rotates

incrementSpeed = 0.9





def calculateX(i: int, j: int, k: int) -> float:

    """

    Calculate the X coordinate in 3D space.



    Parameters:

        i (int): The X coordinate of the point.

        j (int): The Y coordinate of the point.

        k (int): The Z coordinate of the point.



    Returns:

        float: The calculated X coordinate.

    """

    return (

        j * math.sin(A) * math.sin(B) * math.cos(C)

        - k * math.cos(A) * math.sin(B) * math.cos(C)

        + j * math.cos(A) * math.sin(C)

        + k * math.sin(A) * math.sin(C)

        + i * math.cos(B) * math.cos(C)

    )





def calculateY(i: int, j: int, k: int) -> float:

    """

    Calculate the Y coordinate in 3D space.



    Parameters:

        i (int): The X coordinate of the point.

        j (int): The Y coordinate of the point.

        k (int): The Z coordinate of the point.



    Returns:

        float: The calculated Y coordinate.

    """

    return (

        j * math.cos(A) * math.cos(C)

        + k * math.sin(A) * math.cos(C)

        - j * math.sin(A) * math.sin(B) * math.sin(C)

        + k * math.cos(A) * math.sin(B) * math.sin(C)

        - i * math.cos(B) * math.sin(C)

    )





def calculateZ(i: int, j: int, k: int) -> float:

    """

    Calculate the Z coordinate in 3D space.



    Parameters:

        i (int): The X coordinate of the point.

        j (int): The Y coordinate of the point.

        k (int): The Z coordinate of the point.



    Returns:

        float: The calculated Z coordinate.

    """

    return (

        k * math.cos(A) * math.cos(B) - j * math.sin(A) * math.cos(B) + i * math.sin(B)

    )





def calculateForSurface(cubeX: int, cubeY: int, cubeZ: int, ch: str) -> None:

    """

    Calculate the coordinates for a surface of a cube and update the buffer.



    Parameters:

        cubeX (int): The X coordinate of the cube.

        cubeY (int): The Y coordinate of the cube.

        cubeZ (int): The Z coordinate of the cube.

        ch (str): The character to be displayed at the calculated position.

    """

    global buffer, zBuffer, width, height

    x: float = calculateX(cubeX, cubeY, cubeZ)

    y: float = calculateY(cubeX, cubeY, cubeZ)

    z: float = calculateZ(cubeX, cubeY, cubeZ) + distanceFromCam



    ooz: float = 1 / z



    xp = int(width / 2 + horizontalOffset + K1 * ooz * x * 2)

    yp = int(height / 2 + K1 * ooz * y)



    idx: int = xp + yp * width

    if 0 <= idx < width * height and ooz > zBuffer[idx]:

        zBuffer[idx] = ooz

        buffer[idx] = ch





def render() -> None:

    """

    Render the buffer to the console.

    """

    global buffer, zBuffer

    os.system("cls" if os.name == "nt" else "clear")

    for k in range(width * height):

        if k % width:

            print(buffer[k], end="")

        else:

            print()





while True:

    # Initialize the display buffer and the z-buffer

    buffer: list[str] = [" "] * (width * height)

    zBuffer: list[int] = [0] * (width * height)



    # Define the width of the cube and its horizontal offset

    cubeWidth = 20

    horizontalOffset: int = -2 * cubeWidth



    # For each point on the cube's surface, calculate its 2D coordinates and update the display buffer and the z-buffer

    cubeX = -cubeWidth

    while cubeX < cubeWidth:

        cubeY = -cubeWidth

        while cubeY < cubeWidth:

            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")

            calculateForSurface(cubeWidth, cubeY, cubeX, "$")

            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")

            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")

            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")

            calculateForSurface(cubeX, cubeWidth, cubeY, "+")

            cubeY += incrementSpeed

        cubeX += incrementSpeed



    # Repeat the above steps for a smaller cube

    cubeWidth = 10

    horizontalOffset = 1 * cubeWidth



    cubeX = -cubeWidth

    while cubeX < cubeWidth:

        cubeY = -cubeWidth

        while cubeY < cubeWidth:

            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")

            calculateForSurface(cubeWidth, cubeY, cubeX, "$")

            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")

            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")

            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")

            calculateForSurface(cubeX, cubeWidth, cubeY, "+")

            cubeY += incrementSpeed

        cubeX += incrementSpeed



    # Repeat the above steps for an even smaller cube

    cubeWidth = 5

    horizontalOffset = 8 * cubeWidth



    cubeX = -cubeWidth

    while cubeX < cubeWidth:

        cubeY = -cubeWidth

        while cubeY < cubeWidth:

            calculateForSurface(cubeX, cubeY, -cubeWidth, "@")

            calculateForSurface(cubeWidth, cubeY, cubeX, "$")

            calculateForSurface(-cubeWidth, cubeY, -cubeX, "~")

            calculateForSurface(-cubeX, cubeY, cubeWidth, "#")

            calculateForSurface(cubeX, -cubeWidth, -cubeY, ";")

            calculateForSurface(cubeX, cubeWidth, cubeY, "+")

            cubeY += incrementSpeed

        cubeX += incrementSpeed



    # Render the current state of the display buffer to the terminal

    render()



    # Increment the rotation angles, causing the cubes to rotate

    A += 0.05

    B += 0.05

    C += 0.01

This should at least give you a good idea on where to start, based on the logic.

Please ask me anything, here to help!

Hello @CodeByAidan Thanks for the quick response :) I basically am unable to grasp how to change the math logic to change the shape ( or derive it).

Could you tell me which maths concepts I need for this? And can you tell me for which point the angle theta has been used?

Ps: I don't know if it's some problem in my pc, but I am not able to run the program.

Thank you in advance, Regards, Tree-t

tree-t avatar Jun 18 '24 14:06 tree-t