Rotating Tesseract using the new Py5Vector class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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