Convex Hull
Imagine wrapping a rubber band around a pegboard. The rubber band snaps tight, forming theConvex Hull of the pegs. It is the smallest possible convex shape that completely encloses a set of points.
Convex vs Concave:
1# Convex Geometry Concept2def is_convex(shape):3 """A shape is convex if a line between ANY two points stays inside it."""45 for p1 in shape.get_random_points():6 for p2 in shape.get_random_points():78 line = create_line(p1, p2)9 if not shape.contains(line):10 return False1112 return True
To build a Convex Hull algorithm from scratch, we must start with the most foundational math operation in Computational Geometry: The Orientation Check. Given 3 points in a sequence, does the path bend Left or Right?
The 2D Cross Product:
p1 to p2. 2. We want to test a third point p3. 3. The 2D Cross Product returns a single number. 4. A Positive result means p3 is to the Left of the line. A Negative result means it is to the Right.1# 2D Cross Product (Orientation)2def get_turn_direction(p1, p2, p3):3 """Determines if the path p1->p2->p3 turns Left or Right."""45 # This is a specialized 2D cross product of vectors (p2-p1) and (p3-p2)6 cross_product = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x)78 if cross_product > 0:9 return "Left Turn" (Counter-Clockwise)10 elif cross_product < 0:11 return "Right Turn" (Clockwise)12 else:13 return "Collinear" (Straight Line)
How do we actually build the hull using the Turn Check? One of the oldest algorithms is the Jarvis March. From our current point on the boundary, we want to find the next point that makes the widest possible "Right Turn".
Iterative Swapping:
1# Finding the next point on the hull2def find_next_hull_point(current_point, all_points):3 """Finds the most 'Rightward' point from the current position."""45 # Start by guessing the first point in the list is the next hull point6 next_point = all_points[0]78 for candidate in all_points:9 if candidate == current_point:10 continue1112 # If the candidate makes a Right Turn compared to our current guess,13 # it is a BETTER guess! Swap it.14 turn = get_turn_direction(current_point, next_point, candidate)1516 if turn == "Right Turn":17 next_point = candidate1819 return next_point
By repeating the Jarvis March sweep, we can "wrap" the entire set of points. We just need a starting point, and a way to know when to stop.
Closing the Loop:
1# Full 2D Convex Hull2def jarvis_march(points):3 """Wraps the points until we hit the start again."""45 hull = []67 # 1. Find the leftmost point (guaranteed to be on the hull)8 start_point = find_leftmost_point(points)9 current_point = start_point1011 while True:12 hull.append(current_point)1314 # 2. Find the next point using our Sweep method15 next_point = find_next_hull_point(current_point, points)1617 # 3. If the next point is our Start Point, we've closed the loop!18 if next_point == start_point:19 break2021 current_point = next_point2223 return hull
In 3D, we can't just connect lines. We have to build a polyhedron (a mesh of triangles). The 3D equivalent of "Gift Wrapping" is the QuickHull algorithm.
Seeing the Horizon:
1# 3D Horizon Edge Detection2def find_horizon(hull_faces, new_point):3 """Finds the 'terminator line' visible from the new point."""45 horizon_edges = []67 for face in hull_faces:8 # A face is 'visible' if the new point is in front of it9 # (checked using the Dot Product of the face normal and the point vector)10 is_visible = is_face_visible(face, new_point)1112 if is_visible:13 for edge in face.edges:14 # If the neighboring face across this edge is NOT visible,15 # then this edge is on the Horizon!16 if not is_face_visible(edge.neighbor_face, new_point):17 horizon_edges.append(edge)1819 return horizon_edges
Once the Horizon is found, the actual geometry update is simple. We delete the old faces that are "inside" the new expanded volume, and build a new cone of triangles to seal the hole.
Stitching the Mesh:
1# QuickHull 3D Step2def add_point_to_hull(mesh, horizon_edges, new_point):3 """Deletes visible faces and builds a new 'cone' to the new point."""45 # 1. Delete all faces that were visible to the lightbulb6 mesh.delete_faces(visible_faces)78 # 2. For every edge on the Horizon loop, create a new triangle9 # connecting that edge to the new point.10 for edge in horizon_edges:11 v0 = edge.start_vertex12 v1 = edge.end_vertex13 v2 = new_point1415 # Ensure the winding order is correct so normals face outward!16 new_face = create_face(v0, v1, v2)17 mesh.add_face(new_face)1819 return mesh
Why do we care about the Convex Hull? In physics simulations (like dropping a Stanford Bunny into a box), calculating collisions against the bunny's true mesh (thousands of concave triangles) is painfully slow.
Collision Proxies:
1# Collision Proxies2def create_physics_hull(mesh):3 """Creates a fast collision shape from a high-poly mesh."""45 # Extract just the raw points from the mesh6 point_cloud = mesh.get_vertices()78 # Run the QuickHull algorithm9 hull_mesh = generate_quickhull(point_cloud)1011 return hull_mesh