Method Comparisons¶
The field is only known at the grid corners, and every extraction method reconstructs the surface from those samples. They all answer the same two questions: where do the mesh vertices go, and how are they connected? Each method’s page explains its own answer. The comparisons below put the answers side by side, on fields of a few cells where every vertex is visible.
import torch
import isoext
from isoext import viewer
from isoext.sdf import get_sdf_normal, project_to_surface
Two Families¶
Every method places its geometry using the edge crossings: wherever a grid edge runs from an inside corner to an outside one, the surface crosses it, at a point interpolated from the two corner values. The primal methods, Marching Cubes and Marching Tetrahedra, put their vertices at the crossings themselves and cut triangles out of each cell by table lookup. The dual methods put one vertex inside each crossed cell – Surface Nets at the centroid of the cell’s crossings, Dual Contouring at the point that best fits the SDF samples or the normals, and Dual Marching Cubes one per surface piece rather than one per cell – and join them with one quad around each crossed edge.
The families are easy to tell apart on the smallest possible sphere. One inside sample on 2x2x2 cells comes out as an octahedron under marching cubes, with its 6 vertices on the grid edges, and as a cube under surface nets, with its 8 vertices inside the cells. The two meshes are duals of each other:
tiny = isoext.UniformGrid([3, 3, 3])
tiny.set_values(tiny.get_points().norm(dim=-1) - 0.7)
v, f = isoext.marching_cubes(tiny)
print(f"marching_cubes: {v.shape[0]} vertices, {f.shape[0]} triangles")
viewer.embed(v, f, wireframe=True, color="orange", height=320, grid=tiny)
marching_cubes: 6 vertices, 8 triangles
v, f = isoext.surface_nets(tiny)
print(f"surface_nets: {v.shape[0]} vertices, {f.shape[0]} triangles")
viewer.embed(v, f, wireframe=True, color="seagreen", height=320, grid=tiny)
surface_nets: 8 vertices, 12 triangles
How the Methods Relate¶
Method |
Vertices |
Faces |
Ambiguities |
|---|---|---|---|
Marching cubes |
on grid edges |
per cell, from a table |
resolved by the variant |
Marching tetrahedra |
on tetrahedron edges |
per tetrahedron |
none |
Surface nets |
one per cell, at the centroid |
one quad per crossed edge |
none |
Dual contouring |
one per cell, fitted to the SDF samples or the normals |
one quad per crossed edge |
none |
Dual marching cubes |
one per patch, centroid or QEF |
one quad per crossed edge |
resolved by the variant |
All of them run on the same grids, share the same edge crossings, and answer the same two questions from the top of the page. The primal methods answer with vertices at the crossings themselves, so the mesh interpolates the samples exactly. The dual methods answer with faces there instead, which tends to give better-shaped triangles and, given SDF samples or good normals, sharp features – at the cost of vertex positions that are estimated rather than interpolated.
The guarantees differ too. Every method produces a closed mesh when
the surface stays inside the grid (lorensen excepted: its tables
can crack). Manifoldness is the primal family’s structural
advantage: every edge borders exactly two triangles and the mesh
never touches itself. The dual methods can fail that in two ways. A
pinch is two surface sheets forced through a single vertex: a cell
crossed by several sheets gets only one vertex in surface nets and
dual contouring, so the sheets meet there in an hourglass. A tunnel
edge is four quads sharing one edge, which happens when the surface
passes through a cell face twice. Dual marching cubes eliminates the
pinches (one vertex per sheet) but keeps the tunnel edges.
The criteria below follow the comparison table of Shen et al. [2023], which surveys these methods as bases for gradient-based mesh optimization; each entry here is backed by a demonstration on this page or a measurement in the test suite.
Method |
Sharp features |
Uniform triangles |
Intersection-free |
Manifold |
|---|---|---|---|---|
Marching cubes |
no |
no |
yes |
yes |
Marching tetrahedra |
no |
no |
yes |
yes |
Surface nets |
no |
yes |
yes |
pinches and tunnel edges |
Dual contouring |
yes |
yes |
no |
pinches and tunnel edges |
Dual marching cubes |
with normals |
yes |
yes |
tunnel edges only |
Dual contouring gets sharp features from normals on the
intersection (ju, the default) or, with its carrera variant, from
the SDF samples alone; dual marching cubes needs the normals, as
shown below. Uniform triangles refers to the triangle shapes: the
marching methods produce slivers wherever a crossing sits close to
a corner (their worst triangles have edges differing by an order of
magnitude), while the dual methods’ vertices sit centrally in their
cells. Intersection-free fails for dual contouring because its
vertices may leave their cells: for ju with clamp=False, and
always for carrera (see Dual Contouring).
Seeing the Failure Cases¶
Each failure fits on a tiny grid. In the scenes below the same field is extracted three ways, side by side: marching cubes in blue on the left, dual contouring in gold in the middle, dual marching cubes in red on the right.
Pinches¶
Three shallow blobs at mutually diagonal corners of the center cell. Marching cubes extracts three separate pieces, and so does dual marching cubes: the center cell holds three patches and gets three vertices. Dual contouring can only give each cell one vertex, so the sheets pinch together into a single piece – it has five fewer vertices than dual marching cubes, and each missing vertex is a pinch point:
t = 1 / 3
blobs = isoext.UniformGrid([4, 4, 4])
p = blobs.get_points()
centers = torch.tensor([[-t, -t, -t], [-t, t, t], [t, -t, t]], device="cuda")
blobs.set_values((p[..., None, :] - centers).norm(dim=-1).amin(-1) - 0.25)
compare(blobs)
marching_cubes 18 vertices, 24 triangles
dual_contouring 19 vertices, 36 triangles
dual_marching_cubes 24 vertices, 36 triangles
Tunnel Edges¶
Two deep blobs on a face diagonal. The bilinear face test joins them through the shared face, so every method extracts one connected dumbbell – marching cubes stays manifold, but for both dual methods the dual edge across that face is used by four quads. This tunnel edge is the one non-manifold configuration dual marching cubes keeps from the dual family:
pair = isoext.UniformGrid([4, 4, 4])
p = pair.get_points()
centers = torch.tensor([[-t, -t, -t], [-t, t, t]], device="cuda")
pair.set_values((p[..., None, :] - centers).norm(dim=-1).amin(-1) - 0.5)
compare(pair)
marching_cubes 12 vertices, 20 triangles
dual_contouring 14 vertices, 24 triangles
dual_marching_cubes 14 vertices, 24 triangles
Performance compares the methods’ speed, and the papers behind every method are collected in References.
References¶
Tianchang Shen, Jacob Munkberg, Jon Hasselgren, Kangxue Yin, Zian Wang, Wenzheng Chen, Zan Gojcic, Sanja Fidler, Nicholas Sharp, and Jun Gao. Flexible isosurface extraction for gradient-based mesh optimization. ACM Transactions on Graphics, 2023. doi:10.1145/3592430.