Delaunay Triangulation
If you have a random scattering of points, there are millions of different ways to connect them into triangles. But in 3D graphics and engineering, we hate "skinny" or sliver triangles because they cause rendering artifacts and math errors. We want fat, well-proportioned triangles.
Delaunay Optimality:
1# The Empty Circumcircle Rule2def is_delaunay_valid(triangle, all_points):3 """Checks if a triangle is mathematically optimal."""45 # 1. Find the circle that perfectly touches all 3 corners6 circumcircle = calculate_circumcircle(triangle.v0, triangle.v1, triangle.v2)78 # 2. Check every other point in the entire dataset9 for point in all_points:10 if point in triangle.vertices:11 continue1213 # 3. If ANY other point is inside this circle, the triangle is BAD.14 if circumcircle.contains(point):15 return False1617 # If the circle is completely empty of other points, it is Delaunay Valid!18 return True
To check the "Empty Circumcircle Rule", we first need to mathematically construct that circle for any given triangle. We need to find its Center (the Circumcenter) and its Radius.
Perpendicular Bisectors:
1# Calculating the Circumcenter2def get_circumcircle(v0, v1, v2):3 """Finds the center of a circle that touches all 3 vertices."""45 # 1. Find the exact midpoint of two edges6 mid_A = (v0 + v1) / 27 mid_B = (v1 + v2) / 289 # 2. Find the perpendicular vectors to those edges10 perp_A = get_perpendicular_2d(v1 - v0)11 perp_B = get_perpendicular_2d(v2 - v1)1213 # 3. The Circumcenter is exactly where those two perpendicular lines intersect!14 center = line_intersection(mid_A, perp_A, mid_B, perp_B)1516 # 4. The radius is just the distance from the center to any vertex17 radius = distance(center, v0)1819 return center, radius
To actually build a Delaunay Triangulation, the most famous algorithm is Bowyer-Watson. Instead of trying to connect points one by one, it starts with a completed triangulation and incrementally modifies it.
The Starting Canvas:
1# Bowyer-Watson Setup2def init_delaunay(points):3 """Creates a massive triangle that contains all points."""45 # 1. Find the Bounding Box of all our points6 bbox = get_bounding_box(points)78 # 2. Mathematically calculate a triangle so large that9 # it completely engulfs the bounding box.10 super_triangle = create_super_triangle(bbox)1112 # 3. Start our triangulation list with JUST this one triangle.13 triangulation = [super_triangle]1415 return triangulation
Now that we have a starting mesh (the Super-Triangle), we insert our points one by one. When we drop a new point into the mesh, it usually ruins the Delaunay validity of the triangles around it.
Carving the Cavity:
1# Finding Bad Triangles2def find_bowyer_watson_cavity(new_point, current_triangles):3 """Finds all triangles whose circumcircle contains the new point."""45 bad_triangles = []67 for triangle in current_triangles:8 # If the new point violates the Delaunay rule for this triangle...9 if triangle.circumcircle_contains(new_point):10 # ...flag it for deletion!11 bad_triangles.append(triangle)1213 # The area occupied by the bad triangles forms a polygonal 'Cavity'14 return bad_triangles
Once the Bad triangles are deleted, we are left with a polygon-shaped hole (the Cavity) inside our mesh, with our New Point sitting exactly in the middle of it.
Healing the Mesh:
1# Repairing the Cavity2def triangulate_cavity(new_point, cavity_edges, mesh):3 """Fills the hole with new, Delaunay-valid triangles."""45 for edge in cavity_edges:6 # Connect the new point to every edge of the cavity polygon7 v0 = edge.start_vertex8 v1 = edge.end_vertex9 v2 = new_point1011 new_triangle = create_triangle(v0, v1, v2)12 mesh.add_triangle(new_triangle)
Once we have inserted every point from our dataset, the algorithm is almost finished. However, our mesh is still attached to the giant Super-Triangle we created in Step 3!
Removing Scaffolding:
1# Finalizing the Mesh2def finalize_delaunay(mesh, super_triangle):3 """Removes the artificial boundary to reveal the final mesh."""45 final_triangles = []67 for triangle in mesh.triangles:8 # Does this triangle touch any of the 3 vertices of the Super-Triangle?9 shares_vertex = triangle.shares_any_vertex_with(super_triangle)1011 if shares_vertex:12 # Delete it! It was just scaffolding.13 continue14 else:15 final_triangles.append(triangle)1617 mesh.triangles = final_triangles18 return mesh
The most common use of Delaunay Triangulation in computer graphics is generating Terrain or Topography. LiDAR scanners or drones capture millions of points on the ground. By running Delaunay on those points in 2D (ignoring height), and then lifting the vertices to their true 3D height, we generate a perfect landscape.
5D Meshing:
1# 2.5D Delaunay Elevation2def generate_terrain(point_cloud_2d, elevation_data):3 """Lifts a flat Delaunay mesh into 3D topography."""45 # 1. Run Delaunay on the X,Y coordinates only (ignore Z)6 mesh = run_bowyer_watson(point_cloud_2d)78 # 2. Now that we have perfectly fat triangles, apply the Z heights9 for vertex in mesh.vertices:10 vertex.z = elevation_data.get_height(vertex.x, vertex.y)1112 return mesh