Marching Cubes
The Isosurface:
0.5). 3. We want to draw a 3D skin exactly where the grid's value equals 0.5. 4. The algorithm "marches" through every single cube in the grid, one by one. 5. If it finds a cube where the skin should pass through, it generates 1 to 5 tiny triangles inside that cube.1# Scalar Fields to Meshes2def extract_isosurface(voxel_grid, threshold):3 """The magic behind Medical MRI scans and Minecraft mods."""45 mesh = Mesh()67 # 1. Loop through every single box in the grid8 for voxel in voxel_grid:9 # 2. Does the surface pass through this specific box?10 if voxel_contains_surface(voxel, threshold):1112 # 3. If yes, generate triangles for just this box!13 triangles = marching_cubes(voxel)14 mesh.add(triangles)1516 return mesh
Voxels are great for physics and storage, but they look terrible. To make a game look realistic, we need to convert the blocky grid back into a smooth, continuous polygon mesh. The industry standard algorithm for this is called Marching Cubes.
The 8-Bit Index:
10010110). 4. An 8-bit number has exactly 256 possible values (from 0 to 255). 5. This means there are exactly 256 possible ways a surface can intersect a cube!1# Classifying the 8 Corners2def get_cube_index(cube_corners, threshold):3 """Generates an 8-bit binary number (0 to 255)."""45 cube_index = 067 # Check all 8 corners of the cube8 for i in range(8):9 corner_value = cube_corners[i].value1011 # If the corner is INSIDE the surface12 if corner_value < threshold:13 # Flip the i-th bit to 1!14 # Example: 1 << 0 = 1, 1 << 1 = 2, 1 << 2 = 4...15 cube_index |= (1 << i)1617 return cube_index
To figure out what the triangles should look like inside a single cube, we only need to look at the cube's8 Corners. The algorithm uses clever Binary Math (Bitwise Operations) to classify the cube lightning-fast.
Pre-computed Answers:
142). 4. Instead of doing math, the computer just reads Array[142] and instantly gets the list of cube edges that need to be connected to form the triangles!1# The Lookup Table (LUT)2def lookup_triangles(cube_index):3 """Avoids calculating geometry in real-time."""45 # We pre-calculate all 256 possible triangle configurations6 # and store them in a massive Array/Dictionary when the game loads.78 # TRIANGLE_TABLE[0] = [] # Completely outside, no triangles9 # TRIANGLE_TABLE[255] = [] # Completely inside, no triangles10 # TRIANGLE_TABLE[1] = [ [0, 8, 3] ] # One corner cut off1112 # We simply use our 8-bit index to look up the answer instantly!13 intersected_edges = TRIANGLE_TABLE[cube_index]1415 return intersected_edges
Calculating exactly where a plane intersects a cube requires heavy 3D math. If we do that for 1 million cubes 60 times a second, the computer will crash. Instead, we cheat.
Linear Interpolation (Lerp):
0.1. Corner B has a value of 0.9. 3. Our Isosurface Threshold is 0.5. 4. Since 0.5 is exactly halfway between 0.1 and 0.9, we place the vertex exactly in the middle of the physical edge! 5. If Corner A was 0.4 instead, the threshold (0.5) would be much closer to Corner A. We slide the vertex closer to Corner A. This creates perfectly smooth curves!1# Finding the Exact Intersection2def interpolate_edge(corner_A, corner_B, threshold):3 """Slides the vertex along the edge for a perfect fit."""45 # 1. Get the scalar values at both ends of the edge6 val_A = corner_A.value7 val_B = corner_B.value89 # 2. How far along the edge is our threshold?10 # e.g., if A=0.2, B=0.8, and threshold=0.5, then ratio = 0.5 (exactly halfway)11 ratio = (threshold - val_A) / (val_B - val_A)1213 # 3. Use that ratio to mix the XYZ coordinates14 x = corner_A.x + ratio * (corner_B.x - corner_A.x)15 y = corner_A.y + ratio * (corner_B.y - corner_A.y)16 z = corner_A.z + ratio * (corner_B.z - corner_A.z)1718 return Point(x, y, z)
The Lookup Table tells us which edges the skin passes through. But it doesn't tell us whereon the edge it passes through. If we just place vertices at the exact center of every edge, our mesh will look jagged (like Minecraft).
Winding Order:
1# Connecting the Dots2def generate_cube_geometry(intersected_edges, corner_values):3 """Turns the edge dots into solid triangles."""45 triangles = []67 # The Lookup Table returns edges in sets of 3.8 # e.g., [Edge_0, Edge_8, Edge_3, Edge_1, Edge_9, Edge_2]910 # We step through the list 3 edges at a time11 for i in range(0, len(intersected_edges), 3):1213 # Interpolate exact XYZ for the 3 edges14 pt1 = interpolate_edge(intersected_edges[i])15 pt2 = interpolate_edge(intersected_edges[i+1])16 pt3 = interpolate_edge(intersected_edges[i+2])1718 # Connect them to form a triangle!19 triangles.append(Triangle(pt1, pt2, pt3))2021 return triangles
We now have the exact XYZ coordinates of all the intersection points floating on the edges of our cube. The final step inside this cube is to connect those dots to form physical polygon Triangles.
Central Differences:
1# Calculating Smooth Normals2def calculate_normal(x, y, z):3 """How to make a jagged mesh look perfectly smooth."""45 # 1. We look at the grid values immediately around our point6 # We check +X and -X, +Y and -Y, +Z and -Z78 # 2. We calculate the difference (Gradient)9 nx = value_at(x - 1, y, z) - value_at(x + 1, y, z)10 ny = value_at(x, y - 1, z) - value_at(x, y + 1, z)11 nz = value_at(x, y, z - 1) - value_at(x, y, z + 1)1213 # 3. Combine them into a Vector and Normalize it (length = 1.0)14 normal = Vector3(nx, ny, nz).normalize()1516 return normal
If we just stop at generating triangles, the mesh will look faceted and low-poly, because the lighting will be perfectly flat across each triangle. We want it to look buttery smooth. We do this by calculatingVertex Normals based on the grid data.
The Geometry Pipeline:
1# Full Marching Cubes Pipeline2def generate_bunny_mesh(voxel_grid, threshold):3 """From blocky Voxels back to a smooth Mesh."""45 mesh = Mesh()67 # Run the algorithm on the grid8 for voxel in voxel_grid:9 # Get the 8 corner values10 corners = get_corners(voxel)1112 # Get the 8-bit index13 idx = get_cube_index(corners, threshold)1415 # Look up which edges are cut16 edges = lookup_triangles(idx)1718 # Interpolate exact points and form triangles19 triangles = generate_cube_geometry(edges, corners)2021 mesh.add(triangles)2223 # Calculate smooth lighting normals24 mesh.calculate_smooth_normals()2526 return mesh
We have reached the end of the pipeline. We started with a Point Cloud, discretized it intoVoxels, mapped it to a solid Scalar Field, and finally used Marching Cubesto extract a manifold 3D polygon mesh of the Stanford Bunny.
Related Concepts
Tangents, Normals, and Curvatures.
Parametric cubic Bézier curves, Bernstein basis derivatives, unit tangent vectors, and arc-length speed profiles.
Frenet-Serret orthonormal frames, curvature and torsion, inflection flipping, and Bishop parallel transport frames.