Marching Tetrahedra

Marching tetrahedra splits every cell into six tetrahedra and extracts the surface per tetrahedron. No sign pattern of a tetrahedron has more than one valid triangulation, so the mesh comes out closed and consistent by construction. The price is roughly 2-3x more triangles than marching cubes, with worse triangle shapes and a slight directional grain from the cell split. The method goes back to Doi and Koide [1991].

import torch
import isoext
from isoext import viewer
from isoext.sdf import SphereSDF

Basic Usage

The interface matches the other extraction methods: a grid with scalar values in, vertex and face tensors out.

grid = isoext.UniformGrid([32, 32, 32])
grid.set_values(SphereSDF(radius=0.7)(grid.get_points()))

vertices, faces = isoext.marching_tetrahedra(grid)
print(f"marching_tetrahedra: {faces.shape[0]:,} triangles")

v_mc, f_mc = isoext.marching_cubes(grid)
print(f"marching_cubes:      {f_mc.shape[0]:,} triangles")

viewer.embed(vertices, faces, color="tomato", flat_shading=True)
marching_tetrahedra: 13,416 triangles
marching_cubes:      4,508 triangles

How It Works

Each cube is split into six tetrahedra that all share the cube’s main diagonal. The purple lines below are the extra edges the split introduces: one diagonal per face, plus the main diagonal itself:

Hide code cell source

cell = isoext.UniformGrid([2, 2, 2])
cell.set_values(torch.tensor(
    [-0.5, 0.3, 0.4, 0.5, 0.6, 0.4, 0.3, 0.2], device="cuda"
).reshape(2, 2, 2))

corners = cell.get_points().reshape(8, 3)
tets = [[0, 4, 6, 7], [0, 5, 4, 7], [0, 6, 2, 7],
        [0, 2, 3, 7], [0, 1, 5, 7], [0, 3, 1, 7]]  # the split used by src/mt.cu
edges = {tuple(sorted((t[i], t[j]))) for t in tets for i in range(4) for j in range(i + 1, 4)}
diagonals = torch.stack([torch.stack([corners[a], corners[b]])
                         for a, b in edges if bin(a ^ b).count("1") > 1])
viewer.embed(grid=cell, height=300,
             draw=lambda s: viewer.add_lines(s, diagonals, color="orchid"))

A tetrahedron has 4 corners and only 16 sign patterns, each producing one or two triangles with no choices to make, so the mesh is consistent by construction. The price shows on a field with one corner inside: the surface also crosses the diagonals, and marching cubes’ single triangle becomes six:

v, f = isoext.marching_tetrahedra(cell)
print(f"{f.shape[0]} triangle(s)")
viewer.embed(v, f, color="mediumpurple", flat_shading=True, side="double", height=300,
             grid=cell, draw=lambda s: viewer.add_lines(s, diagonals, color="orchid"))
6 triangle(s)

Tessellation

The wireframes on a coarse grid show where the extra triangles go and the diagonal grain left by the six-tetrahedra split. Marching cubes first, marching tetrahedra second.

coarse = isoext.UniformGrid([16, 16, 16])
coarse.set_values(SphereSDF(radius=0.7)(coarse.get_points()))

v, f = isoext.marching_cubes(coarse)
viewer.embed(v, f, color="steelblue", wireframe=True)
v, f = isoext.marching_tetrahedra(coarse)
viewer.embed(v, f, color="tomato", wireframe=True)

When to Use It

Pick marching tetrahedra when guaranteed topology matters more than triangle count, for example as input to solvers or simplification passes that assume a closed manifold. For visual quality at the same budget, marching cubes gives cleaner triangles.

References

[1]

Akio Doi and Akio Koide. An efficient method of triangulating equi-valued surfaces by using tetrahedral cells. IEICE Transactions on Information and Systems, E74-D(1):214–224, 1991.