Python 隨便寫 - ASCII Spinning Cube

前言

因為被旋轉甜甜圈給驚豔到,所以也想要自己來寫寫看,不過甜甜圈的數學看了頭好痛,所以改寫稍微簡單一點的旋轉立方體,不囉嗦先看成果:

Spinning Cube

Problem Analysis

要在螢幕上繪製一個旋轉的立方體,我們大致上要處理兩個問題,一個是 3 維空間中物體的旋轉,另外一個是將 3D 的物體投影到 2D 的平面上

Cube Projection

在開始之前先來定義一下座標空間,從我們的角度看像物體,物體的左右會是 x 軸,物體的上下是 y 軸,我們到物體的方向則是 z 軸。

Coordinate Space

Rotation Matrix

物體在三維空間中的旋轉可以使用旋轉矩陣來計算,以沿著 z 軸的主動旋轉來說 (在 xy 平面逆時針),我們就可用下面的旋轉矩陣來求得旋轉後的位置:

Rotation Matrix Z

舉例來說,[x,y,z]=[1,0,0]{[x, y, z] = [1, 0, 0]} 沿 z 軸逆時針旋轉 90% 會變成 [x,y,z]=[0,1,0]{[x, y, z] = [0, 1, 0]}

Rotation Matrix Example

而旋轉矩陣的效果是可以疊加的,因此我們如果我們依序沿 z軸、y軸、x軸進行旋轉,得到的旋轉矩陣如下:

Rotation Matrix XYZ

有了旋轉矩陣後,我們就能去計算立方體的旋轉了!

Perspective Projection

Perspective Projection

投影的部分其實沒有像項中的難,用的的只是最基礎的等比三角形的概念,假設我們的眼睛到物體的距離是 z{z},而我們的眼睛到螢幕的距離是 z{z'},因此將點 (x,y){(x,y)} 投影到螢幕上分別會落在:

x=xzz{\displaystyle x' = {\frac {xz'}{z}}}

y=yzz{\displaystyle y' = {\frac {yz'}{z}}}

Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from math import sin, cos

# Rotation angles
A = B = C = 0

# Screen layout size
width = 100
height = 30

# Cube's half side length
cubeWidth = 20

# Distances
distance_to_screen = 20
distance_to_cube = 100

# Rotation matrix
def calculateX(x, y, z):
return x * cosB * cosC \
+ y * (sinA * sinB * cosC - cosA * sinC) \
+ z * (cosA * sinB * cosC + sinA * sinC)

def calculateY(x, y, z):
return x * cosB * sinC \
+ y * (sinA * sinB * sinC + cosA * cosC) \
+ z * (cosA * sinB * sinC - sinA * cosC)

def calculateZ(x, y, z):
return - x * sinB + y * sinA * cosB + z * cosA * cosB

# Project 3D cube on 2D screen
def project(_x, _y, _z, char):
# Apply rotation transformations
x = calculateX(_x, _y, _z)
y = calculateY(_x, _y, _z)
z = calculateZ(_x, _y, _z) + distance_to_cube

# Perspective projection
z_ratio = distance_to_screen/z
xp = round(width/2 + x*2*z_ratio) # x*2 becasue Letterspacing is smaller than Linespacing
yp = round(height/2 + y*z_ratio)

# Store the points nearest to us
if (0<=xp<width and 0<=yp<height and z_ratio>buffer[yp][xp]):
buffer[yp][xp] = z_ratio
charbuffer[yp][xp] = char

if __name__ == "__main__":
print("\x1b[2J")

while True:
sinA, sinB, sinC = sin(A), sin(B), sin(C)
cosA, cosB, cosC = cos(A), cos(B), cos(C)

# Initialize buffers to store projected points
buffer = [[0]*width for _ in range(height)]
charbuffer = [[' ']*width for _ in range(height)]

# Project each point of the cube onto the screen
for x in range(-cubeWidth, cubeWidth+1):
for y in range(-cubeWidth, cubeWidth+1):
project(x, y, -cubeWidth, "#")
project(x, y, cubeWidth, "%")
project(x, cubeWidth, y, "&")
project(x, -cubeWidth, y, "@")
project(cubeWidth, x, y, "*")
project(-cubeWidth, x, y, "+")

# Clear the screen and print the current state of the 2D projection
print("\x1b[H")
for row in charbuffer:
for char in row:
print(char, end='')
print()

# Increment rotation angles for the next frame
A += 0.05
B += 0.07
C += 0.02

參考資料

ASMR Programming - Spinning Cube - No Talking
Rotation matrix - Wikipedia
旋轉矩陣(Rotation Matrix) - 拾人牙慧- 痞客邦