cube.c
cube.c copied to clipboard
Add comments
Me bad at math, please explain some of the variables and link to equations, thxz :cat:
Example: why does xp = (int)(width / 2 + horizontalOffset + K1 * ooz * x * 2);
work??
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 😿.
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...
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 )
You can find how this works at this website: http://matrixmultiplication.xyz/
How do we make a cube?
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()
Woah @livxy thank you so much 🔥
thx bro
gotchu <3
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...
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 )
You can find how this works at this website: http://matrixmultiplication.xyz/
How do we make a cube?
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
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:
-
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
-
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. - Rotation around the x-axis:
-
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:
-
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
-
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!
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, andpygame
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:
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
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.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:
![]()
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
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
tocube.py
through terminalI 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