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_cubes → MarchingCubes.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
Non-goals (initially)
- Differentiable / autodiff-through-topology support (same as
MarchingCubes today).
- Replacing or removing
MarchingCubes — both algorithms remain first-class.
References
Summary
Add a new
wp.DualContouringmodule for extracting polygonal meshes from 3D scalar fields, alongside the existingwp.MarchingCubes. Introduce a shared abstract base classwp.IsoSurfaceBaseso callers can swap extraction backends without changing their surrounding code — the same pattern Newton uses withViewerBaseto support multiple viewer backends behind one API.Motivation
wp.MarchingCubesbut with a more organic topology.Proposed design:
wp.IsoSurfaceBaseModel this after Newton's
ViewerBase:ViewerBaseholds shared state (layers, camera, model caches) and defines the public surface area (set_model,log_state,begin_frame, …).UsdViewer, GL viewer, …) subclass it and implement the backend-specific hooks (end_frame,log_mesh, …).ViewerBase, not a specific backend.Apply the same idea to isosurface extraction:
Shared contract (all backends)
wp.array3d(dtype=wp.float32)on a regular grid; shape(nx, ny, nz)must match the context0.0for SDF zero level-set)domain_bounds_lower_corner/domain_bounds_upper_cornersemantics as today’sMarchingCubescontext = Backend(nx, ny, nz); context.surface(field, threshold); verts, tris = context.verts, context.indicesBackend.extract(field, threshold, ...)int32indices: each group of 3 ints referencesverts(compatible withwp.render,wp.Mesh, etc.)NoneValueErrorfor wrong shape/empty field;TypeErrorfor non-float32dtypeBackend-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 formax_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 useDualContouringdirectly.Implementation scope
1. Refactor
MarchingCubesontoIsoSurfaceBaseresize/ attribute plumbing intowarp/_src/iso_surface.py(or similar).MarchingCubesa concrete subclass; preserve the public API and deprecations unchanged.extract_surface_marching_cubes→MarchingCubes.extract(keep old name as a deprecated alias if needed).2. Add
DualContouringwarp/_src/dual_contouring.py.wp.DualContouringfromwarp/__init__.py.3. Docs, tests, example
docs/api_reference/warp.rst— documentIsoSurfaceBase,DualContouring(alongsideMarchingCubes).warp/tests/geometry/test_dual_contouring.py— analytic SDF ground truth; model ontest_marching_cubes.py.warp/examples/core/example_dual_contouring.py— port the marching-cubes example scene.[Unreleased] / Added.Acceptance criteria
wp.IsoSurfaceBaseexists and documents the shared stateful/stateless contract.wp.MarchingCubessubclassesIsoSurfaceBasewith no breaking changes to existing call sites.wp.DualContouringsubclassesIsoSurfaceBaseand accepts the same grid input + bounds asMarchingCubes.A caller can swap backends with a one-line change:
Tests pass on CPU and CUDA (
TestDualContouring, existingTestMarchingCubesunchanged).Example registered in
test_examples.py.Non-goals (initially)
MarchingCubestoday).MarchingCubes— both algorithms remain first-class.References
warp/_src/marching_cubes.pynewton/_src/viewer/viewer.py(ViewerBase+ concrete viewers)