This section describes the OpenVDB Python module and includes Python code snippets and some complete programs that illustrate how to perform common tasks. The Python module exposes most of the functionality of the C++ Grid class, including I/O, metadata management, voxel access and iteration, but almost none of the many tools. We expect to add support for tools in forthcoming releases.
The Python module supports a fixed set of grid types. If the symbol PY_OPENVDB_WRAP_ALL_GRID_TYPES
is defined at compile time, most of the grid types declared in openvdb.h are accessible in Python, otherwise only FloatGrid, BoolGrid and Vec3SGrid are accessible. To add support for grids with other value types or configurations, search for PY_OPENVDB_WRAP_ALL_GRID_TYPES
in the module source code, update the code as appropriate and recompile the module. (It is possible that this process will be streamlined in the future with a plugin mechanism.) Note however that adding grid types can significantly increase the time and memory needed to compile the module and can significantly increase the size of the resulting executable. In addition, grids of custom types that are saved to .vdb
files or pickled will not be readable by clients using the standard version of the module.
Also note that the Tree class is not exposed in Python. Much of its functionality is either available through the Grid or is too low-level to be generally useful in Python. Although trees are not accessible in Python, they can of course be operated on indirectly. Of note are the grid methods copy, which returns a new grid that shares its tree with the original grid, deepCopy, which returns a new grid that owns its own tree, and sharesWith, which reports whether two grids share a tree.
Contents
Getting started
The following example is a complete program that illustrates some of the basic steps to create grids and write them to disk:
1 import pyopenvdb
as vdb
10 cube.fill(min=(100, 100, 100), max=(199, 199, 199), value=1.0)
18 sphere = vdb.createLevelSetSphere(radius=50, center=(1.5, 2, 3))
21 sphere[
'radius'] = 50.0
25 sphere.transform = vdb.createLinearTransform(voxelSize=0.5)
28 sphere.name =
'sphere' 31 vdb.write(
'mygrids.vdb', grids=[cube, sphere])
This example shows how to read grids from files, and some ways to modify grids:
1 import pyopenvdb
as vdb
5 filename =
'mygrids.vdb' 6 grids = vdb.readAllGridMetadata(filename)
11 if (grid.gridClass == vdb.GridClass.LEVEL_SET
and 'radius' in grid
12 and grid[
'radius'] > 10.0):
13 sphere = vdb.read(filename, grid.name)
15 print 'skipping grid', grid.name
22 outside = sphere.background
27 for iter
in sphere.iterOnValues():
29 iter.value = (outside - dist) / width
33 for iter
in sphere.iterOffValues():
39 sphere.background = 0.0
41 sphere.gridClass = vdb.GridClass.FOG_VOLUME
Handling metadata
Metadata of various types (string, bool, int, float, and 2- and 3-element sequences of ints or floats) can be attached both to individual grids and to files on disk, either by supplying a Python dictionary of (name, value) pairs or, in the case of grids, through a dictionary-like interface.
Add (name, value) metadata pairs to a grid as you would to a dictionary. A new value overwrites an existing value if the name matches an existing name.
1 >>>
import pyopenvdb
as vdb
3 >>> grid = vdb.Vec3SGrid()
5 >>> grid[
'vector'] =
'gradient' 6 >>> grid[
'radius'] = 50.0
7 >>> grid[
'center'] = (10, 15, 10)
10 {
'vector':
'gradient',
'radius': 50.0,
'center': (10, 15, 10)}
15 >>>
'radius' in grid,
'misc' in grid
19 >>> grid[
'center'] = 0.0
22 >>> grid[
'center'] = (0, 0, 0, 0)
23 File
"<stdin>", line 1,
in <module>
24 TypeError: metadata value
"(0, 0, 0, 0)" of type tuple
is not allowed
27 >>> grid[0] = (10.5, 15, 30)
28 File
"<stdin>", line 1,
in <module>
29 TypeError: expected str, found int
as argument 1 to __setitem__()
Alternatively, replace all or some of a grid’s metadata by supplying a (name, value) dictionary:
2 ...
'vector':
'gradient',
4 ...
'center': (10, 15, 10)
8 >>> grid.metadata = metadata
11 ...
'center': [10.5, 15, 30],
16 >>> grid.updateMetadata(metadata)
Iterate over a grid’s metadata as you would over a dictionary:
2 ...
print '%s = %s' % (key, grid[key])
7 center = (10.5, 15.0, 30.0)
Removing metadata is also straightforward:
4 File
"<stdin>", line 1,
in <module>
Some grid metadata is exposed in the form of properties, either because it might be frequently accessed (a grid’s name, for example) or because its allowed values are somehow restricted:
1 >>> grid = vdb.createLevelSetSphere(radius=10.0)
5 >>> grid.gridClass = vdb.GridClass.FOG_VOLUME
7 {
'class':
'fog volume'}
10 >>> grid.gridClass = 123
11 File
"<stdin>", line 1,
in <module>
12 TypeError: expected str, found int
as argument 1 to
setGridClass()
16 >>> grid.gridClass =
'Hello, world.' 20 >>> grid.metadata = {}
21 >>> grid.vectorType = vdb.VectorType.COVARIANT
23 {
'vector_type':
'covariant'}
25 >>> grid.name =
'sphere' 26 >>> grid.creator =
'Python' 28 {
'vector_type':
'covariant',
'name':
'sphere',
'creator':
'Python'}
Setting these properties to None
removes the corresponding metadata, but the properties retain default values:
1 >>> grid.creator = grid.vectorType =
None 5 >>> grid.creator, grid.vectorType
Metadata can be associated with a .vdb
file at the time the file is written, by supplying a (name, value) dictionary as the optional metadata
argument to the write function:
2 ...
'creator':
'Python',
5 >>> vdb.write(
'mygrids.vdb', grids=grid, metadata=metadata)
File-level metadata can be retrieved with either the readMetadata function or the readAll function:
1 >>> metadata = vdb.readMetadata(
'mygrids.vdb')
3 {
'creator':
'Python',
'time':
'11:05:00'}
5 >>> grids, metadata = vdb.readAll(
'mygrids.vdb')
7 {
'creator':
'Python',
'time':
'11:05:00'}
8 >>> [grid.name
for grid
in grids]
Voxel access
Grids provide read-only and read/write accessors for voxel lookup via (i,  j,  k) index coordinates. Accessors store references to their parent grids, so a grid will not be deleted while it has accessors in use.
1 >>>
import pyopenvdb
as vdb
4 >>> grids, metadata = vdb.readAll(
'smoke2.vdb')
5 >>> [grid.name
for grid
in grids]
9 >>> dAccessor = grids[0].getAccessor()
10 >>> vAccessor = grids[1].getAccessor()
12 >>> ijk = (100, 103, 101)
14 >>> dAccessor.probeValue(ijk)
15 (0.17614534497261047,
True)
17 >>> dAccessor.setValueOn(ijk, 0.125)
18 >>> dAccessor.probeValue(ijk)
21 >>> vAccessor.probeValue(ijk)
22 ((-2.90625, 9.84375, 0.84228515625),
True)
24 >>> vAccessor.setActiveState(ijk,
False)
25 >>> vAccessor.probeValue(ijk)
26 ((-2.90625, 9.84375, 0.84228515625),
False)
29 >>> dAccessor = grids[0].getConstAccessor()
30 >>> dAccessor.setActiveState(ijk,
False)
31 File
"<stdin>", line 1,
in <module>
32 TypeError: accessor
is read-only
36 >>> del dAccessor, vAccessor
Iteration
Grids provide read-only and read/write iterators over their values. Iteration is over sequences of value objects (BoolGrid.Values, FloatGrid.Values, etc.) that expose properties such as the number of voxels spanned by a value (one, for a voxel value, more than one for a tile value), its coordinates and its active state. Value objects returned by read-only iterators are immutable; those returned by read/write iterators permit assignment to their active state and value properties, which modifies the underlying grid. Value objects store references to their parent grids, so a grid will not be deleted while one of its value objects is in use.
1 >>>
import pyopenvdb
as vdb
3 >>> grid = vdb.read(
'smoke2.vdb', gridname=
'v')
4 >>> grid.__class__.__name__
11 ...
for item
in grid.citerOffValues():
12 ...
if voxels == N
and tiles == N:
14 ...
if item.count == 1:
17 ...
print 'voxel', item.min
21 ...
print 'tile', item.min, item.max
23 tile (0, 0, 0) (7, 7, 7)
24 tile (0, 0, 8) (7, 7, 15)
25 tile (0, 0, 16) (7, 7, 23)
26 tile (0, 0, 24) (7, 7, 31)
27 tile (0, 0, 32) (7, 7, 39)
35 >>>
from math
import sqrt
36 >>>
for item
in grid.iterOnValues():
37 ... vector = item.value
38 ... magnitude = sqrt(sum(x * x
for x
in vector))
39 ... item.value = [x / magnitude
for x
in vector]
For some operations, it might be more convenient to use one of the grid methods mapOn, mapOff or mapAll. These methods iterate over a grid’s tiles and voxels (active, inactive or both, respectively) and replace each value x with f(x), where f is a callable object. These methods are not multithreaded.
1 >>>
import pyopenvdb
as vdb
2 >>>
from math
import sqrt
4 >>> grid = vdb.read(
'smoke2.vdb', gridname=
'v')
7 ... magnitude = sqrt(sum(x * x
for x
in vector))
8 ...
return [x / magnitude
for x
in vector]
10 >>> grid.mapOn(normalize)
Similarly, the combine method iterates over corresponding pairs of values (tile and voxel) of two grids A and B of the same type (FloatGrid, Vec3SGrid, etc.), replacing values in A with f(a, b), where f is a callable object. This operation assumes that index coordinates (i,  j,  k) in both grids correspond to the same physical, world space location. Also, the operation always leaves grid B empty.
1 >>>
import pyopenvdb
as vdb
3 >>> density = vdb.read(
'smoke2.vdb', gridname=
'density')
4 >>> density.__class__.__name__
7 >>> sphere = vdb.createLevelSetSphere(radius=50.0, center=(100, 300, 100))
9 >>> density.combine(sphere,
lambda a, b:
min(a, b))
For now, combine operates only on tile and voxel values, not on their active states or other attributes.
Working with NumPy arrays
Large data sets are often handled in Python using NumPy. The OpenVDB Python module can optionally be compiled with NumPy support. With NumPy enabled, the copyFromArray and copyToArray grid methods can be used to exchange data efficiently between scalar-valued grids and three-dimensional NumPy arrays and between vector-valued grids and four-dimensional NumPy arrays.
1 >>>
import pyopenvdb
as vdb
4 >>> array = numpy.random.rand(200, 200, 200)
10 >>> grid = vdb.FloatGrid()
11 >>> grid.copyFromArray(array)
12 >>> grid.activeVoxelCount() == array.size
14 >>> grid.evalActiveVoxelBoundingBox()
15 ((0, 0, 0), (199, 199, 199))
19 >>> vecarray = numpy.ndarray((60, 70, 80, 3), int)
21 >>> vecgrid = vdb.Vec3SGrid()
22 >>> vecgrid.copyFromArray(vecarray)
23 >>> vecgrid.activeVoxelCount() == 60 * 70 * 80
25 >>> vecgrid.evalActiveVoxelBoundingBox()
26 ((0, 0, 0), (59, 69, 79))
When copying from a NumPy array, values in the array that are equal to the destination grid’s background value (or close to it, if the tolerance
argument to copyFromArray is nonzero) are set to the background value and are marked inactive. All other values are marked active.
2 >>> grid.copyFromArray(array, tolerance=0.2)
3 >>>
print '%d%% of copied voxels are active' % (
4 ... round(100.0 * grid.activeVoxelCount() / array.size))
5 80% of copied voxels are active
The optional ijk
argument specifies the index coordinates of the voxel in the destination grid into which to start copying values. That is, array index (0, 0, 0) maps to voxel (i,  j,  k).
2 >>> grid.copyFromArray(array, ijk=(-1, 2, 3))
3 >>> grid.evalActiveVoxelBoundingBox()
4 ((-1, 2, 3), (198, 201, 202))
The copyToArray method also accepts an ijk
argument. It specifies the index coordinates of the voxel to be copied to array index (0, 0, 0).
1 >>> grid = vdb.createLevelSetSphere(radius=10.0)
2 >>> array = numpy.ndarray((40, 40, 40), int)
6 >>> grid.copyToArray(array, ijk=(-15, -20, -35))
8 array([ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
9 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
10 3, 3, 3, 2, 1, 0, -1, -2, -3, -3,
11 -3, -3, -3, -3, -3, -3, -3, -3, -3, -3])
copyToArray has no tolerance
argument, because there is no distinction between active and inactive values in the destination array.
Mesh conversion
Also available if the OpenVDB Python module is compiled with NumPy support (see above) are grid methods to convert polygonal meshes to level sets (see tools::meshToLevelSet for some restrictions) and to convert isosurfaces of scalar-valued grids to meshes.
1 >>>
import pyopenvdb
as vdb
4 >>> grid = vdb.read(
'bunny.vdb',
'ls_bunny')
7 >>> points, quads = grid.convertToQuads()
11 array([[-14.05082607, -0.10118673, -0.40250054],
12 [-14.05230808, -0.05570767, -0.45693323],
13 [-14.05613995, -0.0734246 , -0.42150033],
15 [ 7.25201273, 13.25417805, 6.45283508],
16 [ 7.25596714, 13.31225586, 6.40827513],
17 [ 7.30518484, 13.21096039, 6.40724468]], dtype=float32)
26 [1327942, 1327766, 1339685, 1339733],
27 [1339728, 1327921, 1327942, 1339733],
28 [1339733, 1339685, 1339661, 1339728]], dtype=uint32)
32 >>> gridFromQuads = vdb.FloatGrid.createLevelSetFromPolygons(
33 ... points, quads=quads, transform=grid.transform)
38 >>> points, triangles, quads = grid.convertToPolygons(adaptivity=0.5)
42 array([[-14.02906322, -0.07213751, -0.49265194],
43 [-14.11877823, -0.11127799, -0.17118289],
44 [-13.85006142, -0.08145611, -0.86669081],
46 [ 7.31098318, 12.97358608, 6.55133963],
47 [ 7.20240211, 12.80632019, 6.80356836],
48 [ 7.23679161, 13.28100395, 6.45595646]], dtype=float32)
57 [22794, 22796, 22797],
58 [22784, 22783, 22810],
59 [22796, 22795, 22816]], dtype=uint32)
68 [23351, 23349, 23341, 23344],
69 [23344, 23117, 23169, 23351],
70 [23169, 23167, 23349, 23351]], dtype=uint32)
73 >>> doubleGridFromPolys = vdb.DoubleGrid.createLevelSetFromPolygons(
74 ... points, triangles, quads, transform=grid.transform)
The mesh representation is similar to that of the commonly-used Wavefront .obj
file format, except that the vertex array is indexed starting from 0 rather than 1. To output mesh data to a file in .obj
format, one might do the following:
1 >>>
def writeObjFile(filename, points, triangles=[], quads=[]):
2 ... f = open(filename,
'w')
5 ... f.write(
'v %g %g %g\n' % tuple(xyz))
8 ...
for ijk
in triangles:
9 ... f.write(
'f %d %d %d\n' %
10 ... (ijk[0]+1, ijk[1]+1, ijk[2]+1))
11 ...
for ijkl
in quads:
12 ... f.write(
'f %d %d %d %d\n' %
13 ... (ijkl[0]+1, ijkl[1]+1, ijkl[2]+1, ijkl[3]+1))
16 >>> mesh = grid.convertToPolygons(adaptivity=0.8)
17 >>> writeObjFile(
'bunny.obj', *mesh)
C++ glue routines
Python objects of type FloatGrid, Vec3SGrid, etc. are backed by C structs that “inherit” from PyObject
. The OpenVDB Python extension module includes public functions that you can call in your own extension modules to convert between openvdb::Grids and PyObject
s. See the pyopenvdb.h reference for a description of these functions and a usage example.
Your extension module might need to link against the OpenVDB extension module in order to access these functions. On UNIX systems, it might also be necessary to specify the RTLD_GLOBAL
flag when importing the OpenVDB module, to allow its symbols to be shared across modules. See setdlopenflags in the Python sys module for one way to do this.