Initializing 3D Canvas...

Laplacian Smoothing

2 min read1 page

When you 3D scan an object using a laser or an iPhone, the sensor is never perfect. It makes tiny mistakes. The resulting 3D mesh is covered in microscopic spikes and bumps called Noise. We need a way to mathematically iron out the wrinkles without destroying the overall shape.

The Problem with Noise:

1. Noise is high-frequency data. It changes rapidly from one vertex to the next. 2. If we just delete vertices randomly, we get holes. 3. If we use Catmull-Clark Subdivision (from the previous article), we multiply our polygon count by 4, which makes the file size massive, but the noise is still there! 4. We need a Relaxation Algorithm. The most famous one is Laplacian Smoothing.
python
1# Adding Artificial Noise
2def add_noise_to_mesh(mesh, severity):
3 """Simulates a bad 3D scan or rounding errors."""
4
5 for vertex in mesh.vertices:
6
7 # 1. Get the vertex normal (which way is it facing)
8 normal = vertex.normal
9
10 # 2. Pick a random number between -severity and +severity
11 random_shift = random.uniform(-severity, severity)
12
13 # 3. Push the vertex along its normal
14 vertex.position += normal * random_shift
15
16 return mesh
Noise Severity
0.00
2 min read1 page

To smooth out a spike in the mesh, the algorithm needs to look at the geometry immediately surrounding that spike. In topology, this immediate surrounding area is called the 1-Ring Neighborhood.

The Umbrella:

1. Pick any Vertex on the mesh. This is our target. 2. Look at all the Edges that connect directly to that target vertex (like the metal spokes connecting to the center of an umbrella). 3. The vertices at the other ends of those edges are the Neighbors. 4. By averaging the positions of these neighbors, the algorithm can figure out where the target vertex should be if it wasn't spiking outward.
python
1# Finding the 1-Ring Neighbors
2def get_adjacent_vertices(mesh, target_vertex):
3 """Who is directly connected to this point?"""
4
5 neighbors = []
6
7 # Check every edge in the entire mesh
8 for edge in mesh.edges:
9
10 # If our target vertex is on one side of the edge...
11 if edge.v1 == target_vertex:
12 # ...the vertex on the other side is a neighbor!
13 neighbors.append(edge.v2)
14
15 elif edge.v2 == target_vertex:
16 neighbors.append(edge.v1)
17
18 return neighbors
Highlight (Target / 1-Ring Neighbors)
0.00
2 min read1 page

Now that we know who the neighbors are, we can calculate exactly how "spiky" our target vertex is. We do this by calculating a 3D arrow called the Laplacian Vector.

The Pulling Force:

1. Calculate the exact centroid (average) position of the 1-Ring Neighbors. 2. Draw a 3D arrow starting at our target vertex and pointing directly at that centroid. 3. The length of this arrow tells us how bad the spike is. If the vertex is already perfectly smooth with its neighbors, the arrow has a length of 0. 4. The direction of this arrow tells us exactly which way we need to push the vertex to fix the spike!
python
1# Calculating the Laplacian Vector
2def calculate_laplacian(vertex, neighbors):
3 """How far away is the vertex from its ideal smooth position?"""
4
5 # 1. Find the centroid (average) of all the neighbors
6 total_x = sum([n.x for n in neighbors])
7 total_y = sum([n.y for n in neighbors])
8 total_z = sum([n.z for n in neighbors])
9
10 count = len(neighbors)
11 centroid = Point(total_x/count, total_y/count, total_z/count)
12
13 # 2. Draw a 3D arrow from the target vertex TO that centroid
14 # This arrow is the Laplacian Vector!
15 laplacian_vector = centroid - vertex.position
16
17 return laplacian_vector
Math (Neighbors / Centroid / Vector)
0.00
2 min read1 page

Once we have the Laplacian Vector for every vertex in the mesh, we simply push the vertices along those arrows.

The Lambda Factor:

1. If we pushed every vertex all the way to the end of its arrow (the centroid), the entire 3D model would instantly collapse and flatten itself out like a deflated balloon. 2. Instead, we use a fractional weight called Lambda (λ). 3. A Lambda of 0.1 means the vertex only moves 10% of the way down the arrow. 4. By doing this iteratively (running the math 10 or 20 times in a row with a small Lambda), the mesh smoothly relaxes over time without collapsing instantly!
python
1# Smoothing the Vertex
2def update_vertex_position(vertex, laplacian_vector, lambda_factor):
3 """Moving the vertex along the arrow."""
4
5 # We do NOT move the vertex all the way to the centroid!
6 # That would instantly flatten out the entire 3D model.
7 # Instead, we only move it a tiny fraction of the way.
8
9 # lambda_factor is usually a tiny number like 0.1
10 movement = laplacian_vector * lambda_factor
11
12 # Update the position!
13 vertex.position = vertex.position + movement
Lambda Weight %
0.00
2 min read1 page

There is a major flaw with standard Laplacian Smoothing: it shrinks the model! Because every spike is pulled inward, if you run the algorithm 100 times, a majestic 3D dragon will shrink into a tiny, smooth pebble.

Taubin Smoothing:

1. In 1995, Gabriel Taubin invented a brilliant fix for the shrinkage problem. 2. Step 1: Run a normal Laplacian Smooth with a positive Lambda (e.g., 0.33). This removes the noise, but shrinks the volume. 3. Step 2: Run a Reverse Laplacian Smooth with a slightly stronger negative Lambda (called Mu, e.g., -0.34). 4. The reverse step acts like inflation. It perfectly restores the lost volume of the model, while magically leaving the high-frequency noise destroyed!
python
1# Taubin Smoothing (Preserving Volume)
2def taubin_smooth(mesh, iterations):
3 """The push-and-pull method."""
4
5 # Positive lambda (pushes inward)
6 lambda_factor = 0.33
7
8 # Negative mu (pushes outward!)
9 mu_factor = -0.34
10
11 for i in range(iterations):
12
13 # Step 1: Standard Laplacian Smooth (shrinks the mesh)
14 mesh = apply_laplacian(mesh, lambda_factor)
15
16 # Step 2: Reverse Laplacian Smooth (inflates the mesh!)
17 mesh = apply_laplacian(mesh, mu_factor)
18
19 return mesh
Iterations
0.00
2 min read1 page

How does the computer actually know if a mesh is "noisy" or "smooth"? We can use the exact same Laplacian math to evaluate and score the quality of the 3D model.

Visualizing the Error:

1. We calculate the Laplacian Vector for every vertex. 2. If the arrow is long, it means the vertex is spiking far away from its neighbors (High Error). 3. We can color-code the mesh based on this error length. Smooth areas become Blue, and noisy/spiky areas become Red. 4. By summing up all the arrow lengths, we get a single number representing the overall "Bumbpiness Score" of the entire model.
python
1# Calculating the Error Metric
2def evaluate_mesh_smoothness(mesh):
3 """How bumpy is this mesh overall?"""
4
5 total_error = 0
6
7 for vertex in mesh.vertices:
8
9 # We calculate the Laplacian Vector (the arrow to the centroid)
10 laplacian_vector = calculate_laplacian(vertex)
11
12 # The length of that arrow is the "Error" for this vertex
13 error = length(laplacian_vector)
14
15 # Add it to the total score
16 total_error += error
17
18 # Average it out
19 average_error = total_error / len(mesh.vertices)
20
21 return average_error
Noise Severity
0.00
2 min read1 page

When applied to a complex geometry like a 3D Scan, Laplacian Smoothing acts exactly like a Blur filter in Photoshop, but for 3D coordinates instead of 2D pixels.

The Denoising Pipeline:

1. Input: A high-resolution, noisy 3D scan from a LiDAR sensor or Photogrammetry. 2. Topology: The algorithm dynamically maps out the 1-Ring adjacency graph for all 100,000+ vertices. 3. Taubin Relaxation: It iteratively pushes and pulls every single point based on the Laplacian vectors, perfectly preserving the overall volume and shape features while completely destroying the high-frequency spikes. 4. Result: A beautiful, clean, 3D-printable mesh.
python
1# Full Smoothing Pipeline
2def repair_scan_data(mesh, taubin_iterations=10):
3 """Clean up a noisy 3D scan for 3D Printing."""
4
5 print("Initial Noise Level:", evaluate_mesh_smoothness(mesh))
6
7 # 1. We run Taubin smoothing to protect the volume
8 cleaned_mesh = taubin_smooth(mesh, taubin_iterations)
9
10 print("Final Noise Level:", evaluate_mesh_smoothness(cleaned_mesh))
11
12 # 2. Re-calculate lighting normals for the new smooth surface
13 cleaned_mesh.calculate_smooth_normals()
14
15 return cleaned_mesh
Taubin Smoothing Levels
0.00