Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@louisswarren
Last active August 28, 2021 01:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save louisswarren/bc87c89b818005fd5599a2c96d758700 to your computer and use it in GitHub Desktop.
Save louisswarren/bc87c89b818005fd5599a2c96d758700 to your computer and use it in GitHub Desktop.
3D SVG renderer for menger sponges
import numpy as np
from svg import *
compose = lambda f: lambda g: lambda *a, **k: f(g(*a, **k))
PURPLE = '#6a1eb0'
ORANGE = '#ff824a'
BLUE = '#75c1ff'
VP_WIDTH = 2.0
VP_HEIGHT = 2.0
VP_EYE = -np.array((11.90, 11, 27))
VP_DIR = -np.array((-2.00, -1, -3))
VP_FOC = 2.5
# I don't know numpy, there is probably a builtin thing for doing this
B3 = -VP_DIR; B3 = B3 / np.linalg.norm(B3)
B2 = (0, 1, 0) - B3[2] * B3; B2 = B2 / np.linalg.norm(B2)
B1 = np.cross(B2, B3); B1 = B1 / np.linalg.norm(B1)
B = np.matrix((B1, B2, B3)).transpose()
VIEWBASIS = B.I
def zcoordinate(u):
x = VIEWBASIS * np.matrix(u - (VP_EYE + VP_DIR)).transpose()
return np.ndarray.item(x[2]), np.ndarray.item(VP_FOC*x[0]/(1-x[2]), 0), np.ndarray.item(VP_FOC*x[1]/(1-x[2]), 0)
class View:
def __init__(self, position, direction):
self.pos = position
self.dir = direction
bz = -self.dir / np.linalg.norm(self.dir)
by = (0, 1, 0) - bz[2] * bz
by = by / np.linalg.norm(by)
bx = np.cross(by, bz)
self.basis = bx, by, bz
self.invbasis = np.matrix(self.basis).transpose().I
def global_to_view(self, v):
v = v - (self.pos + self.dir)
return self.invbasis * v.transpose()
# Basic header
svg_header()
svg_open(width=1024, height=1024,
x_bias=-VP_WIDTH/2, y_bias=-VP_HEIGHT/2,
xw=VP_WIDTH, yh=VP_HEIGHT)
class Poly:
def __init__(self, *points, **opts):
self.points = [np.array(p) for p in points]
self.opts = opts
centre = sum(self.points[:-1]) / len(points)
zcoords = [zcoordinate(point) for point in self.points]
self.projpoints = tuple((x, y) for _, x, y in zcoords)
self.z = zcoordinate(centre)[0]
def draw(self):
svg_polygon(*self.projpoints, **self.opts)
def __lt__(self, other):
return self.z < other.z
class Cube:
def __init__(self, x, y, z, w=1):
self.faces = [
Poly((x,y,z), (x+w,y,z), (x+w,y+w,z), (x,y+w,z), fill=PURPLE, stroke=PURPLE, stroke_width="0.001"),
Poly((x,y,z), (x,y,z+w), (x,y+w,z+w), (x,y+w,z), fill=ORANGE, stroke=ORANGE, stroke_width="0.001"),
Poly((x,y,z), (x+w,y,z), (x+w,y,z+w), (x,y,z+w), fill=BLUE, stroke=BLUE, stroke_width="0.001"),
]
def projections(self):
yield from self.faces
class Level:
def __init__(self, x, y, z, n=1):
if n == 1:
f = Cube
else:
f = lambda *a, **k: Level(*a, **k, n=n-1)
self.cubes = [
# Back
f(x+3**(n-1)*2, y+3**(n-1)*2, z+3**(n-1)*2), f(x+3**(n-1)*1, y+3**(n-1)*2, z+3**(n-1)*2), f(x+3**(n-1)*0, y+3**(n-1)*2, z+3**(n-1)*2),
f(x+3**(n-1)*0, y+3**(n-1)*1, z+3**(n-1)*2), f(x+3**(n-1)*2, y+3**(n-1)*1, z+3**(n-1)*2),
f(x+3**(n-1)*2, y+3**(n-1)*0, z+3**(n-1)*2), f(x+3**(n-1)*1, y+3**(n-1)*0, z+3**(n-1)*2), f(x+3**(n-1)*0, y+3**(n-1)*0, z+3**(n-1)*2),
# Mid
f(x+3**(n-1)*2, y+3**(n-1)*2, z+3**(n-1)*1), f(x+3**(n-1)*0, y+3**(n-1)*2, z+3**(n-1)*1),
f(x+3**(n-1)*2, y+3**(n-1)*0, z+3**(n-1)*1), f(x+3**(n-1)*0, y+3**(n-1)*0, z+3**(n-1)*1),
# Front
f(x+3**(n-1)*2, y+3**(n-1)*2, z+3**(n-1)*0), f(x+3**(n-1)*1, y+3**(n-1)*2, z+3**(n-1)*0), f(x+3**(n-1)*0, y+3**(n-1)*2, z+3**(n-1)*0),
f(x+3**(n-1)*0, y+3**(n-1)*1, z+3**(n-1)*0), f(x+3**(n-1)*2, y+3**(n-1)*1, z+3**(n-1)*0),
f(x+3**(n-1)*2, y+3**(n-1)*0, z+3**(n-1)*0), f(x+3**(n-1)*1, y+3**(n-1)*0, z+3**(n-1)*0), f(x+3**(n-1)*0, y+3**(n-1)*0, z+3**(n-1)*0),
]
def projections(self):
for cube in self.cubes:
yield from cube.projections()
def draw(obj):
polygons = sorted(obj.projections())
for polygon in polygons:
polygon.draw()
draw(Level(0, 0, 0, n=2))
# Footer
svg_close()
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
def xml(tag, _xml_tag_is_a_singleton=True, **options):
s = f'<{tag}'
kw_attrib = lambda x: x.replace('_', '-')
if options:
s += ' '
s += ' '.join(f'{kw_attrib(k)}="{str(v)}"' for k, v in options.items())
if _xml_tag_is_a_singleton:
s += ' />'
else:
s += '>'
print(s)
def xml_open(*args, **kwargs):
xml(*args, **kwargs, _xml_tag_is_a_singleton=False)
def xml_close(tag):
print(f'</{tag}>')
def svg_header():
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
def svg_open(width, height, x_bias, y_bias, xw, yh, **opts):
vb = f'{x_bias} {y_bias} {xw} {yh}'
ns = 'http://www.w3.org/2000/svg'
xml_open('svg', width=width, height=height, viewBox=vb, xmlns=ns, **opts)
def svg_close():
xml_close('svg')
def svg_poly(*points, **opts):
point_str = ' '.join(f'{x},{y}' for x, y in points)
xml('polyline', points=point_str, **opts)
def svg_polygon(*points, **opts):
point_str = ' '.join(f'{x},{y}' for x, y in points)
xml('polygon', points=point_str, **opts)
def svg_circle(point, radius, **opts):
xml('circle', cx=point[0], cy=point[1], r=radius, **opts)
def svg_line(p1, p2, **opts):
xml('line', x1=p1[0], y1=p1[1], x2=p2[0], y2=p2[1], **opts)
def svg_rect(p, width, height, x_radius=0, y_radius=0, **opts):
xml('rect', x=p[0], y=p[1], width=width, height=height,
rx=x_radius, ry=y_radius, **opts)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment