Mesh Welding
When importing raw 3D data (like STL files from CAD or 3D scans), the geometry is often a "Polygon Soup". This means adjacent triangles don't actually share a Vertex ID. They just happen to have vertices placed at the exact same 3D coordinates.
The Seam Problem:
1# Finding Un-welded Seams2def analyze_mesh_seams(mesh):3 """Finds coincident vertices that are not topologically connected."""45 # In a "polygon soup" mesh (like raw STL files),6 # two adjacent triangles might share the exact same 3D coordinate7 # but use different vertex indices.89 # 1. Build topology10 edges = build_half_edges(mesh.faces)1112 # 2. Any edge without a twin is a Naked Edge.13 # If two Naked Edges occupy the exact same physical space,14 # that is a Seam that needs welding.1516 return [e for e in edges if e.twin is None]
To weld vertices, we need to find which ones are very close to each other. Checking every vertex against every other vertex takes O(N²) time, which is too slow for meshes with millions of triangles.
Spatial Hashing:
Tolerance. 3. We now only have to compare vertices that landed in the same or adjacent buckets!1# Spatial Hashing for Fast Lookups2def create_spatial_hash(vertices, cell_size=0.01):3 """Sorts vertices into a 3D grid to avoid O(N^2) comparisons."""4 hash_map = {}56 for i, v in enumerate(vertices):7 # Convert continuous 3D coordinate to discrete grid indices8 ix = int(math.floor(v.x / cell_size))9 iy = int(math.floor(v.y / cell_size))10 iz = int(math.floor(v.z / cell_size))1112 key = (ix, iy, iz)1314 if key not in hash_map:15 hash_map[key] = []1617 hash_map[key].append(i) # Store the vertex index1819 return hash_map
Once vertices are sorted into buckets, the algorithm iterates through each bucket. If two vertices are closer to each other than the user-defined Tolerance, they are flagged to be merged into a single vertex.
Distance Check:
(x,y,z). 2. Measure the exact 3D Euclidean distance between every pair of vertices inside it. 3. If Distance <= Tolerance, record the index pair.1# Proximity Matching2def find_matches(hash_map, vertices, tolerance):3 """Finds which vertices should be welded based on distance."""4 matches = []56 for bucket, indices in hash_map.items():7 # Compare points only within the same bucket8 for i in range(len(indices)):9 for j in range(i + 1, len(indices)):10 v1_idx = indices[i]11 v2_idx = indices[j]1213 dist = vertices[v1_idx].distance_to(vertices[v2_idx])1415 if dist <= tolerance:16 matches.append((v1_idx, v2_idx))1718 return matches
When two vertices are matched, we do not physically "glue" them. Instead, we performIndex Re-mapping. We tell the triangle that was using Vertex B to now use Vertex A. Vertex B is then deleted from memory.
Topological Merge:
1# Remapping Face Indices2def merge_vertices(mesh, matches):3 """Updates faces to point to a single shared vertex."""45 # Create a map showing which old index points to which new index6 # e.g., index_map[5] = 2 means "vertex 5 has been merged into vertex 2"7 index_map = build_index_map(matches)89 # Go through every face, and update its indices10 for face in mesh.faces:11 face[0] = index_map.get(face[0], face[0])12 face[1] = index_map.get(face[1], face[1])13 face[2] = index_map.get(face[2], face[2])1415 # Delete the unused vertices from memory16 mesh.vertices = remove_unused_vertices(mesh.vertices, index_map)
A dangerous side effect of Vertex Welding is that if your Tolerance is set too high, you might merge two vertices that belong to the same triangle.
Degenerate Collapse:
v0 == v1 or Area is zero.1# Removing Degenerate Faces2def clean_degenerate_faces(mesh):3 """Removes faces that collapsed into lines or points after welding."""4 valid_faces = []56 for face in mesh.faces:7 # Check if any two indices in the triangle are exactly the same8 if face[0] == face[1] or face[1] == face[2] or face[2] == face[0]:9 # This triangle has collapsed! Skip it.10 continue1112 # Optional: Check if the 3D area is zero13 area = calculate_triangle_area(mesh.vertices[face[0]],14 mesh.vertices[face[1]],15 mesh.vertices[face[2]])16 if area > 0.00001:17 valid_faces.append(face)1819 mesh.faces = valid_faces
The primary visual benefit of welding a mesh is achieving smooth shading across seams. Before welding, the renderer sees two separate vertices at the seam, and assigns them hard, flat normals.
Smooth Shading:
1# Recalculating Normals2def compute_vertex_normals(mesh):3 """Averages face normals to create smooth shading."""45 # Reset all vertex normals to zero6 for v in mesh.vertices:7 v.normal = Vector3(0,0,0)89 # Add each face's normal to its vertices10 for face in mesh.faces:11 face_normal = calculate_face_normal(face)12 mesh.vertices[face[0]].normal += face_normal13 mesh.vertices[face[1]].normal += face_normal14 mesh.vertices[face[2]].normal += face_normal1516 # Normalize the final sums17 for v in mesh.vertices:18 v.normal = v.normal.normalize()
In a real 3D application, you control the Weld Tolerance. If the tolerance is too small, seams remain unwelded. If it is too large, you might accidentally weld distinct features together, collapsing geometry and destroying details (a process that actually forms the basis of "Mesh Decimation").
Welding Tool:
1# Automated Welding Workflow2def weld_mesh(mesh, tolerance=0.01):3 """The complete welding pipeline."""45 # 1. Spatial Hashing for performance6 buckets = create_spatial_hash(mesh.vertices, cell_size=tolerance*2)78 # 2. Find points within tolerance9 matches = find_matches(buckets, mesh.vertices, tolerance)1011 # 3. Update face indices and delete duplicates12 merge_vertices(mesh, matches)1314 # 4. Remove zero-area degenerate triangles15 clean_degenerate_faces(mesh)1617 # 5. Recalculate topology graph and vertex normals18 mesh.rebuild_topology()19 compute_vertex_normals(mesh)2021 return mesh