Skip to content

Add wp.DualContouring and wp.IsoSurfaceBase interface for swappable isosurface extraction #1614

Description

@christophercrouzet

Summary

Add a new wp.DualContouring module for extracting polygonal meshes from 3D scalar fields, alongside the existing wp.MarchingCubes. Introduce a shared abstract base class wp.IsoSurfaceBase so callers can swap extraction backends without changing their surrounding code — the same pattern Newton uses with ViewerBase to support multiple viewer backends behind one API.

Motivation

  • Same performance as wp.MarchingCubes but with a more organic topology.

Proposed design: wp.IsoSurfaceBase

Model this after Newton's ViewerBase:

  • ViewerBase holds shared state (layers, camera, model caches) and defines the public surface area (set_model, log_state, begin_frame, …).
  • Concrete backends (UsdViewer, GL viewer, …) subclass it and implement the backend-specific hooks (end_frame, log_mesh, …).
  • Callers depend on ViewerBase, not a specific backend.

Apply the same idea to isosurface extraction:

from abc import ABC, abstractmethod

class IsoSurfaceBase(ABC):
    """Shared context for isosurface mesh extraction from a regular 3D scalar field."""

    nx: int
    ny: int
    nz: int
    domain_bounds_lower_corner: wp.vec3f | tuple | None
    domain_bounds_upper_corner: wp.vec3f | tuple | None

    verts: wp.array(dtype=wp.vec3f) | None   # populated by surface()
    indices: wp.array(dtype=wp.int32) | None  # flat triangle list (MC convention)

    def __init__(self, nx, ny, nz,
                 domain_bounds_lower_corner=None,
                 domain_bounds_upper_corner=None): ...

    def resize(self, nx: int, ny: int, nz: int) -> None: ...

    @abstractmethod
    def surface(self, field: wp.array3d(dtype=wp.float32), threshold: float) -> None:
        """Extract the iso-surface; populate verts and indices."""
        ...

    @classmethod
    @abstractmethod
    def extract(cls, field, threshold=0.0,
                domain_bounds_lower_corner=None,
                domain_bounds_upper_corner=None,
                **kwargs) -> tuple[wp.array, wp.array, ...]:
        """Stateless one-shot extraction."""
        ...

Shared contract (all backends)

Concern Contract
Input wp.array3d(dtype=wp.float32) on a regular grid; shape (nx, ny, nz) must match the context
Threshold Scalar iso-value (typically 0.0 for SDF zero level-set)
Domain scaling Same domain_bounds_lower_corner / domain_bounds_upper_corner semantics as today’s MarchingCubes
Stateful use context = Backend(nx, ny, nz); context.surface(field, threshold); verts, tris = context.verts, context.indices
Stateless use Backend.extract(field, threshold, ...)
Triangle output Flat int32 indices: each group of 3 ints references verts (compatible with wp.render, wp.Mesh, etc.)
Empty surface Return length-0 arrays, never None
Errors ValueError for wrong shape/empty field; TypeError for non-float32 dtype

Backend-specific extensions

Subclasses may expose additional outputs or tuning knobs without breaking the base contract:

  • MarchingCubes(IsoSurfaceBase) — keeps existing lookup-table class attributes (CUBE_CORNER_OFFSETS, CASE_TO_TRI_RANGE, …) and the current deprecation path for max_verts / max_tris / device.
  • DualContouring(IsoSurfaceBase) — adds native quad connectivity (quads: wp.array(dtype=wp.int32, ndim=2) with shape (num_quads, 4)). Triangle indices remain the (0,1,2)+(0,2,3) triangulation of each quad so triangle-only consumers need no changes.

Callers that only need triangles can type-hint against IsoSurfaceBase; callers that need quads use DualContouring directly.

Implementation scope

1. Refactor MarchingCubes onto IsoSurfaceBase

  • Extract shared init / resize / attribute plumbing into warp/_src/iso_surface.py (or similar).
  • Make MarchingCubes a concrete subclass; preserve the public API and deprecations unchanged.
  • Rename static extract_surface_marching_cubesMarchingCubes.extract (keep old name as a deprecated alias if needed).

2. Add DualContouring

  • New module warp/_src/dual_contouring.py.
  • Export as wp.DualContouring from warp/__init__.py.

3. Docs, tests, example

  • docs/api_reference/warp.rst — document IsoSurfaceBase, DualContouring (alongside MarchingCubes).
  • warp/tests/geometry/test_dual_contouring.py — analytic SDF ground truth; model on test_marching_cubes.py.
  • warp/examples/core/example_dual_contouring.py — port the marching-cubes example scene.
  • CHANGELOG entry under [Unreleased] / Added.

Acceptance criteria

  • wp.IsoSurfaceBase exists and documents the shared stateful/stateless contract.

  • wp.MarchingCubes subclasses IsoSurfaceBase with no breaking changes to existing call sites.

  • wp.DualContouring subclasses IsoSurfaceBase and accepts the same grid input + bounds as MarchingCubes.

  • A caller can swap backends with a one-line change:

    Extractor = wp.MarchingCubes  # or wp.DualContouring
    iso = Extractor(nx, ny, nz)
    iso.surface(field, threshold=0.0)
    render(iso.verts, iso.indices)
  • Tests pass on CPU and CUDA (TestDualContouring, existing TestMarchingCubes unchanged).

  • Example registered in test_examples.py.

Non-goals (initially)

  • Differentiable / autodiff-through-topology support (same as MarchingCubes today).
  • Replacing or removing MarchingCubes — both algorithms remain first-class.

References

Metadata

Metadata

Labels

feature requestRequest for something to be added

Fields

No fields configured for Enhancement.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions