Marching Cubes¶
Marching Cubes extracts a triangular mesh from a scalar field. It examines each cell in the grid and generates triangles where the iso-surface passes through.
import torch
import isoext
from _viz import show_mesh
# Setup: create a grid with a sphere
grid = isoext.UniformGrid([64, 64, 64])
points = grid.get_points()
grid.set_values(points.norm(dim=-1) - 0.7)
Running cmake --build & --install in /home/gcnick/Documents/code/isoext/build/cp312-abi3-linux_x86_64
Basic Usage¶
vertices, faces = isoext.marching_cubes(grid, level=0.0, method="nagae")
grid: AUniformGridorSparseGridwith values setlevel: The iso-value to extract (default: 0.0)method: Algorithm variant ("nagae"or"lorensen")
vertices, faces = isoext.marching_cubes(grid)
print(f"Vertices: {vertices.shape}") # (N, 3) float32
print(f"Faces: {faces.shape}") # (M, 3) uint32
show_mesh(vertices, faces)
Vertices: torch.Size([9168, 3])
Faces: torch.Size([18332, 3])
Algorithm Variants¶
nagae (default)¶
Uses only rotations to handle ambiguous cases, producing watertight meshes without holes or cracks.
From: “Surface construction and contour generation from volume data” (1991)
lorensen¶
The original Marching Cubes algorithm. Uses rotations and reflections, which can create ambiguities leading to small holes in the mesh.
From: “Marching cubes: A high resolution 3D surface construction algorithm” (1987)
v_nagae, f_nagae = isoext.marching_cubes(grid, method="nagae")
v_lorensen, f_lorensen = isoext.marching_cubes(grid, method="lorensen")
print(f"Nagae: {f_nagae.shape[0]:,} triangles")
print(f"Lorensen: {f_lorensen.shape[0]:,} triangles")
Nagae: 18,332 triangles
Lorensen: 18,332 triangles
Iso-Level¶
The level parameter controls which iso-surface to extract. For signed distance fields, level=0 gives the surface. Other values give offset surfaces:
# Extract at different iso-levels
for level in [-0.2, 0.0, 0.2]:
v, f = isoext.marching_cubes(grid, level=level)
print(f"level={level:+.1f}: {v.shape[0]:,} vertices (radius ≈ {0.7 - level:.1f})")
level=-0.2: 4,728 vertices (radius ≈ 0.9)
level=+0.0: 9,168 vertices (radius ≈ 0.7)
level=+0.2: 15,072 vertices (radius ≈ 0.5)
Saving Meshes¶
Use write_obj to save the mesh to an OBJ file:
vertices, faces = isoext.marching_cubes(grid)
isoext.write_obj("mesh.obj", vertices, faces)
print("Saved to mesh.obj")
Saved to mesh.obj
Lookup tables¶
The lookup tables that drive marching cubes are generated by luts/gen_mc_lut.py. This script:
Reads base cases from a JSON file (e.g.,
luts/mc_methods/nagae.json)Generates all 256 cases via rotations (and optionally reflections)
Outputs lookup tables and debug meshes
You can use this as a reference for:
Understanding the algorithm: See how base cases expand to cover all configurations
Implementing your own variant: Create a new JSON file with your base cases
Debugging: The script outputs OBJ files for each case to visualize the triangulations
# Generate lookup tables for a marching cubes variant
python luts/gen_mc_lut.py luts/mc_methods/nagae.json output_dir/