Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
63 changes: 63 additions & 0 deletions fvdb/functional/_meshing.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,69 @@ 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 factor (``1`` = full detail).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = uniform/off):
collapse flat blocks while keeping detail at features.
Comment thread
swahtz marked this conversation as resolved.
Outdated

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 factor (``1`` = full detail).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = uniform/off).
Comment thread
swahtz marked this conversation as resolved.
Outdated

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 factor (``1`` = full detail).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = uniform/off).
Comment thread
swahtz marked this conversation as resolved.
Outdated

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 factor (``1`` = full detail).
adaptivity (float): Curvature-adaptive decimation in ``[0, 1.5]`` (``0`` = uniform/off).
Comment thread
swahtz marked this conversation as resolved.
Outdated

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