Manifold Geometry
Edge Rule:
1# Checking for Manifold Edges2def check_watertight(mesh):3 """A mesh is watertight if EVERY edge is shared by exactly 2 faces."""4 edge_counts = {}56 for face in mesh.faces:7 # Get the 3 edges of the triangle8 edges = get_edges_from_face(face)9 for e in edges:10 # Sort vertex indices to ensure consistency (e.g. edge(1,2) == edge(2,1))11 sorted_edge = tuple(sorted(e))12 if sorted_edge in edge_counts:13 edge_counts[sorted_edge] += 114 else:15 edge_counts[sorted_edge] = 11617 # Check if any edge has 1 or >2 faces18 for edge, count in edge_counts.items():19 if count == 1:20 return False, "Hole found!"21 if count > 2:22 return False, "Non-manifold geometry found!"2324 return True, "Mesh is Watertight"
In 3D geometry, a mesh is "Manifold" if it resembles a physically realizable surface. Specifically, a watertight manifold means the surface has no holes, no self-intersections, and encloses a solid volume.
Half-Edge Structure:
twin (the Half-Edge on the adjacent face). 4. If a Half-Edge has no twin, it's a boundary (hole).1# Topology as a Graph2class HalfEdge:3 def __init__(self, vertex, face):4 self.vertex = vertex # Pointing to vertex5 self.face = face # Belonging to face6 self.next = None # Next HalfEdge in face7 self.twin = None # Opposite HalfEdge in adjacent face
To efficiently analyze and repair topology, 3D software doesn't just store a raw list of triangles. It builds a Connectivity Graph (often using the Half-Edge data structure) which links vertices, edges, and faces together, allowing instant traversal across the surface.
Twin Mapping:
A->B in a dictionary. 3. If the dictionary already contains B->A, link them as Twins. 4. If A->B is already in the dictionary, the mesh is Non-Manifold!1# Mapping Edges to Find Twins2def build_half_edges(faces):3 """Creates the Half-Edge graph from a list of faces."""4 edge_map = {}56 for face in faces:7 for i in range(3):8 v0 = face[i]9 v1 = face[(i+1)%3]1011 # The directed edge (v0 -> v1)12 edge = HalfEdge(vertex=v1, face=face)1314 # Look for the opposite edge (v1 -> v0)15 twin_key = (v1, v0)16 if twin_key in edge_map:17 # We found its twin!18 twin = edge_map[twin_key]19 edge.twin = twin20 twin.twin = edge21 else:22 # Store this edge so its twin can find it later23 edge_map[(v0, v1)] = edge2425 return edge_map
To build the connectivity graph, algorithms scan through every triangle and create directed "Half-Edges". By using a hash map where the key is the ordered pair of vertices (v0, v1), it can instantly check if the opposite edge (v1, v0) already exists.
Loop Traversal:
twin == None. 2. Pick a naked edge, and look at the vertex it points to. 3. Find the next naked edge that originates from that vertex. 4. Repeat until you return to the start, forming a continuous Boundary Loop.1# Finding Boundary Loops2def extract_boundary_loops(half_edges):3 """Finds all closed loops of naked edges (holes)."""4 loops = []5 visited = set()67 # 1. Find all naked edges (no twin)8 naked_edges = [e for e in half_edges if e.twin is None]910 for edge in naked_edges:11 if edge in visited:12 continue1314 # 2. Walk the loop15 current = edge16 loop = []1718 while current not in visited:19 visited.add(current)20 loop.append(current)2122 # To find the next naked edge in the loop, we find the next23 # outgoing edge from current.vertex that is also naked.24 # In a valid manifold, there is exactly 1 such edge.25 next_naked = find_next_naked_edge(current.vertex, naked_edges)26 current = next_naked2728 loops.append(loop)2930 return loops
Once the Half-Edge graph is built, identifying holes in the mesh becomes trivial: any Half-Edge that does not have a twin is a "Naked Edge" lying on a boundary.
Vertex Rule:
1# Checking Vertex Manifoldness2def is_vertex_manifold(vertex, connected_faces):3 """A vertex is manifold if its connected faces form a single continuous strip/fan."""45 # 1. Build a local graph of the faces sharing this vertex6 # 2. Start at one face, and walk to adjacent faces that share an edge AND this vertex7 # 3. Count how many distinct "fans" or "components" you find89 components = count_connected_components(connected_faces, shared_vertex=vertex)1011 # If the faces touch ONLY at the vertex (forming an hourglass shape),12 # there will be >1 component, meaning it is Non-Manifold.13 return components == 1
A mesh can pass the "Watertight Edge Rule" but still be non-manifold! If two distinct parts of a mesh touch only at a single vertex (like two pyramids meeting point-to-point), that vertex is Non-Manifold.
Euler Formula: V - E + F = 2:
V: Total Vertices. 2. E: Total Unique Edges. 3. F: Total Faces. 4. If X != 2, the mesh either has boundaries (holes), tunnels (like a donut/torus), or is non-manifold.1# Euler-Poincaré Characteristic2def calculate_euler_characteristic(mesh):3 """Calculates the Euler characteristic (X) of a mesh."""45 V = len(mesh.vertices)6 E = len(mesh.get_unique_edges())7 F = len(mesh.faces)89 # X = V - E + F10 euler_chi = V - E + F1112 # For a closed surface (sphere topology), X should equal 213 # For a surface with 'g' holes (torus), X = 2 - 2g14 # If the mesh has 'b' boundary loops, X = 2 - 2g - b1516 return euler_chi, V, E, F
One of the most profound mathematical laws governing 3D topology is the Euler Characteristic. For any closed, watertight manifold mesh without holes or tunnels (topologically equivalent to a sphere), the number of Vertices minus Edges plus Faces will always equal 2.
Diagnostic Pass:
1# Diagnostics & Repair Workflow2def diagnose_mesh(mesh):3 """Checks all manifold conditions and reports errors."""45 # 1. Build Graph6 edges = build_half_edges(mesh.faces)78 # 2. Find Naked Edges (Holes)9 naked = [e for e in edges if e.twin is None]10 if len(naked) > 0:11 print(f"Mesh has {len(naked)} naked edges. Requires Hole Filling.")1213 # 3. Find Non-Manifold Edges14 nm_edges = find_non_manifold_edges(mesh)15 if len(nm_edges) > 0:16 print("Mesh has non-manifold edges. Requires topological repair.")1718 # 4. Check Euler19 X, V, E, F = calculate_euler_characteristic(mesh)20 print(f"Euler Characteristic: {X}")2122 return len(naked) == 0 and len(nm_edges) == 0
Before running computationally expensive boolean operations (like intersecting two meshes) or sending a file to a 3D printer, software must run a full diagnostic pass to ensure the geometry is a perfectly watertight manifold.
Related Concepts
Sweeps, Revolves, and Curve Networks.
Rail and profile splines, Frenet vs Parallel Transport frames, profile scaling, skinning, and end caps.
Surfaces of revolution, Rodrigues axis rotation, latitude-longitude quad meshing, and seam welding.