Skip to content

Instantly share code, notes, and snippets.

Last active Aug 28, 2021
What would you like to do?
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_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()
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_open(width=1024, height=1024,
x_bias=-VP_WIDTH/2, y_bias=-VP_HEIGHT/2,
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
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:
draw(Level(0, 0, 0, n=2))
# Footer
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 += ' />'
s += '>'
def xml_open(*args, **kwargs):
xml(*args, **kwargs, _xml_tag_is_a_singleton=False)
def xml_close(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 = ''
xml_open('svg', width=width, height=height, viewBox=vb, xmlns=ns, **opts)
def svg_close():
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