Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
11 changes: 11 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ The rules for this file:
* accompany each entry with github issue/PR number (Issue #xyz)

-------------------------------------------------------------------------------
??/??/???? orbeckst, spyke7
Comment thread
spyke7 marked this conversation as resolved.
Outdated

* 1.2.1
Comment thread
spyke7 marked this conversation as resolved.
Outdated

Enhancements

* Implemented loading of a Grid from a native object for OpenVDB (Issue #162, PR #170)

Fixes


05/22/2026 orbeckst, spyke7

* 1.2.0
Expand Down
102 changes: 80 additions & 22 deletions gridData/OpenVDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,21 @@
@dataclass
class DownCastTo:
""":func:`~dataclasses.dataclass` decorator serving as a marker for a downcast.

This function is used to create a proxy for an OpenVDB grid type.
The field :attr:`gridType` contains the OpenVDB grid type that it represents.
:meth:`OpenVDBField._get_best_grid_type` selects a OpenVDB grid that best matches
the numpy dtype of the data but in some cases, only target OpenVDB grid types are
available that loose precision. In this case, this class wraps the orginal OpenVDB
class to indicate that the downcast. For example, ::

np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")]

indicates that NumPy int32 data should be represented by a :class:`openvdb.Int32Grid`
but if this is not available, a :class:`openvdb.FloatGrid` is used instead,
which, however, is only able to represent a subset of all 32-bit integers.
"""

gridType: str


Expand All @@ -142,7 +143,7 @@ class OpenVDBField(object):

import gridData.OpenVDB as OpenVDB

vdb_field = OpenVDB.OpenVDBField(grid=np.ones((3, 4, 5)),
vdb_field = OpenVDB.OpenVDBField(grid=np.ones((3, 4, 5)),
origin=np.array([1.5, 0, 0]),
delta=np.array([0.5, 0.5, 0.25]),
name='density')
Expand All @@ -154,6 +155,22 @@ class OpenVDBField(object):
g.export('output.vdb', format='vdb')
"""

# dtype maps
_DATATYPES = {
Comment thread
spyke7 marked this conversation as resolved.
Outdated
np.dtype("bool"): ["BoolGrid"],
np.dtype("int8"): ["Int32Grid", "FloatGrid"],
np.dtype("uint8"): ["Int32Grid", "FloatGrid"],
np.dtype("int16"): ["Int32Grid", "FloatGrid"],
np.dtype("uint16"): ["Int32Grid", "FloatGrid"],
np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")],
np.dtype("uint32"): [DownCastTo("Int32Grid"), DownCastTo("FloatGrid")],
np.dtype("int64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("uint64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("float16"): ["HalfGrid", "FloatGrid"],
np.dtype("float32"): ["FloatGrid"],
np.dtype("float64"): ["DoubleGrid", DownCastTo("FloatGrid")],
}
Comment thread
spyke7 marked this conversation as resolved.
Outdated

def __init__(
self,
grid=None,
Expand Down Expand Up @@ -205,8 +222,13 @@ def __init__(
self.metadata = {}

if grid is not None:
self._populate(grid, origin, delta)
self.vdb_grid = self._create_openvdb_grid()
if isinstance(grid, vdb.GridBase):
self.vdb_grid = grid
self._extract_from_vdb_grid()
else:
self._populate(grid, origin, delta)
self.vdb_grid = self._create_openvdb_grid()

else:
self.grid = None
self.origin = None
Expand Down Expand Up @@ -272,6 +294,57 @@ def native(self):
"""
return self.vdb_grid

def _extract_from_vdb_grid(self):
"""Extract numpy array, origin, delta from stored VDB grid.

This method converts the sparse VDB grid to a dense numpy array
and extracts the transform information.
"""
for key in self.vdb_grid.metadata:
try:
self.metadata[key] = self.vdb_grid[key]
except (TypeError, ValueError):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under which conditions does that fail? I am always suspicious of try/except that passes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vec3f can be one of the example. But I can create a path for a warning to be raised for this

pass

transformation = self.vdb_grid.transform

v_origin = np.array(transformation.indexToWorld([0, 0, 0]))
v_delta = np.array(transformation.indexToWorld([1, 1, 1])) - v_origin

self.origin = v_origin
self.delta = v_delta

dtype = np.dtype("float32")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's cleaner to set the dtype to the default in an else block of the for loop:

        for numpy_dtype, vdb_names in self._DATATYPES.items():
            name_dtype = vdb_names[0]
            ...
            if vdb_class_name == canonical_name:
                dtype = numpy_dtype
                break
        else:
             # could not find a matching dtype, use default
             dtype = np.float32

Or maybe should we use float64? @BradyAJohnston @PardhavMaradani do you have an opinion?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also log a warning if you encounter an unknown type and use the default.

vdb_class_name = type(self.vdb_grid).__name__
for numpy_dtype, vdb_names in self._DATATYPES.items():
name_dtype = vdb_names[0]
canonical_name = (
name_dtype.gridType
if isinstance(name_dtype, DownCastTo)
else name_dtype
)

if vdb_class_name == canonical_name:
dtype = numpy_dtype
break

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is flawed. You are picking the first match to a value in the _DATATYPES dict and the relationship from vdb to np is many-to-one (eg FloatGrid can map back to int8, unint16, float32, ...) and the dict order is not defined so you might choose the wrong dtype.

Instead you must define an appropriate mapping from VDB to np.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I will try to redo that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please once relook at the logic


bbox = self.vdb_grid.evalActiveVoxelBoundingBox()

if bbox is None or bbox[0] == bbox[1]:
self.grid = np.zeros((0, 0, 0), dtype=dtype)
return
Comment thread
spyke7 marked this conversation as resolved.
Outdated

shape = tuple(np.array(bbox[1]) - np.array(bbox[0]) + 1)

self.grid = np.zeros(shape, dtype=dtype)
print(dtype)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still a print there!

self.vdb_grid.copyToArray(self.grid, ijk=bbox[0])

if not np.all(np.array(bbox[0]) == 0):
self.origin = np.array(
transformation.indexToWorld(np.array(bbox[0]).tolist())
)

def _populate(self, grid, origin, delta):
"""Populate the field with grid data.

Expand Down Expand Up @@ -331,23 +404,8 @@ def _get_best_grid_type(self):
TypeError
If dtype is not supported or no suitable grid type is available
"""
datatypes = {
np.dtype("bool"): ["BoolGrid"],
np.dtype("int8"): ["Int32Grid", "FloatGrid"],
np.dtype("uint8"): ["Int32Grid", "FloatGrid"],
np.dtype("int16"): ["Int32Grid", "FloatGrid"],
np.dtype("uint16"): ["Int32Grid", "FloatGrid"],
np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")],
np.dtype("uint32"): [DownCastTo("Int32Grid"), DownCastTo("FloatGrid")],
np.dtype("int64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("uint64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("float16"): ["HalfGrid", "FloatGrid"],
np.dtype("float32"): ["FloatGrid"],
np.dtype("float64"): ["DoubleGrid", DownCastTo("FloatGrid")],
}

try:
vdb_gridtypes = datatypes[self.grid.dtype]
vdb_gridtypes = self._DATATYPES[self.grid.dtype]
except KeyError:
raise TypeError(f"Data type {self.grid.dtype} not supported for VDB")

Expand Down
15 changes: 15 additions & 0 deletions gridData/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,21 @@ def __init__(self, grid=None, edges=None, origin=None, delta=None,
self.interpolation_cval = None # default to using min(grid)

if grid is not None:
try:
# if a openvdb native grid is passed
import openvdb as vdb
if isinstance(grid, vdb.GridBase):
vdb_field = OpenVDB.OpenVDBField(grid=grid)
self.metadata = vdb_field.metadata
self._load(
grid=vdb_field.grid,
origin=vdb_field.origin,
delta=vdb_field.delta,
metadata=vdb_field.metadata,
)
return
except ImportError:
pass
Comment on lines +252 to +266

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right idea. This (together with the filename/array code below) is quite clunky. For each additional format we will need to add another try/except.

We need to come up with a more general solution, e.g., a dict-based dispatch lookup or a dispatcher function that calls the correct setup, based on the detected format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jus like converter, can map different loader for native in a dict
But before that need to check the type of grid passed, then Ig it will look clean.

if isinstance(grid, str):
# can probably safely try to load() it...
filename = grid
Expand Down
14 changes: 14 additions & 0 deletions gridData/tests/test_vdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,20 @@ def test_from_native_grid_shape_values_and_dimension(self, grid345):
world = native_grid.transform.indexToWorld((0, 0, 0))
assert_allclose([world[0], world[1], world[2]], g.origin, rtol=1e-5)

def test_extract_from_vdb_grid(self, grid345):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameterize the test to try out all normally supported vdb gridtypes

data, g = grid345
g.metadata["name"] = "new_density"

native = g.convert_to("vdb")
new_vdb_grid = Grid(grid=native)

assert_allclose(new_vdb_grid.grid, g.grid, rtol=1e-5)
assert_allclose(new_vdb_grid.origin, g.origin, rtol=1e-5)
assert_allclose(new_vdb_grid.delta, g.delta, rtol=1e-5)
assert new_vdb_grid.metadata["name"] == "new_density"
assert new_vdb_grid.grid.dtype == np.dtype("float32")
assert_allclose(new_vdb_grid.grid, data, rtol=1e-5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also include a test that shows that file round-tripping works, e.g.,

  1. create Grid
  2. write OpenVDB
  3. read OpenVDB
  4. compare to Grid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include a test that reads the test vdb file directly with OpenVDB first.


@pytest.mark.skipif(
not HAS_OPENVDB, reason="Need openvdb to test import error handling"
Expand Down
Loading