Skip to content

Instantly share code, notes, and snippets.

@Denbergvanthijs
Last active November 9, 2024 08:09
Show Gist options
  • Save Denbergvanthijs/7f6936ca90a683d37216fd80f5750e9c to your computer and use it in GitHub Desktop.
Save Denbergvanthijs/7f6936ca90a683d37216fd80f5750e9c to your computer and use it in GitHub Desktop.
3D spinning donut in Python. Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
import numpy as np
screen_size = 40
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")
A = 1
B = 1
R1 = 1
R2 = 2
K2 = 5
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))
def render_frame(A: float, B: float) -> np.ndarray:
"""
Returns a frame of the spinning 3D donut.
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
"""
cos_A = np.cos(A)
sin_A = np.sin(A)
cos_B = np.cos(B)
sin_B = np.sin(B)
output = np.full((screen_size, screen_size), " ") # (40, 40)
zbuffer = np.zeros((screen_size, screen_size)) # (40, 40)
cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing)) # (315,)
sin_phi = np.sin(phi) # (315,)
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing)) # (90,)
sin_theta = np.sin(theta) # (90,)
circle_x = R2 + R1 * cos_theta # (90,)
circle_y = R1 * sin_theta # (90,)
x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T # (90, 315)
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T # (90, 315)
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T # (90, 315)
ooz = np.reciprocal(z) # Calculates 1/z
xp = (screen_size / 2 + K1 * ooz * x).astype(int) # (90, 315)
yp = (screen_size / 2 - K1 * ooz * y).astype(int) # (90, 315)
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta) # (315, 90)
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A)) # (315, 90)
L = np.around(((L1 + L2) * 8)).astype(int).T # (90, 315)
mask_L = L >= 0 # (90, 315)
chars = illumination[L] # (90, 315)
for i in range(90):
mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]]) # (315,)
zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])
return output
def pprint(array: np.ndarray) -> None:
"""Pretty print the frame."""
print(*[" ".join(row) for row in array], sep="\n")
if __name__ == "__main__":
for _ in range(screen_size * screen_size):
A += theta_spacing
B += phi_spacing
print("\x1b[H")
pprint(render_frame(A, B))
@EraserGuy
Copy link

miih cant run it

@Mech-08
Copy link

Mech-08 commented Apr 24, 2024

My doughnut is not spinning :(. I'm getting snapshots for each timestamp.

if youre using VS code , then you have to put the terminal on fullscreen when you run the code. (click the red circled button in the pic attached). And it runs smoothly Screenshot (6)
Screenshot (5)_LI

for me terminal dosent show up anything after running it yk how to fix?

Screenshot 2024-04-25 001301
Tap on the blue circled button then you will see the terminal

@FaysouRGB
Copy link

Nice

@Mech-08
Copy link

Mech-08 commented Apr 28, 2024

Nice

thnx :)

@Kipstal
Copy link

Kipstal commented Jun 5, 2024

im so happy

@longz85
Copy link

longz85 commented Nov 9, 2024

using os library.It better

@longz85
Copy link

longz85 commented Nov 9, 2024

like this:

import numpy as np
from time import sleep
import os

screen_size = 40
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")

A = 1
B = 1
R1 = 1
R2 = 2
K2 = 5
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))

def render_frame(A: float, B: float) -> np.ndarray:
"""
Returns a frame of the spinning 3D donut.
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
"""
cos_A = np.cos(A)
sin_A = np.sin(A)
cos_B = np.cos(B)
sin_B = np.sin(B)

output = np.full((screen_size, screen_size), " ")  # (40, 40)
zbuffer = np.zeros((screen_size, screen_size))  # (40, 40)

cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing))  # (315,)
sin_phi = np.sin(phi)  # (315,)
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing))  # (90,)
sin_theta = np.sin(theta)  # (90,)
circle_x = R2 + R1 * cos_theta  # (90,)
circle_y = R1 * sin_theta  # (90,)

x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T  # (90, 315)
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T  # (90, 315)
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T  # (90, 315)
ooz = np.reciprocal(z)  # Calculates 1/z
xp = (screen_size / 2 + K1 * ooz * x).astype(int)  # (90, 315)
yp = (screen_size / 2 - K1 * ooz * y).astype(int)  # (90, 315)
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta)  # (315, 90)
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A))  # (315, 90)
L = np.around(((L1 + L2) * 8)).astype(int).T  # (90, 315)
mask_L = L >= 0  # (90, 315)
chars = illumination[L]  # (90, 315)

for i in range(90):
    mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]])  # (315,)

    zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
    output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])

return output

def pprint(array: np.ndarray) -> None:
"""Pretty print the frame."""
print(*[" ".join(row) for row in array], sep="\n")

if name == "main":
for _ in range(screen_size * screen_size):
A += theta_spacing
B += phi_spacing
print("\x1b[H")
os.system('cls')
pprint(render_frame(A, B))
sleep(0.05)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment