Skip to content

Instantly share code, notes, and snippets.

@hx2A
Created January 21, 2022 19:22
Show Gist options
  • Save hx2A/e8328595e156cbd12adea5d0579ff7d3 to your computer and use it in GitHub Desktop.
Save hx2A/e8328595e156cbd12adea5d0579ff7d3 to your computer and use it in GitHub Desktop.
Rotating Tesseract using the new Py5Vector class
# This example is a py5 version of a 3D printing project I did a few years ago.
# See https://ixora.io/itp/3d_printing/tesseracts/ for history and more details
# about the math.
# This code is written in py5's module mode. This could be quickly modified for
# imported mode, which would eliminate the need for the `import py5` and the
# `py5.` prefixes in the code. See http://py5.ixora.io/content/py5_modes.html
# to learn more.
import numpy as np
import py5_tools
import py5
from py5 import Py5Vector
# Create a list of Py5Vectors representing the 4D coordinates for all 16 of the
# tesseract's vertices. The trick with `bin(x)[2:].zfill(4)]` is just a way to
# get each vertex in the simplest way possible.
tesseract_vertices = [
(50, 250 * Py5Vector([{'0': -1, '1': 1}[c] for c in bin(x)[2:].zfill(4)])) for x in range(16)]
# Create a list of indices into `tesseract_vertices` to store all pairs of
# vertices that are connected with an edge. The trick with `i ^ 2**x` uses xor
# to simplify the code with binary operations. Both this and the previous line
# of code took some thought and experimentation to get right.
tesseract_edges = [e for sublist in [
[(i, i ^ 2**x) for x in range(4)] for i in range(16)] for e in sublist if e[0] < e[1]]
# This creates a numpy array that contains the vertex coordinates for a
# cylinder with a height of 1 and a radius of 1. The vertices are ordered in
# such a way that they can be interpreted by py5 as a TRIANGLE_STRIP shape.
# These vertex coordinates can be transformed to represent the coordinates
# of truncated cones of any height and radii. These vertices will be used to
# create the object's edges. Creating the edges this way is considerably faster
# than creating each edge's vertex coordinates from scratch. The frame rate of
# this example would be poor if the edges were not drawn this way.
cylinder_detail = 100
circle_vertices = np.array([Py5Vector.from_heading(
py5.TWO_PI * x / cylinder_detail) for x in range(cylinder_detail + 1)])
cylinder_vertices = np.zeros((2 * cylinder_detail + 2, 3))
cylinder_vertices[::2, :2] = circle_vertices
cylinder_vertices[1::2, :2] = circle_vertices
cylinder_vertices[1::2, 2] = -1
# The below function is similar to the math that would project 3D coordinates
# down to 2D.
def vertex_4d_to_3d(rad, v, theta, w):
# rotate around yz plane
v.xw = v.xw.rotate(theta)
# translate in the w dimension
v.w += w
# calculate sphere size, project 4D vector to 3D space
return rad / v.w, v.xyz / v.w
# The below function will draw an edge using the precalculated
# `cylinder_vertices` values.
def draw_edge(r1, p1, r2, p2):
# The below calculations adjust the parameters to improve the aesthetics of
# the result. See https://ixora.io/itp/3d_printing/tesseracts/ for an
# explanation of the math.
v = p1 - p2
beta = py5.sin((r2 - r1) / v.mag)
r1p = r1 * py5.cos(beta)
p1p = p1 - v.norm * r1 * py5.sin(beta)
r2p = r2 * py5.cos(beta)
p2p = p2 - v.norm * r2 * py5.sin(beta)
# transform the cylinder vertices into truncated cone vertices
truncated_cone_vertices = cylinder_vertices.copy()
truncated_cone_vertices[::2, :2] *= r1p
truncated_cone_vertices[1::2, :2] *= r2p
truncated_cone_vertices[1::2, 2] *= p1p.dist(p2p) # or (p1p - p2p).mag
with py5.push_matrix():
# translate to one endpoint of the truncated cone
py5.translate(*p1p)
# orient the truncated cone correctly
py5.rotate_z(v.heading[1])
py5.rotate_y(v.heading[0])
# now create the shape
with py5.begin_shape(py5.TRIANGLE_STRIP):
py5.vertices(truncated_cone_vertices)
rot = 0.0
def setup():
py5.size(800, 800, py5.P3D)
py5.fill('#800')
py5.no_stroke()
def draw():
global rot
py5.background('#fff')
py5.ambient_light(100, 100, 100)
py5.light_specular(100, 100, 100)
py5.directional_light(100, 100, 100, 0, 0, -1)
py5.specular(150, 150, 150)
py5.shininess(5.0)
msg_height = 30
py5.text_size(msg_height)
py5.text(msg := '@py5coding', py5.width - py5.text_width(msg) - 10, py5.height - msg_height / 2)
py5.translate(py5.width / 2, py5.height / 2)
py5.scale(125)
py5.rotate_y(py5.radians(45))
rot += py5.radians(1)
projected_vertices = [vertex_4d_to_3d(
rad, v.copy, rot, 500) for rad, v in tesseract_vertices]
for radius, v in projected_vertices:
# draw a sphere for each vertex
with py5.push_matrix():
# observe that `v` is a Py5Vector and that the below code passes
# the vector coordinates to `translate()`. This line could also be
# written as `py5.translate(v.x, v.y, v.z)`.
py5.translate(*v)
# increasing the radius slightly fixes a slight cosmetic issue.
# is there a better way to do this?
py5.sphere(1.0075 * radius)
for index0, index1 in tesseract_edges:
draw_edge(*projected_vertices[index0], *projected_vertices[index1])
# Presently, the `run_sketch()` method by default will by block when run with
# the basic python interpreter. That means the execution would not advance to
# the following line of code until after the Sketch exits, which in this case
# would mean the animated gif would never get created. Setting the block
# parameter to `False` is necessary for any subsequent lines of code to execute
# while the Sketch is running. [As I write this I feel the need to revisit these
# design decisions, so perhaps this behavior might change somewhat in the
# future.]
py5.run_sketch(block=False)
# Create an animated GIF. 90 frames is a 90 degree rotation. Because of
# symmetry, 90 degrees is all that is needed to make the GIF loop properly.
py5_tools.animated_gif('/tmp/tesseract.gif', 90, 0, 0.02)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment