3D Convex hull in Python, a Blender implementation.

A slight adaption of the code in my previous post to make it directly usable as a add mesh extension in Blender.

A Blender add mesh extension

As of Blender 2.64 there is a native Convex Hull operator availablein Blender.

I won't elaborate too much on my adaption as it has nothing to do with web development. The script can be downloaded from my site and should be installed in Blenders addons directory and enabled in the user preferences. It is tested with Blender 2.61.

I still think it is a nice implementation and although it is pure Python, it performs quite well. Of course there is a native convex hull implementation in Blender game engine but that one isn't accessible from Python. Previous implemenations relied on external programs like qhull, which might be faster for large point sets but having a pure Python implementation that is tightly integrated with Blender feels cleaner. It certainly can handle points sets of up to a couple of thousand points.

I have adapted it to randomize the order of the vertices if the hull algorithm bombs. This will happen if adding a point involves calculating the volume of many degenerate tetrahedrons which is the case when trying to compute the hull of Blenders default uv-sphere. I tried to use Python's decimal module but the results did not improve so now if an error is encountered the vertices are reordered and a second attempt is made. Not clean, but simple.

3D Convex hull in Python

In this article I present a present a reimplementation in pure Python of Joseph O'Rourke's incremental 3D convex hull algorithm from his book Computational Geometry in C.

A convex hull in pure Python

This is the second, rather off topic, article on computational geometry in this blog. The previous article presented an implementation of the marching tetrahedrons algorithm.
The goal of this article is to provide object oriented, pythonic code to compute the convex hull of a collection of 3D points. The code is contained in a single Python module that may be downloaded from GitHub.
A sample of how to use this module is shown below, where we create a a roughly spherical cloud of points, calculate its convex hull and print this hull in STL format to stdout. The resulting object is shown in the image (as seen in Blender).
from random import random
from chull import Vector,Hull

sphere=[]
for i in range(2000):
 x,y,z = 2*random()-1,2*random()-1,2*random()-1
 if x*x+y*y+z*z < 1.0:
  sphere.append(Vector(x,y,z))
h=Hull(sphere)
h.Print()

Difference from the original implementation

The original code restricted the coordinates of points to integers, here there is no such restriction. This might result in errors if the coordinates are large.

Marching Tetrahedrons in Python

In this atricle we show a simple implementation of the Marching Tetrahedrons algorithm in Python.

Marching Tetrahedrons

The listing povided below is a straightforward reimplementation in Python of the ideas and code presented by Paul Bourke. The image

shows the result of sampling a simple lobed function (rendered in Blender). I might implement the code directly in Blender but for now we export to an STL file that can be read by almost any 3D package. Note that the exported triangles do not have normals that point in a uniform direction, in fact we do no export any normals at all. You have to recalculate the normals in your 3D package before you can render the resulte with smooth shading.

class Vector: # struct XYZ
 def __init__(self,x,y,z):
  self.x=x
  self.y=y
  self.z=z
 
 def __str__(self):
  return str(self.x)+" "+str(self.y)+" "+str(self.z)
  
class Gridcell: # struct GRIDCELL
 def __init__(self,p,n,val):
  self.p   = p   # p=[8]
  self.n   = n   # n=[8]
  self.val = val # val=[8]

class Triangle: # struct TRIANGLE
 def __init__(self,p1,p2,p3):
  self.p = [p1, p2, p3] # vertices

# return triangle as an ascii STL facet  
 def __str__(self):
  return """facet normal 0 0 0
outer loop
vertex %s
vertex %s
vertex %s
endloop
endfacet"""%(self.p[0],self.p[1],self.p[2])

# return a 3d list of values
def readdata(f=lambda x,y,z:x*x+y*y+z*z,size=5.0,steps=11):
 m=int(steps/2)
 ki = []
 for i in range(steps):
  kj = []
  for j in range(steps):
   kd=[]
   for k in range(steps):
    kd.append(f(size*(i-m)/m,size*(j-m)/m,size*(k-m)/m))
   kj.append(kd)
  ki.append(kj)
 return ki

from math import cos,exp,atan2

def lobes(x,y,z):
 try:
  theta = atan2(x,y)         # sin t = o 
 except:
  theta = 0
 try:
  phi = atan2(z,y)
 except:
  phi = 0
 r = x*x+y*y+z*z
 ct=cos(theta)
 cp=cos(phi)
 return ct*ct*cp*cp*exp(-r/10)
 
def main():

 data = readdata(lobes,5,41)
 isolevel = 0.1

 #print(data)
 
 triangles=[]
 for i in range(len(data)-1):
  for j in range(len(data[i])-1):
   for k in range(len(data[i][j])-1):
    p=[None]*8
    val=[None]*8
    #print(i,j,k)
    p[0]=Vector(i,j,k)
    val[0] = data[i][j][k]
    p[1]=Vector(i+1,j,k)
    val[1] = data[i+1][j][k]
    p[2]=Vector(i+1,j+1,k)
    val[2] = data[i+1][j+1][k]
    p[3]=Vector(i,j+1,k)
    val[3] = data[i][j+1][k]
    p[4]=Vector(i,j,k+1)
    val[4] = data[i][j][k+1]
    p[5]=Vector(i+1,j,k+1)
    val[5] = data[i+1][j][k+1]
    p[6]=Vector(i+1,j+1,k+1)
    val[6] = data[i+1][j+1][k+1]
    p[7]=Vector(i,j+1,k+1)
    val[7] = data[i][j+1][k+1]
   
    grid=Gridcell(p,[],val)
    triangles.extend(PolygoniseTri(grid,isolevel,0,2,3,7))
    triangles.extend(PolygoniseTri(grid,isolevel,0,2,6,7))
    triangles.extend(PolygoniseTri(grid,isolevel,0,4,6,7))
    triangles.extend(PolygoniseTri(grid,isolevel,0,6,1,2))
    triangles.extend(PolygoniseTri(grid,isolevel,0,6,1,4))
    triangles.extend(PolygoniseTri(grid,isolevel,5,6,1,4))
    
 export_triangles(triangles)

def export_triangles(triangles): # stl format
 print("solid points")
 for tri in triangles:
  print(tri)
 print("endsolid points")
 
def t000F(g, iso, v0, v1, v2, v3):
 return []

def t0E01(g, iso, v0, v1, v2, v3):
 return [Triangle(
 VertexInterp(iso,g.p[v0],g.p[v1],g.val[v0],g.val[v1]),
 VertexInterp(iso,g.p[v0],g.p[v2],g.val[v0],g.val[v2]),
 VertexInterp(iso,g.p[v0],g.p[v3],g.val[v0],g.val[v3]))
 ]

def t0D02(g, iso, v0, v1, v2, v3):
 return [Triangle(
 VertexInterp(iso,g.p[v1],g.p[v0],g.val[v1],g.val[v0]),
 VertexInterp(iso,g.p[v1],g.p[v3],g.val[v1],g.val[v3]),
 VertexInterp(iso,g.p[v1],g.p[v2],g.val[v1],g.val[v2]))
 ]

def t0C03(g, iso, v0, v1, v2, v3):
 tri=Triangle(
 VertexInterp(iso,g.p[v0],g.p[v3],g.val[v0],g.val[v3]),
 VertexInterp(iso,g.p[v0],g.p[v2],g.val[v0],g.val[v2]),
 VertexInterp(iso,g.p[v1],g.p[v3],g.val[v1],g.val[v3]))
 return [tri,Triangle(
 tri.p[2],
 VertexInterp(iso,g.p[v1],g.p[v2],g.val[v1],g.val[v2]),
 tri.p[1])
 ]

def t0B04(g, iso, v0, v1, v2, v3):
 return [Triangle(
 VertexInterp(iso,g.p[v2],g.p[v0],g.val[v2],g.val[v0]),
 VertexInterp(iso,g.p[v2],g.p[v1],g.val[v2],g.val[v1]),
 VertexInterp(iso,g.p[v2],g.p[v3],g.val[v2],g.val[v3]))
 ]

def t0A05(g, iso, v0, v1, v2, v3):
 tri = Triangle(
 VertexInterp(iso,g.p[v0],g.p[v1],g.val[v0],g.val[v1]),
 VertexInterp(iso,g.p[v2],g.p[v3],g.val[v2],g.val[v3]),
 VertexInterp(iso,g.p[v0],g.p[v3],g.val[v0],g.val[v3]))
 return [tri,Triangle(
 tri.p[0],
 VertexInterp(iso,g.p[v1],g.p[v2],g.val[v1],g.val[v2]),
 tri.p[1])
 ]

def t0906(g, iso, v0, v1, v2, v3):
 tri=Triangle(
 VertexInterp(iso,g.p[v0],g.p[v1],g.val[v0],g.val[v1]),
 VertexInterp(iso,g.p[v1],g.p[v3],g.val[v1],g.val[v3]),
 VertexInterp(iso,g.p[v2],g.p[v3],g.val[v2],g.val[v3]))
 return [tri,
 Triangle(
 tri.p[0],
 VertexInterp(iso,g.p[v0],g.p[v2],g.val[v0],g.val[v2]),
 tri.p[2])
 ]

def t0708(g, iso, v0, v1, v2, v3):
 return [Triangle(
 VertexInterp(iso,g.p[v3],g.p[v0],g.val[v3],g.val[v0]),
 VertexInterp(iso,g.p[v3],g.p[v2],g.val[v3],g.val[v2]),
 VertexInterp(iso,g.p[v3],g.p[v1],g.val[v3],g.val[v1]))
 ]

trianglefs = {7:t0708,8:t0708,9:t0906,6:t0906,10:t0A05,5:t0A05,11:t0B04,4:t0B04,12:t0C03,3:t0C03,13:t0D02,2:t0D02,14:t0E01,1:t0E01,0:t000F,15:t000F}

def PolygoniseTri(g, iso, v0, v1, v2, v3):
 triangles = []

 #   Determine which of the 16 cases we have given which vertices
 #   are above or below the isosurface

 triindex = 0;
 if g.val[v0] < iso: triindex |= 1
 if g.val[v1] < iso: triindex |= 2
 if g.val[v2] < iso: triindex |= 4
 if g.val[v3] < iso: triindex |= 8

 return trianglefs[triindex](g, iso, v0, v1, v2, v3)

def VertexInterp(isolevel,p1,p2,valp1,valp2):
 if abs(isolevel-valp1) < 0.00001 :
  return(p1);
 if abs(isolevel-valp2) < 0.00001 :
  return(p2);
 if abs(valp1-valp2) < 0.00001 :
  return(p1);
 mu = (isolevel - valp1) / (valp2 - valp1)
 return Vector(p1.x + mu * (p2.x - p1.x), p1.y + mu * (p2.y - p1.y), p1.z + mu * (p2.z - p1.z))

if __name__ == "__main__":
 main()