Triangulation Backgrounds for Hockey
January 21, 2019

In my effort to advance the graphics for the University of South Florida (USF) Ice Bulls club hockey team, I wanted to create a script that I could use to create unique and new Delauney triangulated backgrounds for general full screen graphics. I have a workflow for creating Delauney triangulation backgrounds using ArcMap, which I have a blog post on here, but this project I wanted to have a 3D solution for using with Blender.

Creating Points

So the first requirement for the graphic is a triangulated background, which I settled on using Python to create the points for the background, and then to export an OBJ file. All of the points go into a numpy array, and some are created with a pattern, and others with the built-in random module, then scipy spatial kicks in to organize the points, and a basic text document is created as the output OBJ.

The first step in creating the triangulation is to create edge points, which this project at this point is generally meant for screens, I am assuming a 0 starting point for all dimensions, and have the variable x and y dimensions set to 16 and 9 respectively, the z axis was set to 2, but that isn’t as important, nor will I even create coordinates for these coordinate pairs until later. So I have a couple lines to create the corner points at the minimum and maximums, and a couple lines to create a new point every 1 unit on the edge, but not at the minimums and maximums.

[cc lang=”python”] def edge_pts():
“””Creates the edge points not on corners first, and corners second”””
for j in Dim[:2]:
for i in range(1, Dim[0]):
rp.append(AOR(i, 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(i, Dim[1], R.uniform(0.0, float(Dim[2]))))
for i in range(1, Dim[1]):
rp.append(AOR(0, i, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], i, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(0, 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], Dim[1], R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(0, Dim[1], R.uniform(0.0, float(Dim[2]))))
[/cc]

The rest of the points are created using an edge buffer distance variable that aims to keep interior points a number of units away from the edge, and then using the Python default random.uniform library, I create a number of interior points, which is also a variable in the function.

[cc lang=”python”] def interior_pts():
“””A function that creates a random point within the boundaries
and at the buffer distance away from the edges.”””
rp.append(AOR(R.uniform(edge_buff, Dim[0]-edge_buff), R.uniform(edge_buff, Dim[1]-edge_buff), R.uniform(0.0, Dim[2])))
[/cc]

Creating the Delauney Mesh

The numpy array is then sent over to scipy.spatial.delauney and the points are ordered in that function. I had originally added z values to the coordinate pairs at the beginning of the coordinate creation process, but the Z-values made the triangulation more difficult than it needed to be. The z-coordinate solution is to just create random values in the OBJ file writer. So, in the OBJ file writer, we write out all of the coordinates in the order that they come up as in the scipy.spatial.delauney object, and then make up a z coordinate on the fly. After that we write out the triangulation ordering from the same scipy.spatial.delauney object, but since for OBJ files, arrays start at 1, so in this part of the writer, we add 1 to each value from the scipy.spatial.delauney object.

[cc lang=”python”] def delaunian():
“””Calculates the Delauney triangulation for the global array of points”””
rpxy = [[x,y] for [x,y,z] in rp] #List comprehension to get rid of the z-coordinate
Tri = SSP.Delaunay(rpxy)
write_obj(Tri) #write the OBJ file now that we have calculated the triangulation

def write_obj(tri):
“””A simple OBJ file writer to write points first and then the vertex face trios”””
with open(out_obj, “w”) as f:
for v in tri.points:
f.write(“v %2.4f %2.4f %2.4f\n” % (v[0], v[1], R.uniform(0.0, float(Dim[2]))))
for q in tri.vertices:
f.write(“f %s %s %s\n” % (q[0]+1, q[1]+1, q[2]+1)) #OBJ arrays start at 1
[/cc]

At the end of this part, I can then import the OBJ into blender, customize the look of the obj, and then render a sequence to my liking, and I can create as many new randomly created meshes to my liking.

The future of this project

An easy add will be to add the argparse library so the script can be run from the command line.

The first major hurdle is to automate the rendering process for this project, so an interface to the Blender library needs to be created, which will probably force me to lock down the 16×9 aspect ratio, or limit it to options with major standard video aspect ratios.

The second is to add a function that will randomly assign a material to each triangle of the output mesh. I believe this can be done before even touching blender using an MTL sidecar file to accompany the OBJ, and this would allow for the creation of Blender Material libraries, and then assignment right away of what the color should be.

In the longer term, automation of this script using an accompanying nVidia RTX graphics card (or AMD if they can also get ray tracing technology to work), and if Blender will support ray tracing workflows, this script could create a brand new unique mesh, and then quickly render a new sequence for use quite quickly.

The full code of the script

[cc lang=”python”] import random as R
import scipy.spatial as SSP
import numpy as np

rp = [] #the coordinate pairs (random points)
coord_setting = “List” #List or Tuple
Dim =(16, 9, 2) #dimensions of the screen
edge_buff = 0.5 #minimum distance from edge of dimensions
intpts = 150 #number of random points to generate
out_obj = r”Z:\test_pts.obj”

def AOR(x,y,z):
“””Array Object Return
Returns either a tuple or a list”””
if coord_setting == “List”:
return([x,y,z])
elif coord_setting == “Tuple”:
return((x,y,z))

def edge_pts():
“””Creates the edge points not on corners first, and corners second”””
for j in Dim[:2]:
for i in range(1, Dim[0]):
rp.append(AOR(i, 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(i, Dim[1], R.uniform(0.0, float(Dim[2]))))
for i in range(1, Dim[1]):
rp.append(AOR(0, i, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], i, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(0, 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], 0, R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(Dim[0], Dim[1], R.uniform(0.0, float(Dim[2]))))
rp.append(AOR(0, Dim[1], R.uniform(0.0, float(Dim[2]))))

def interior_pts():
“””A function that creates a random point within the boundaries
and at the buffer distance away from the edges.”””
rp.append(AOR(R.uniform(edge_buff, Dim[0]-edge_buff), R.uniform(edge_buff, Dim[1]-edge_buff), R.uniform(0.0, Dim[2])))

def delaunian():
“””Calculates the Delauney triangulation for the global array of points”””
rpxy = [[x,y] for [x,y,z] in rp] #List comprehension to get rid of the z-coordinate
Tri = SSP.Delaunay(rpxy)
write_obj(Tri) #write the OBJ file now that we have calculated the triangulation

def write_obj(tri):
“””A simple OBJ file writer to write points first and then the vertex face trios”””
with open(out_obj, “w”) as f:
for v in tri.points:
f.write(“v %2.4f %2.4f %2.4f\n” % (v[0], v[1], R.uniform(0.0, float(Dim[2]))))
for q in tri.vertices:
f.write(“f %s %s %s\n” % (q[0]+1, q[1]+1, q[2]+1)) #OBJ arrays start at 1

def main():
edge_pts()
for i in range(intpts):
interior_pts()
delaunian()

main()

[/cc]