Surface Nets¶
Surface nets (Gibson [1998]) is a dual method: instead of cutting triangles out of each cell the way marching cubes does, it places one vertex inside every cell the surface crosses and connects the vertices of neighboring cells. It needs no normals and has no parameters to tune, and the mesh comes out smooth and evenly tessellated.
import isoext
from isoext import viewer
from isoext.sdf import CuboidSDF, MandelbulbSDF, get_sdf_normal
from isoext.utils import gaussian_smooth
Basic Usage¶
vertices, faces = isoext.surface_nets(grid, level=0.0)
grid: AUniformGridorSparseGridwith values setlevel: The iso-value to extract (default: 0.0)intersection: Optional precomputed edge crossings fromget_intersection; computed automatically when omitted
grid = isoext.UniformGrid([64, 64, 64])
grid.set_values(grid.get_points().norm(dim=-1) - 0.8) # Sphere
vertices, faces = isoext.surface_nets(grid)
print(f"{vertices.shape[0]:,} vertices, {faces.shape[0]:,} triangles")
11,954 vertices, 23,904 triangles
How It Works¶
One vertex goes inside each cell the surface crosses, at the centroid of the points where the surface crosses the cell’s edges. The connectivity comes from the crossed edges: every crossed edge is surrounded by four cells, and their four vertices are joined into a quad that straddles the edge.
Below, a flat field crosses every vertical edge of a 2x2x1 block of cells (gold dots). Only the central edge has four cells around it, so the mesh is exactly one quad, two triangles, connecting the four cell vertices around that edge. The other crossings sit on boundary edges with fewer neighbors and get no face:
block = isoext.UniformGrid([3, 3, 2], aabb_min=[-1, -1, -0.5], aabb_max=[1, 1, 0.5])
block.set_values(block.get_points()[..., 2]) # a flat surface at z = 0
v, f = isoext.surface_nets(block)
crossings = isoext.get_intersection(block).get_points()
print(f"{f.shape[0]} triangles")
viewer.embed(v, f, color="seagreen", flat_shading=True, side="double", height=300,
grid=block,
draw=lambda s: viewer.add_points(s, crossings, point_size=0.07))
2 triangles
A Smooth Field¶
Because the vertex placement only uses where the surface crosses the cell edges, surface nets works well on fields where normals are unreliable: distance estimators, neural network outputs, or smoothed data. The Mandelbulb below is a distance estimator, and the mesh stays clean without any of the tuning dual contouring would need here:
bulb_grid = isoext.UniformGrid([128] * 3, aabb_min=[-1.2] * 3, aabb_max=[1.2] * 3)
values = MandelbulbSDF(iterations=6)(bulb_grid.get_points())
bulb_grid.set_values(gaussian_smooth(values, sigma=1.0))
v, f = isoext.surface_nets(bulb_grid)
print(f"{v.shape[0]:,} vertices")
viewer.embed(v, f, color="coral")
84,708 vertices
Versus Dual Contouring¶
Surface nets and dual contouring build the same mesh connectivity and differ only in where the vertex goes inside each cell. Dual contouring solves for the point that best fits the surface normals, which can reconstruct sharp edges and corners exactly – provided it gets good normals, here computed from the SDF gradient. Surface nets averages the edge crossings, which rounds them off. The cube below shows the difference. Use dual contouring when the field has sharp features worth preserving and normals you trust; use surface nets otherwise. See Dual Contouring for the details on normals.
cube_grid = isoext.UniformGrid([64] * 3)
cube = CuboidSDF(size=[1.0, 1.0, 1.0])
cube_grid.set_values(cube(cube_grid.get_points()))
its = isoext.get_intersection(cube_grid)
its.set_normals(get_sdf_normal(cube, its.get_points()))
v, f = isoext.dual_contouring(cube_grid, intersection=its)
print(f"dual_contouring: {v.shape[0]:,} vertices")
viewer.embed(v, f, color="goldenrod", flat_shading=True, height=300)
dual_contouring: 6,146 vertices
v, f = isoext.surface_nets(cube_grid)
print(f"surface_nets: {v.shape[0]:,} vertices")
viewer.embed(v, f, color="steelblue", flat_shading=True, height=300)
surface_nets: 6,146 vertices
References¶
Sarah F. F. Gibson. Constrained elastic surface nets: generating smooth surfaces from binary segmented data. In Medical Image Computing and Computer-Assisted Intervention (MICCAI '98), LNCS 1496, 888–898. Springer, 1998. doi:10.1007/BFb0056277.