1+ r"""
2+ :mod:`~gridData.OpenVDB` --- routines to write OpenVDB files
3+ =============================================================
4+
5+ The OpenVDB format is used by Blender and other VFX software for
6+ volumetric data. See https://www.openvdb.org
7+
8+ .. Note:: This module implements a simple writer for 3D regular grids,
9+ sufficient to export density data for visualization in Blender.
10+
11+ The OpenVDB format uses a sparse tree structure to efficiently store
12+ volumetric data. It is the native format for Blender's volume system.
13+
14+
15+ Writing OpenVDB files
16+ ---------------------
17+
18+ If you have a :class:`~gridData.core.Grid` object, you can write it to
19+ OpenVDB format::
20+
21+ from gridData import Grid
22+ g = Grid("data.dx")
23+ g.export("data.vdb")
24+
25+ This will create a file that can be imported directly into Blender
26+ (File -> Import -> OpenVDB).
27+
28+
29+ Building an OpenVDB field from a numpy array
30+ ---------------------------------------------
31+
32+ Requires:
33+
34+ grid
35+ numpy 3D array
36+ origin
37+ cartesian coordinates of the center of the (0,0,0) grid cell
38+ delta
39+ n x n array with the length of a grid cell along each axis
40+
41+ Example::
42+
43+ import OpenVDB
44+ vdb_field = OpenVDB.field('density')
45+ vdb_field.populate(grid, origin, delta)
46+ vdb_field.write('output.vdb')
47+
48+
49+ Classes and functions
50+ ---------------------
51+
52+ """
53+
54+ import numpy
55+ import warnings
56+
57+ try :
58+ import pyopenvdb as vdb
59+ except ImportError :
60+ vdb = None
61+
62+
63+ class field (object ):
64+ """OpenVDB field object for writing volumetric data.
65+
66+ This class provides a simple interface to write 3D grid data to
67+ OpenVDB format, which can be imported into Blender and other
68+ VFX software.
69+
70+ The field object holds grid data and metadata, and can write it
71+ to a .vdb file.
72+
73+ Example
74+ -------
75+ Create a field and write it::
76+
77+ vdb_field = OpenVDB.field('density')
78+ vdb_field.populate(grid, origin, delta)
79+ vdb_field.write('output.vdb')
80+
81+ Or use directly from Grid::
82+
83+ g = Grid(...)
84+ g.export('output.vdb', format='vdb')
85+
86+ """
87+
88+ def __init__ (self , name = 'density' ):
89+ """Initialize an OpenVDB field.
90+
91+ Parameters
92+ ----------
93+ name : str
94+ Name of the grid (will be visible in Blender)
95+
96+ """
97+ if vdb is None :
98+ raise ImportError (
99+ "pyopenvdb is required to write VDB files. "
100+ )
101+ self .name = name
102+ self .grid = None
103+ self .origin = None
104+ self .delta = None
105+
106+ def populate (self , grid , origin , delta ):
107+ """Populate the field with grid data.
108+
109+ Parameters
110+ ----------
111+ grid : numpy.ndarray
112+ 3D numpy array with the data
113+ origin : numpy.ndarray
114+ Coordinates of the center of grid cell [0,0,0]
115+ delta : numpy.ndarray
116+ Grid spacing (can be 1D array or diagonal matrix)
117+
118+ Raises
119+ ------
120+ ValueError
121+ If grid is not 3D
122+
123+ """
124+ grid = numpy .asarray (grid )
125+ if grid .ndim != 3 :
126+ raise ValueError (
127+ "OpenVDB only supports 3D grids, got {}D" .format (grid .ndim ))
128+
129+ self .grid = grid .astype (numpy .float32 ) # OpenVDB uses float32
130+ self .origin = numpy .asarray (origin )
131+
132+ # Handle delta: could be 1D array or diagonal matrix
133+ delta = numpy .asarray (delta )
134+ if delta .ndim == 2 :
135+ # Extract diagonal if it's a matrix
136+ self .delta = numpy .array ([delta [i , i ] for i in range (3 )])
137+ else :
138+ self .delta = delta
139+
140+ def write (self , filename ):
141+ """Write the field to an OpenVDB file.
142+
143+ Parameters
144+ ----------
145+ filename : str
146+ Output filename (should end in .vdb)
147+
148+ """
149+ if self .grid is None :
150+ raise ValueError ("No data to write. Use populate() first." )
151+
152+ # Create OpenVDB grid
153+ vdb_grid = vdb .FloatGrid ()
154+ vdb_grid .name = self .name
155+
156+ # Set up transform (voxel size and position)
157+ # Check for uniform spacing
158+ if not numpy .allclose (self .delta , self .delta [0 ]):
159+ warnings .warn (
160+ "Non-uniform grid spacing {}. Using average spacing." .format (
161+ self .delta ))
162+ voxel_size = float (numpy .mean (self .delta ))
163+ else :
164+ voxel_size = float (self .delta [0 ])
165+
166+ # Create linear transform with uniform voxel size
167+ transform = vdb .createLinearTransform (voxelSize = voxel_size )
168+
169+ # OpenVDB transform is at corner of voxel [0,0,0],
170+ # but GridDataFormats origin is at center of voxel [0,0,0]
171+ corner_origin = self .origin - 0.5 * self .delta
172+ transform .translate (corner_origin )
173+ vdb_grid .transform = transform
174+
175+ # Set background value for sparse storage
176+ vdb_grid .background = 0.0
177+
178+ # Populate the grid
179+
180+ accessor = vdb_grid .getAccessor ()
181+ threshold = 1e-10
182+
183+ for i in range (self .grid .shape [0 ]):
184+ for j in range (self .grid .shape [1 ]):
185+ for k in range (self .grid .shape [2 ]):
186+ value = float (self .grid [i , j , k ])
187+ if abs (value ) > threshold :
188+ accessor .setValueOn ((i , j , k ), value )
189+
190+ vdb .write (filename , grids = [vdb_grid ])
0 commit comments