From fa5db540e4cf53a10847787c98754bbd6f1dbd60 Mon Sep 17 00:00:00 2001 From: Eric Shi Date: Sun, 26 May 2024 17:00:22 -0700 Subject: [PATCH] MarchingCube docs + more runtime checks --- CHANGELOG.md | 1 + docs/modules/runtime.rst | 12 +++++++ warp/types.py | 72 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 505f13e23..3891b164c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add `wp.isnan()`, `wp.isinf()`, and `wp.isfinite()` for scalars, vectors, matrices, etc. - Implicitly initialize Warp when first required - Speed up `omni.warp.core`'s startup time +- Add runtime checks for `wp.MarchingCubes` on field dimensions and size ## [1.1.0] - 2024-05-09 diff --git a/docs/modules/runtime.rst b/docs/modules/runtime.rst index dd60c1249..27ff3e2bf 100644 --- a/docs/modules/runtime.rst +++ b/docs/modules/runtime.rst @@ -1129,6 +1129,18 @@ within a specified bounding box. The kernel is nearly identical to the ray-traversal example, except we obtain ``query`` using :func:`wp.bvh_query_aabb() `. +Marching Cubes +-------------- + +The :class:`wp.MarchingCubes ` class can be used to extract a 2-D mesh approximating an +isosurface of a 3-D scalar field. The resulting triangle mesh can be saved to a USD +file using the :class:`warp.renderer.UsdRenderer`. + +See :github:`warp/examples/core/example_marching_cubes.py` for a usage example. + +.. autoclass:: MarchingCubes + :members: + Profiling --------- diff --git a/warp/types.py b/warp/types.py index 42c42a4ed..9def6538a 100644 --- a/warp/types.py +++ b/warp/types.py @@ -4160,6 +4160,36 @@ def __del__(self): class MarchingCubes: def __init__(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int, device=None): + """CUDA-based Marching Cubes algorithm to extract a 2D surface mesh from a 3D volume. + + Attributes: + id: Unique identifier for this object. + verts (:class:`warp.array`): Array of vertex positions of type :class:`warp.vec3f` + for the output surface mesh. + This is populated after running :func:`surface`. + indices (:class:`warp.array`): Array containing indices of type :class:`warp.int32` + defining triangles for the output surface mesh. + This is populated after running :func:`surface`. + + Each set of three consecutive integers in the array represents a single triangle, + in which each integer is an index referring to a vertex in the :attr:`verts` array. + + Args: + nx: Number of cubes in the x-direction. + ny: Number of cubes in the y-direction. + nz: Number of cubes in the z-direction. + max_verts: Maximum expected number of vertices (used for array preallocation). + max_tris: Maximum expected number of triangles (used for array preallocation). + device (Devicelike): CUDA device on which to run marching cubes and allocate memory. + + Raises: + RuntimeError: ``device`` not a CUDA device. + + .. note:: + The shape of the marching cubes should match the shape of the scalar field being surfaced. + + """ + self.id = 0 self.runtime = warp.context.runtime @@ -4185,7 +4215,7 @@ def __init__(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int, dev from warp.context import zeros self.verts = zeros(max_verts, dtype=vec3, device=self.device) - self.indices = zeros(max_tris * 3, dtype=int, device=self.device) + self.indices = zeros(max_tris * 3, dtype=warp.int32, device=self.device) # alloc surfacer self.id = ctypes.c_uint64(self.alloc(self.device.context)) @@ -4199,7 +4229,19 @@ def __del__(self): # destroy surfacer self.free(self.id) - def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int): + def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int) -> None: + """Update the expected input and maximum output sizes for the marching cubes calculation. + + This function has no immediate effect on the underlying buffers. + The new values take effect on the next :func:`surface` call. + + Args: + nx: Number of cubes in the x-direction. + ny: Number of cubes in the y-direction. + nz: Number of cubes in the z-direction. + max_verts: Maximum expected number of vertices (used for array preallocation). + max_tris: Maximum expected number of triangles (used for array preallocation). + """ # actual allocations will be resized on next call to surface() self.nx = nx self.ny = ny @@ -4207,13 +4249,37 @@ def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int): self.max_verts = max_verts self.max_tris = max_tris - def surface(self, field: array(dtype=float), threshold: float): + def surface(self, field: array(dtype=float, ndim=3), threshold: float) -> None: + """Compute a 2D surface mesh of a given isosurface from a 3D scalar field. + + The triangles and vertices defining the output mesh are written to the + :attr:`indices` and :attr:`verts` arrays. + + Args: + field: Scalar field from which to generate a mesh. + threshold: Target isosurface value. + + Raises: + ValueError: ``field`` is not a 3D array. + ValueError: Marching cubes shape does not match the shape of ``field``. + RuntimeError: :attr:`max_verts` and/or :attr:`max_tris` might be too small to hold the surface mesh. + """ + # WP_API int marching_cubes_surface_host(const float* field, int nx, int ny, int nz, float threshold, wp::vec3* verts, int* triangles, int max_verts, int max_tris, int* out_num_verts, int* out_num_tris); num_verts = ctypes.c_int(0) num_tris = ctypes.c_int(0) self.runtime.core.marching_cubes_surface_device.restype = ctypes.c_int + # For now we require that input field shape matches nx, ny, nz + if field.ndim != 3: + raise ValueError(f"Input field must be a three-dimensional array (got {field.ndim}).") + if field.shape[0] != self.nx or field.shape[1] != self.ny or field.shape[2] != self.nz: + raise ValueError( + f"Marching cubes shape ({self.nx}, {self.ny}, {self.nz}) does not match the " + f"input array shape {field.shape}." + ) + error = self.runtime.core.marching_cubes_surface_device( self.id, ctypes.cast(field.ptr, ctypes.c_void_p),