Skip to content

Commit

Permalink
Merge branch 'ershi/marching_cube_docs' into 'main'
Browse files Browse the repository at this point in the history
MarchingCube docs + more runtime checks

See merge request omniverse/warp!509
  • Loading branch information
mmacklin committed May 27, 2024
2 parents 37285d7 + fa5db54 commit b8e920f
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions docs/modules/runtime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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() <bvh_query_aabb>`.

Marching Cubes
--------------

The :class:`wp.MarchingCubes <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
---------

Expand Down
72 changes: 69 additions & 3 deletions warp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -4199,21 +4229,57 @@ 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
self.nz = nz
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),
Expand Down

0 comments on commit b8e920f

Please sign in to comment.