Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions fvdb/_fvdb_cpp.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,13 @@ def marching_cubes(
field: JaggedTensor,
level: float,
) -> list[JaggedTensor]: ...
def dual_contour(
grid: GridBatchData,
field: JaggedTensor,
iso: float,
reduce: int,
adaptivity: float,
) -> tuple[JaggedTensor, JaggedTensor, JaggedTensor]: ...
def integrate_tsdf(
grid: GridBatchData,
truncation_margin: float,
Expand Down
20 changes: 12 additions & 8 deletions fvdb/functional/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@

# Meshing / TSDF
from ._meshing import (
dual_contour_batch,
dual_contour_single,
integrate_tsdf_batch,
integrate_tsdf_single,
integrate_tsdf_with_features_batch,
Expand All @@ -93,14 +95,6 @@
marching_cubes_single,
)

# Signed distance fields
from ._sdf import (
reinitialize_sdf_batch,
reinitialize_sdf_single,
retopologize_sdf_batch,
retopologize_sdf_single,
)

# Pooling / refinement
from ._pooling import (
avg_pool_batch,
Expand Down Expand Up @@ -145,6 +139,14 @@
voxels_along_rays_single,
)

# Signed distance fields
from ._sdf import (
reinitialize_sdf_batch,
reinitialize_sdf_single,
retopologize_sdf_batch,
retopologize_sdf_single,
)

# Grid topology
from ._topology import (
clip_batch,
Expand Down Expand Up @@ -264,6 +266,8 @@
# Meshing
"marching_cubes_batch",
"marching_cubes_single",
"dual_contour_batch",
"dual_contour_single",
"integrate_tsdf_batch",
"integrate_tsdf_single",
"integrate_tsdf_with_features_batch",
Expand Down
62 changes: 62 additions & 0 deletions fvdb/functional/_meshing.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,68 @@ def marching_cubes_single(
return result[0].jdata, result[1].jdata, result[2].jdata


def dual_contour_batch(
grid: GridBatch,
field: JaggedTensor,
iso: float = 0.0,
reduce: int = 1,
adaptivity: float = 0.0,
) -> tuple[JaggedTensor, JaggedTensor, JaggedTensor]:
"""Extract a dual-contouring (DC + QEF) mesh from an SDF on a grid batch.

Places one vertex per surface cell by minimizing a quadratic error function from the cell's edge
zero-crossings and SDF-gradient normals (sharp-feature preserving), with optional cluster-collapse
decimation. Requires a narrow-band SDF with a >= ~3-voxel band for a watertight result (e.g. from
:func:`retopologize_sdf_batch`).

Args:
grid (GridBatch): The grid batch defining the sparse topology.
field (JaggedTensor): Per-voxel signed distance values.
iso (float): Isovalue at which to extract the surface. Default ``0.0``.
reduce (int): Uniform ``F x F x F`` cluster-collapse decimation block size. ``reduce=1`` is full detail only when ``adaptivity == 0``; when ``adaptivity > 0`` it instead sets the coarse-block size for the adaptive collapse (default ``8`` when ``reduce <= 1``).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = off). When ``> 0``, flat blocks are collapsed while features keep full detail, so the mesh is simplified even at ``reduce=1``.

Returns:
vertices (JaggedTensor): Mesh vertex positions, shape ``(B, -1, 3)``.
faces (JaggedTensor): Triangle face indices, shape ``(B, -1, 3)``.
normals (JaggedTensor): Per-vertex (normalized SDF gradient) normals, shape ``(B, -1, 3)``.

.. seealso:: :func:`dual_contour_single`, :func:`marching_cubes_batch`
"""
grid_data = grid.data
result = _fvdb_cpp.dual_contour(grid_data, field._impl, iso, reduce, adaptivity)
return JaggedTensor(impl=result[0]), JaggedTensor(impl=result[1]), JaggedTensor(impl=result[2])


def dual_contour_single(
grid: Grid,
field: torch.Tensor,
iso: float = 0.0,
reduce: int = 1,
adaptivity: float = 0.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Extract a dual-contouring (DC + QEF) mesh from an SDF on a single grid.

Args:
grid (Grid): The single grid defining the sparse topology.
field (torch.Tensor): Per-voxel signed distance values.
iso (float): Isovalue at which to extract the surface. Default ``0.0``.
reduce (int): Uniform ``F x F x F`` cluster-collapse decimation block size. ``reduce=1`` is full detail only when ``adaptivity == 0``; when ``adaptivity > 0`` it instead sets the coarse-block size for the adaptive collapse (default ``8`` when ``reduce <= 1``).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = off). When ``> 0``, flat blocks are collapsed while features keep full detail, so the mesh is simplified even at ``reduce=1``.

Returns:
vertices (torch.Tensor): Vertex positions, shape ``(V, 3)``.
faces (torch.Tensor): Triangle face indices, shape ``(T, 3)``.
normals (torch.Tensor): Per-vertex (normalized SDF gradient) normals, shape ``(V, 3)``.

.. seealso:: :func:`dual_contour_batch`, :func:`marching_cubes_single`
"""
grid_data = grid.data
field_jt = JaggedTensor(field)
result = _fvdb_cpp.dual_contour(grid_data, field_jt._impl, iso, reduce, adaptivity)
return result[0].jdata, result[1].jdata, result[2].jdata


def integrate_tsdf_batch(
grid: GridBatch,
truncation_distance: float,
Expand Down
28 changes: 14 additions & 14 deletions fvdb/functional/_sdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def retopologize_sdf_batch(
"""Retopologize a signed field into a clean narrow-band SDF on a (possibly pruned) grid batch.

If ``pad`` is ``True`` the grid is first dilated by ``band`` voxels (so the eikonal solve has room
to propagate a full-width band), then :func:`reinitialize_sdf_batch` is run, and finally, if
to fill the full ``±band*vx`` band), then :func:`reinitialize_sdf_batch` is run, and finally, if
``prune`` is ``True``, the grid is pruned to the voxels strictly inside the band
(``|phi| < band*vx*0.999``). The prune reuses :meth:`GridBatch.pruned_grid`; the resulting field
is selected in the grid's canonical voxel order so it stays aligned with the pruned grid.
Expand All @@ -135,12 +135,12 @@ def retopologize_sdf_batch(
:attr:`~fvdb.SmoothingMode.TAUBIN` (volume-preserving). Only used when ``smooth > 0``.
redistance_iters (int): Number of redistancing sweeps. ``<= 0`` uses the default.
pad (bool): If ``True`` (default) dilate the grid by ``band`` voxels before redistancing so
the output narrow band is a full ``band`` voxels wide even if the input grid had a
thinner active region. Newly added voxels are seeded as *exterior* (``+band*vx``), which
is correct when the dilation extends outward -- i.e. when the grid's interior (the
``phi < 0`` region) is already represented (the usual case for occupancy/TSDF/mesh-derived
fields). For a thin shell that does not fill its interior, pass ``pad=False`` and supply a
grid that already has an adequate band.
the output band reaches the full ``band`` voxels on each side of the surface (``±band*vx``)
even if the input grid had a thinner active region. Newly added voxels are seeded as
*exterior* (``+band*vx``), which is correct when the dilation extends outward -- i.e. when
the grid's interior (the ``phi < 0`` region) is already represented (the usual case for
occupancy/TSDF/mesh-derived fields). For a thin shell that does not fill its interior, pass
``pad=False`` and supply a grid that already has an adequate band.
prune (bool): If ``True`` prune to the narrow band; if ``False`` return the (possibly
padded) grid and the re-initialized field unchanged.

Expand Down Expand Up @@ -179,7 +179,7 @@ def retopologize_sdf_single(
"""Retopologize a signed field into a clean narrow-band SDF on a (possibly pruned) single grid.

If ``pad`` is ``True`` the grid is first dilated by ``band`` voxels (so the eikonal solve has room
to propagate a full-width band), then :func:`reinitialize_sdf_single` is run, and finally, if
to fill the full ``±band*vx`` band), then :func:`reinitialize_sdf_single` is run, and finally, if
``prune`` is ``True``, the grid is pruned to the voxels strictly inside the band
(``|phi| < band*vx*0.999``).

Expand All @@ -194,12 +194,12 @@ def retopologize_sdf_single(
:attr:`~fvdb.SmoothingMode.TAUBIN` (volume-preserving). Only used when ``smooth > 0``.
redistance_iters (int): Number of redistancing sweeps. ``<= 0`` uses the default.
pad (bool): If ``True`` (default) dilate the grid by ``band`` voxels before redistancing so
the output narrow band is a full ``band`` voxels wide even if the input grid had a
thinner active region. Newly added voxels are seeded as *exterior* (``+band*vx``), which
is correct when the dilation extends outward -- i.e. when the grid's interior (the
``phi < 0`` region) is already represented (the usual case for occupancy/TSDF/mesh-derived
fields). For a thin shell that does not fill its interior, pass ``pad=False`` and supply a
grid that already has an adequate band.
the output band reaches the full ``band`` voxels on each side of the surface (``±band*vx``)
even if the input grid had a thinner active region. Newly added voxels are seeded as
*exterior* (``+band*vx``), which is correct when the dilation extends outward -- i.e. when
the grid's interior (the ``phi < 0`` region) is already represented (the usual case for
occupancy/TSDF/mesh-derived fields). For a thin shell that does not fill its interior, pass
``pad=False`` and supply a grid that already has an adequate band.
prune (bool): If ``True`` prune to the narrow band; if ``False`` return the (possibly padded)
grid and the re-initialized field unchanged.

Expand Down
39 changes: 34 additions & 5 deletions fvdb/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,34 @@ def marching_cubes(

return functional.marching_cubes_single(self, field, level)

def dual_contour(
self,
field: torch.Tensor,
iso: float = 0.0,
reduce: int = 1,
adaptivity: float = 0.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Extract a dual-contouring (DC + QEF) mesh from a per-voxel SDF on this grid.

Places one vertex per surface cell via quadratic-error-function minimization (sharp-feature
preserving), with optional cluster-collapse decimation. Needs a narrow-band SDF with a
>= ~3-voxel band for a watertight result (e.g. from :meth:`retopologize_sdf`).

Args:
field (torch.Tensor): Per-voxel signed distance values, shape ``(num_voxels,)``.
iso (float): Isovalue at which to extract the surface.
reduce (int): Uniform ``F x F x F`` cluster-collapse decimation block size. ``reduce=1`` is full detail only when ``adaptivity == 0``; when ``adaptivity > 0`` it instead sets the coarse-block size for the adaptive collapse (default ``8`` when ``reduce <= 1``).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = off). When ``> 0``, flat blocks are collapsed while features keep full detail, so the mesh is simplified even at ``reduce=1``.

Returns:
vertices (torch.Tensor): Vertex positions, shape ``(V, 3)``.
faces (torch.Tensor): Triangle face indices, shape ``(T, 3)``.
normals (torch.Tensor): Per-vertex (normalized SDF gradient) normals, shape ``(V, 3)``.
"""
from . import functional

return functional.dual_contour_single(self, field, iso, reduce, adaptivity)

def reinitialize_sdf(
self,
field: torch.Tensor,
Expand Down Expand Up @@ -1528,7 +1556,7 @@ def retopologize_sdf(
"""Retopologize a signed field into a clean narrow-band SDF on a (possibly pruned) grid.

If ``pad`` is ``True`` this grid is first dilated by ``band`` voxels so the redistance has
room to build a full-width band, then :meth:`reinitialize_sdf` is run, and finally, if
room to fill the full ``±band*vx`` band, then :meth:`reinitialize_sdf` is run, and finally, if
``prune`` is ``True``, the grid is pruned to the voxels strictly inside the band
(``|phi| < band*vx*0.999``).

Expand All @@ -1541,10 +1569,11 @@ def retopologize_sdf(
:attr:`~fvdb.SmoothingMode.MEAN_CURVATURE` (default) or
:attr:`~fvdb.SmoothingMode.TAUBIN` (volume-preserving). Only used when ``smooth > 0``.
redistance_iters (int): Number of redistancing sweeps; ``<= 0`` uses the default.
pad (bool): If ``True`` (default) dilate by ``band`` first so the output band is a full
``band`` voxels wide even if the input grid was thinner. New voxels are seeded as
exterior (``+band*vx``), which is correct when the interior (``phi < 0``) is already
represented; for a hollow thin shell, pass ``pad=False`` with a pre-banded grid.
pad (bool): If ``True`` (default) dilate by ``band`` first so the output band reaches the
full ``band`` voxels on each side of the surface (``±band*vx``) even if the input grid
was thinner. New voxels are seeded as exterior (``+band*vx``), which is correct when the
interior (``phi < 0``) is already represented; for a hollow thin shell, pass
``pad=False`` with a pre-banded grid.
prune (bool): If ``True`` prune to the narrow band, else return the (padded) grid.

Returns:
Expand Down
41 changes: 36 additions & 5 deletions fvdb/grid_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,36 @@ def marching_cubes(

return functional.marching_cubes_batch(self, field, level)

def dual_contour(
self,
field: JaggedTensor,
iso: float = 0.0,
reduce: int = 1,
adaptivity: float = 0.0,
) -> tuple[JaggedTensor, JaggedTensor, JaggedTensor]:
"""Extract a dual-contouring (DC + QEF) mesh from a per-voxel SDF on this grid batch.

Places one vertex per surface cell via quadratic-error-function minimization (sharp-feature
preserving), with optional cluster-collapse decimation. Needs a narrow-band SDF with a
>= ~3-voxel band for a watertight result (e.g. from :meth:`retopologize_sdf`).

Args:
field (JaggedTensor): Per-voxel signed distance values.
iso (float): Isovalue at which to extract the surface.
reduce (int): Uniform ``F x F x F`` cluster-collapse decimation block size. ``reduce=1`` is full detail only when ``adaptivity == 0``; when ``adaptivity > 0`` it instead sets the coarse-block size for the adaptive collapse (default ``8`` when ``reduce <= 1``).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = off). When ``> 0``, flat blocks are collapsed while features keep full detail, so the mesh is simplified even at ``reduce=1``.

Returns:
vertices (JaggedTensor): Mesh vertex positions, shape ``(batch_size, -1, 3)``.
faces (JaggedTensor): Triangle face indices, shape ``(batch_size, -1, 3)``.
normals (JaggedTensor): Per-vertex (normalized SDF gradient) normals, shape ``(batch_size, -1, 3)``.

.. seealso:: :meth:`Grid.dual_contour`
"""
from . import functional

return functional.dual_contour_batch(self, field, iso, reduce, adaptivity)

def reinitialize_sdf(
self,
field: JaggedTensor,
Expand Down Expand Up @@ -1076,7 +1106,7 @@ def retopologize_sdf(
"""Retopologize a signed field into a clean narrow-band SDF on a (possibly pruned) grid batch.

If ``pad`` is ``True`` the grid is first dilated by ``band`` voxels so the redistance has
room to build a full-width band, then :meth:`reinitialize_sdf` is run, and finally, if
room to fill the full ``±band*vx`` band, then :meth:`reinitialize_sdf` is run, and finally, if
``prune`` is ``True``, the grid is pruned to the voxels strictly inside the band
(``|phi| < band*vx*0.999``).

Expand All @@ -1089,10 +1119,11 @@ def retopologize_sdf(
:attr:`~fvdb.SmoothingMode.MEAN_CURVATURE` (default) or
:attr:`~fvdb.SmoothingMode.TAUBIN` (volume-preserving). Only used when ``smooth > 0``.
redistance_iters (int): Number of redistancing sweeps; ``<= 0`` uses the default.
pad (bool): If ``True`` (default) dilate by ``band`` first so the output band is a full
``band`` voxels wide even if the input grid was thinner. New voxels are seeded as
exterior (``+band*vx``), which is correct when the interior (``phi < 0``) is already
represented; for a hollow thin shell, pass ``pad=False`` with a pre-banded grid.
pad (bool): If ``True`` (default) dilate by ``band`` first so the output band reaches the
full ``band`` voxels on each side of the surface (``±band*vx``) even if the input grid
was thinner. New voxels are seeded as exterior (``+band*vx``), which is correct when the
interior (``phi < 0``) is already represented; for a hollow thin shell, pass
``pad=False`` with a pre-banded grid.
prune (bool): If ``True`` prune to the narrow band, else return the (padded) grid batch.

Returns:
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ set(FVDB_CU_FILES
fvdb/detail/ops/ClipGrid.cu
fvdb/detail/ops/CloneGrid.cu
fvdb/detail/ops/ConcatenateGrids.cu
fvdb/detail/ops/DualContour.cu
fvdb/detail/ops/IndexGrid.cu
fvdb/detail/ops/MakeContiguous.cu
fvdb/detail/ops/SerializeGrid.cu
Expand Down
Loading
Loading