Dual Contouring

Dual Contouring extracts a triangle mesh with vertices placed optimally within each cell. Unlike Marching Cubes, it can preserve sharp features when given accurate surface normals.

Simple Usage

For most cases, you can use the simple one-liner interface:

vertices, faces = isoext.dual_contouring(grid, level=0.0)

This automatically computes intersection points and normals from the grid values.

import torch
import isoext
from isoext.sdf import CuboidSDF, get_sdf_normal
from _viz import show_mesh
Running cmake --build & --install in /home/gcnick/Documents/code/isoext/build/cp312-abi3-linux_x86_64
# Simple usage - just call dual_contouring directly
grid = isoext.UniformGrid([64, 64, 64])
cube = CuboidSDF(size=[1.0, 1.0, 1.0])
grid.set_values(cube(grid.get_points()))

# One-liner: normals computed automatically from grid values
vertices, faces = isoext.dual_contouring(grid, level=0.0)

print(f"Vertices: {vertices.shape}")
print(f"Faces: {faces.shape}")

show_mesh(vertices, faces, color="lightblue", smooth_shading=False)
Vertices: torch.Size([6146, 3])
Faces: torch.Size([12288, 3])
_images/f6824002caec5341565488f112638e6f94f93442be38a7ef249b0f11b9e83250.png

Custom Normals for Sharp Features

For shapes with sharp features (like cubes), you can provide custom normals computed from the SDF gradient for better results:

  1. Get intersections — Call get_intersection() to get intersection points

  2. Compute normals — Use SDF gradient for accurate surface normals

  3. Run dual contouring — Pass the intersection with custom normals

# Step 1: Find edge-surface intersections
intersection = isoext.get_intersection(grid, level=0.0)
points = intersection.get_points()
print(f"Intersection shape: {points.shape}")
Intersection shape: torch.Size([24576, 3])
# Step 2: Compute accurate normals using the SDF gradient
normals = get_sdf_normal(cube, points)
intersection.set_normals(normals)

print(f"Normals shape: {normals.shape}")
Normals shape: torch.Size([24576, 3])
# Step 3: Run dual contouring with custom normals
vertices, faces = isoext.dual_contouring(grid, level=0.0, intersection=intersection)

print(f"Vertices: {vertices.shape}")  # (N, 3) float32
print(f"Faces: {faces.shape}")        # (M, 3) int32

show_mesh(vertices, faces, color="salmon", smooth_shading=False)
Vertices: torch.Size([6146, 3])
Faces: torch.Size([12288, 3])
_images/1299278a4fcb88ca592a223f577116075ea6bb1ddbfc986a73d466a029b98fdc.png