This section provides code snippets and some complete programs that illustrate how to use OpenVDB and how to perform common tasks.
Contents
“Hello, World” for OpenVDB
This is a very simple example showing how to create a grid and access its voxels. OpenVDB supports both random access to voxels by coordinates and sequential access by means of iterators. This example illustrates both types of access:
#include <iostream>
int main()
{
std::cout << "Testing random access:" << std::endl;
openvdb::Coord xyz(1000, -200000000, 30000000);
accessor.setValue(xyz, 1.0);
std::cout << "Grid" << xyz << " = " << accessor.getValue(xyz) << std::endl;
xyz.reset(1000, 200000000, -30000000);
std::cout << "Grid" << xyz << " = " << accessor.getValue(xyz) << std::endl;
accessor.setValue(xyz, 2.0);
std::cout << "Testing sequential access:" << std::endl;
std::cout << "Grid" << iter.getCoord() << " = " << *iter << std::endl;
}
}
Output:
Testing random access:
Grid[1000, -200000000, 30000000] = 1
Grid[1000, 200000000, -30000000] = 0
Testing sequential access:
Grid[-2147483648, -2147483648, -2147483648] = 3
Grid[1000, -200000000, 30000000] = 1
Grid[1000, 200000000, -30000000] = 2
Grid[2147483647, 2147483647, 2147483647] = 4
Creating and writing a grid
This example is a complete program that illustrates some of the basic steps to create grids and write them to disk. (See Populating a grid with values, below, for the implementation of the makeSphere function.)
int main()
{
grid->setTransform(
openvdb::math::Transform::createLinearTransform(0.5));
grid->setName("LevelSetSphere");
openvdb::io::File file("mygrids.vdb");
grids.push_back(grid);
file.write(grids);
file.close();
}
The OpenVDB library includes optimized routines for many common tasks. For example, most of the steps given above are encapsulated in the function tools::createLevelSetSphere, so that the above can be written simply as follows:
int main()
{
openvdb::tools::createLevelSetSphere<openvdb::FloatGrid>(
0.5, 4.0);
grid->setName("LevelSetSphere");
openvdb::io::File("mygrids.vdb").write({grid});
}
Populating a grid with values
The following code is templated so as to operate on grids containing values of any scalar type, provided that the value type supports negation and comparison. Note that this algorithm is only meant as an example and should never be used in production; use the much more efficient routines in tools/LevelSetSphere.h instead.
See Generic programming for more on processing grids of arbitrary type.
template<class GridType>
void
{
using ValueT = typename GridType::ValueType;
const ValueT outside = grid.background();
const ValueT inside = -outside;
int dim = int(radius + padding);
typename GridType::Accessor accessor = grid.getAccessor();
openvdb::Coord ijk;
int &i = ijk[0], &j = ijk[1], &k = ijk[2];
for (i = c[0] - dim; i < c[0] + dim; ++i) {
for (j = c[1] - dim; j < c[1] + dim; ++j) {
for (k = c[2] - dim; k < c[2] + dim; ++k) {
ValueT val = ValueT(dist);
if (val < inside || outside < val) continue;
accessor.setValue(ijk, val);
}
}
}
}
Reading and modifying a grid
openvdb::io::File file("mygrids.vdb");
file.open();
for (openvdb::io::File::NameIterator nameIter = file.beginName();
nameIter != file.endName(); ++nameIter)
{
if (nameIter.gridName() == "LevelSetSphere") {
baseGrid = file.readGrid(nameIter.gridName());
} else {
std::cout << "skipping grid " << nameIter.gridName() << std::endl;
}
}
file.close();
const float outside = grid->background();
const float width = 2.0 * outside;
float dist = iter.getValue();
iter.setValue((outside - dist) / width);
}
if (iter.getValue() < 0.0) {
iter.setValue(1.0);
iter.setValueOff();
}
}
Stream I/O
The io::Stream class allows grids to be written to and read from streams that do not support random access, with the restriction that all grids must be written or read at once. (With io::File, grids can be read individually by name, provided that they were originally written with io::File, rather than streamed to a file.)
grids->push_back(...);
std::ostringstream ostr(std::ios_base::binary);
openvdb::io::Stream(ostr).write(*grids);
std::ofstream ofile("mygrids.vdb", std::ios_base::binary);
openvdb::io::Stream(ofile).write(*grids);
std::istringstream istr(ostr.str(), std::ios_base::binary);
openvdb::io::Stream strm(istr);
grids = strm.getGrids();
std::ifstream ifile("mygrids.vdb", std::ios_base::binary);
grids = openvdb::io::Stream(ifile).getGrids();
Handling metadata
Metadata of various types (string, floating point, integer, etc.—see Metadata.h for more) can be attached both to individual Grids and to files on disk. The examples that follow refer to Grids, but the usage is the same for the MetaMap that can optionally be supplied to a file or stream for writing.
Adding metadata
The Grid::insertMeta method either adds a new (name, value) pair if the name is unique, or overwrites the existing value if the name matches an existing one. An existing value cannot be overwritten with a new value of a different type; the old metadata must be removed first.
Retrieving metadata
Call Grid::metaValue to retrieve the value of metadata of a known type. For example,
std::string s = grid->metaValue<std::string>("vector type");
float r = grid->metaValue<float>("radius");
float center = grid->metaValue<float>("center");
Grid::beginMeta and Grid::endMeta return std::map iterators over all of the metadata associated with a grid:
iter != grid->endMeta(); ++iter)
{
const std::string& name = iter->first;
std::string valueAsString = value->str();
std::cout << name << " = " << valueAsString << std::endl;
}
If the type of the metadata is not known, use the index operator to retrieve a shared pointer to a generic Metadata object, then query its type:
std::cout << metadata->typeName() << std::endl;
}
Removing metadata
Grid::removeMeta removes metadata by name. If the given name is not found, the call has no effect.
grid->removeMeta("vector type");
grid->removeMeta("center");
grid->removeMeta("vector type");
Iteration
Node Iterator
A Tree::NodeIter visits each node in a tree exactly once. In the following example, the tree is known to have a depth of 4; see the Overview for a discussion of why node iteration can be complicated when the tree depth is not known. There are techniques (beyond the scope of this Cookbook) for operating on trees of arbitrary depth.
using TreeType = GridType::TreeType;
using RootType = TreeType::RootNodeType;
assert(RootType::LEVEL == 3);
using Int1Type = RootType::ChildNodeType;
using Int2Type = Int1Type::ChildNodeType;
using LeafType = TreeType::LeafNodeType;
GridType::Ptr grid = ...;
for (TreeType::NodeIter iter = grid->tree().beginNode(); iter; ++iter) {
switch (iter.getDepth()) {
case 0: { RootType* node = nullptr; iter.getNode(node); if (node) ...; break; }
case 1: { Int1Type* node = nullptr; iter.getNode(node); if (node) ...; break; }
case 2: { Int2Type* node = nullptr; iter.getNode(node); if (node) ...; break; }
case 3: { LeafType* node = nullptr; iter.getNode(node); if (node) ...; break; }
}
}
Leaf Node Iterator
A Tree::LeafIter visits each leaf node in a tree exactly once.
using TreeType = GridType::TreeType;
GridType::Ptr grid = ...;
for (TreeType::LeafCIter iter = grid->tree().cbeginLeaf(); iter; ++iter) {
const TreeType::LeafNodeType& leaf = *iter;
...
}
for (TreeType::LeafIter iter = grid->tree().beginLeaf(); iter; ++iter) {
TreeType::LeafNodeType& leaf = *iter;
...
}
for (TreeType::LeafCIter iter = grid->tree().cbeginLeaf(); iter; ++iter) {
const TreeType::LeafNodeType* leaf = iter.getLeaf();
...
}
for (TreeType::LeafIter iter = grid->tree().beginLeaf(); iter; ++iter) {
TreeType::LeafNodeType* leaf = iter.getLeaf();
...
}
See the Overview for more on leaf node iterators.
Value Iterator
A Tree::ValueIter visits each value (both tile and voxel) in a tree exactly once. Iteration can be unrestricted or can be restricted to only active values or only inactive values. Note that tree-level value iterators (unlike the node iterators described above) can be accessed either through a grid's tree or directly through the grid itself, as in the following example:
using TreeType = GridType::TreeType;
GridType::Ptr grid = ...;
for (GridType::ValueOnCIter iter = grid->cbeginValueOn(); iter.test(); ++iter) {
if (iter.isVoxelValue()) {
std::cout << iter.getCoord() << std::endl;
} else {
openvdb::CoordBBox bbox;
iter.getBoundingBox(bbox);
std::cout << bbox << std::endl;
}
}
}
for (GridType::ValueOffIter iter = grid->beginValueOff(); iter.test(); ++iter) {
iter.setValue(value);
}
See the Overview for more on value iterators.
Iterator Range
A tree::IteratorRange wraps any grid or tree iterator and gives the iterator TBB splittable range semantics, so that it can be used as the Range argument to functions like tbb::parallel_for and tbb::parallel_reduce. (This is in fact how tools::foreach and tools::transformValues are implemented; see Value transformation, below, for more on those functions.) There is some overhead to splitting, since grid and tree iterators are not random-access, but the overhead should typically be negligible compared with the amount of work done per subrange.
The following is a complete program that uses tree::IteratorRange. The program iterates in parallel over the leaf nodes of a tree (by splitting the iteration range of a Tree::LeafCIter) and computes the total number of active leaf-level voxels by incrementing a global, thread-safe counter.
#include <tbb/parallel_for.h>
#include <atomic>
#include <cassert>
#include <iostream>
std::atomic<openvdb::Index64> activeLeafVoxelCount;
template<typename GridType>
struct LeafProcessor
{
using TreeType = typename GridType::TreeType;
using LeafNode = typename TreeType::LeafNodeType;
using IterRange = openvdb::tree::IteratorRange<typename TreeType::LeafCIter>;
void operator()(IterRange& range) const
{
for ( ; range; ++range) {
const LeafNode& leaf = *range.iterator();
activeLeafVoxelCount.fetch_add(leaf.onVoxelCount());
}
}
};
int
main()
{
openvdb::tools::createLevelSetSphere<openvdb::FloatGrid>(20.0,
using FloatLeafProc = LeafProcessor<openvdb::FloatGrid>;
FloatLeafProc::IterRange range(grid->tree().cbeginLeaf());
tbb::parallel_for(range, proc);
std::cout << activeLeafVoxelCount << " active leaf voxels" << std::endl;
assert(activeLeafVoxelCount == grid->activeVoxelCount());
}
Interpolation of grid values
Applications such as rendering require evaluation of grids at arbitrary, fractional coordinates in either index or world space. This is achieved, of course, by interpolating between known grid values at neighboring whole-voxel locations, that is, at integer coordinates in index space. The following sections introduce OpenVDB’s various interpolation schemes as well as the Grid Sampler and Dual Grid Sampler classes for efficient, continuous sampling of grids. In most cases, GridSampler is the preferred interface for interpolation, but note that when a fixed transform is to be applied to all values in a grid (that is, the grid is to be resampled), it is both easier and more efficient to use the multithreaded GridTransformer class, introduced in Transforming grids.
Index-space samplers
OpenVDB offers low-level zero-, first- and second-order interpolators PointSampler, BoxSampler and QuadraticSampler, in addition to the variants StaggeredPointSampler, StaggeredBoxSampler and StaggeredQuadraticSampler for staggered velocity grids.
GridType::ValueType v0 = openvdb::tools::PointSampler::sample(grid.tree(), ijk);
GridType::ValueType v1 = openvdb::tools::BoxSampler::sample(grid.tree(), ijk);
GridType::ValueType v2 = openvdb::tools::QuadraticSampler::sample(grid.tree(), ijk);
These examples invoke the getValue method on the grid’s tree to fetch sample values in the neighborhood of (i,  j,  k). Accessing values via the tree is thread-safe due to the lack of caching, but for that reason it is also suboptimal. For better performance, use value accessors (but be careful to use one accessor per computational thread):
GridType::ConstAccessor accessor = grid.getConstAccessor();
GridType::ValueType v0 = openvdb::tools::PointSampler::sample(accessor, ijk);
GridType::ValueType v1 = openvdb::tools::BoxSampler::sample(accessor, ijk);
GridType::ValueType v2 = openvdb::tools::QuadraticSampler::sample(accessor, ijk);
Another issue with these low-level interpolators is that they operate only in index space. To interpolate in world space, use the higher-level classes discussed below.
Grid Sampler
The GridSampler class allows for continuous sampling in both world space and index space and can be used with grids, trees or value accessors.
openvdb::tools::GridSampler<GridType, openvdb::tools::BoxSampler> sampler(grid);
GridType::ValueType indexValue = sampler.isSample(
openvdb::Vec3R(10.5, -100.2, 50.3));
GridType::ValueType worldValue = sampler.wsSample(
openvdb::Vec3R(0.25, 1.4, -1.1));
GridType::ConstAccessor accessor = grid.getConstAccessor();
openvdb::tools::GridSampler<GridType::ConstAccessor, openvdb::tools::BoxSampler>
fastSampler(accessor, grid.transform());
indexValue = fastSampler.isSample(
openvdb::Vec3R(10.5, -100.2, 50.3));
Note that when constructing a GridSampler with either a tree or a value accessor, you must also supply an index-to-world transform. When constructing a GridSampler with a grid, the grid's transform is used automatically.
Dual Grid Sampler
It might sometimes be necessary to interpolate values from a source grid into the index space of a target grid. If this transformation is to be applied to all of the values in the source grid, then it is best to use the tools in GridTransformer.h. For other cases, consider using the DualGridSampler class. Like the GridSampler class, this class can be used with grids, trees or value accessors. In addition, DualGridSampler checks if the source and target grids are aligned (that is, they have the same transform), in which case it avoids unnecessary interpolation.
openvdb::tools::DualGridSampler<GridType, openvdb::tools::BoxSampler>
sampler(sourceGrid, targetGrid.constTransform());
GridType::ValueType value = sampler(openvdb::Coord(-23, -50, 202));
GridType::ConstAccessor accessor = sourceGrid.getConstAccessor();
openvdb::tools::DualGridSampler<GridType::ConstAccessor, openvdb::tools::BoxSampler>
fastSampler(accessor, sourceGrid.constTransform(), targetGrid.constTransform());
value = fastSampler(openvdb::Coord(-23, -50, 202));
Note that interpolation is done by invoking a DualGridSampler as a functor, in contrast to the more general-purpose GridSampler.
Transforming grids
Geometric transformation
A GridTransformer applies a geometric transformation to an input grid using one of several sampling schemes, and stores the result in an output grid. The operation is multithreaded by default, though threading can be disabled by calling setThreaded(false). A GridTransformer object can be reused to apply the same transformation to multiple input grids, optionally using different sampling schemes.
sourceGrid = ...
targetGrid = ...;
const openvdb::math::Transform
&sourceXform = sourceGrid->transform(),
&targetXform = targetGrid->transform();
sourceXform.baseMap()->getAffineMap()->getMat4() *
targetXform.baseMap()->getAffineMap()->getMat4().
inverse();
openvdb::tools::GridTransformer transformer(xform);
*sourceGrid, *targetGrid);
*sourceGrid, *targetGrid);
*sourceGrid, *targetGrid);
targetGrid->
tree().prune();
Value transformation
This example uses tools::foreach to multiply all values (both tile and voxel and both active and inactive) of a scalar, floating-point grid by two:
struct Local {
iter.setValue(*iter * 2);
}
};
This example uses tools::foreach to rotate all active vectors of a vector-valued grid by 45° about the y axis:
struct MatMul {
iter.setValue(M.transform(*iter));
}
};
tools::transformValues is similar to tools::foreach, but it populates an output grid with transformed values from an input grid that may have a different value type. The following example populates a scalar, floating-point grid with the lengths of all active vectors from a vector-valued grid (like tools::magnitude):
struct Local {
{
if (iter.isVoxelValue()) {
accessor.setValue(iter.getCoord(), iter->length());
} else {
openvdb::CoordBBox bbox;
iter.getBoundingBox(bbox);
accessor.getTree().fill(bbox, iter->length());
}
}
};
Combining grids
The following examples show various ways in which a pair of grids can be combined in index space. The assumption is that index coordinates (i,  j,  k) in both grids correspond to the same physical, world space location. When the grids have different transforms, it is usually necessary to first resample one grid into the other grid's index space.
Level set CSG operations
The level set CSG functions in tools/Composite.h operate on pairs of grids of the same type, using sparse traversal for efficiency. These operations always leave the second grid empty.
copyOfGridA = gridA->deepCopy(),
copyOfGridB = gridB->deepCopy();
gridA = copyOfGridA->deepCopy();
gridB = copyOfGridB->deepCopy();
gridA = copyOfGridA->deepCopy();
gridB = copyOfGridB->deepCopy();
Compositing operations
Like the CSG operations, the compositing functions in tools/Composite.h operate on pairs of grids of the same type, and they always leave the second grid empty.
copyOfGridA = gridA->deepCopy(),
copyOfGridB = gridB->deepCopy();
gridA = copyOfGridA->deepCopy();
gridB = copyOfGridB->deepCopy();
gridA = copyOfGridA->deepCopy();
gridB = copyOfGridB->deepCopy();
gridA = copyOfGridA->deepCopy();
gridB = copyOfGridB->deepCopy();
Generic combination
The Tree::combine family of methods apply a user-supplied operator to pairs of corresponding values of two trees. These methods are efficient because they take into account the sparsity of the trees; they are not multithreaded, however.
This example uses the Tree::combine method to compute the difference between corresponding voxels of two floating-point grids:
struct Local {
static inline void diff(const float& a, const float& b, float& result) {
result = a - b;
}
};
aGrid->tree().combine(bGrid->tree(), Local::diff);
Another Tree::combine example, this time using a functor to preserve state:
struct Blend {
Blend(float f): frac(f) {}
inline void operator()(const float& a, const float& b, float& result) const {
result = frac * a + (1.0 - frac) * b;
}
float frac;
};
aGrid->tree().combine(bGrid->tree(), Blend(0.25));
The Tree::combineExtended method invokes a function of the form void f(CombineArgs<T>& args)
, where the CombineArgs object encapsulates an a and a b value and their active states as well as a result value and its active state. In the following example, voxel values in floating-point aGrid are replaced with corresponding values from floating-point bGrid (leaving bGrid empty) wherever the b values are larger. The active states of any transferred values are preserved.
struct Local {
static inline void max(CombineArgs<float>& args) {
if (args.b() > args.a()) {
args.setResult(args.b());
args.setResultIsActive(args.bIsActive());
} else {
args.setResult(args.a());
args.setResultIsActive(args.aIsActive());
}
}
};
aGrid->tree().combineExtended(bGrid->tree(),
Local::max);
Like combine, Tree::combine2 applies an operation to pairs of corresponding values of two trees. However, combine2 writes the result to a third, output tree and does not modify either of the two input trees. (As a result, it is less space-efficient than the combine method.) Here, the voxel differencing example above is repeated using combine2:
#include
struct Local {
static inline void diff(const float& a, const float& b, float& result) {
result = a - b;
}
};
resultGrid->tree().combine2(aGrid->tree(), bGrid->tree(), Local::diff);
An extended combine2 is also available.
Generic programming
Calling Grid methods
A common task is to perform some operation on all of the grids in a file, where the operation involves Grid method calls and the grids are of different types. Only a handful of Grid methods, such as activeVoxelCount, are virtual and can be called through a GridBase pointer; most are not, because they require knowledge of the Grid's value type. For example, one might want to prune the trees of all of the grids in a file regardless of their type, but Tree::prune is non-virtual because it accepts an optional pruning tolerance argument whose type is the grid's value type.
The processTypedGrid function below makes this kind of task easier. It is called with a GridBase pointer and a functor whose call operator accepts a pointer to a Grid of arbitrary type. The call operator should be templated on the grid type and, if necessary, overloaded for specific grid types.
template<typename OpType>
{
#define CALL_OP(GridType) \
op.template operator()<GridType>(openvdb::gridPtrCast<GridType>(grid))
#undef CALL_OP
}
The following example shows how to use processTypedGrid to implement a generic pruning operation for grids of all built-in types:
struct PruneOp {
double tolerance;
PruneOp(double t): tolerance(t) {}
template<typename GridType>
void operator()(typename GridType::Ptr grid) const
{
grid->tree().prune(typename GridType::ValueType(tolerance));
}
};
openvdb::io::File file("mygrids.vdb");
file.open();
file.close();
const PruneOp pruner(0.01);
iter != myGrids->end(); ++iter)
{
processTypedGrid(grid, pruner);
}
“Hello, World” for OpenVDB Points
This is a simple example showing how to convert a few points, perform I/O and iterate over them to extract their world-space positions.
For more information about using OpenVDB to store point data, see the OpenVDB Points Documentation.
#include <iostream>
#include <vector>
int main()
{
std::vector<openvdb::Vec3R> positions;
openvdb::points::PointAttributeVector<openvdb::Vec3R> positionsWrapper(positions);
int pointsPerVoxel = 8;
float voxelSize =
std::cout << "VoxelSize=" << voxelSize << std::endl;
openvdb::math::Transform::Ptr transform =
openvdb::math::Transform::createLinearTransform(voxelSize);
openvdb::points::PointDataGrid::Ptr grid =
grid->setName("Points");
openvdb::io::File("mypoints.vdb").write({grid});
openvdb::io::File newFile("mypoints.vdb");
newFile.open();
newFile.close();
grid = openvdb::gridPtrCast<openvdb::points::PointDataGrid>(baseGrid);
std::cout << "PointCount=" << count << std::endl;
for (auto leafIter = grid->tree().cbeginLeaf(); leafIter; ++leafIter) {
std::cout << "Leaf" << leafIter->origin() << std::endl;
const openvdb::points::AttributeArray& array =
leafIter->constAttributeArray("P");
openvdb::points::AttributeHandle<openvdb::Vec3f> positionHandle(array);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
const openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
grid->transform().indexToWorld(voxelPosition + xyz);
std::cout << "* PointIndex=[" << *indexIter << "] ";
std::cout << "WorldPosition=" << worldPosition << std::endl;
}
}
}
Output:
VoxelSize=3.34716
PointCount=4
Leaf[0, 0, -8]
Leaf[0, 0, 0]
PointIndex=[0] WorldPosition=[0, 1, 0]
PointIndex=[1] WorldPosition=[1.1, 1.25, 0.06]
PointIndex=[2] WorldPosition=[1.5, 3.5, 1]
Converting Point Attributes
This example is the same as the “Hello, World” for OpenVDB Points example, however it demonstrates converting radius in addition to position. It uses a tailored attribute compression for the radius to demonstrate how to reduce memory.
These methods heavily rely on the point conversion methods contained in points/PointConversion.h.
#include <iostream>
#include <vector>
int main()
{
std::vector<openvdb::Vec3R> positions;
std::vector<float> radius;
radius.push_back(0.1);
radius.push_back(0.15);
radius.push_back(0.2);
radius.push_back(0.5);
openvdb::points::PointAttributeVector<openvdb::Vec3R> positionsWrapper(positions);
int pointsPerVoxel = 8;
float voxelSize =
openvdb::math::Transform::Ptr transform =
openvdb::math::Transform::createLinearTransform(voxelSize);
openvdb::tools::PointIndexGrid::Ptr pointIndexGrid =
openvdb::tools::createPointIndexGrid<openvdb::tools::PointIndexGrid>(
positionsWrapper, *transform);
openvdb::points::PointDataGrid::Ptr grid =
using Codec = openvdb::points::FixedPointCodec<
false,
openvdb::points::UnitRange>;
openvdb::points::TypedAttributeArray<float, Codec>::registerType();
openvdb::points::TypedAttributeArray<float, Codec>::attributeType();
openvdb::points::PointAttributeVector<float> radiusWrapper(radius);
grid->tree(), pointIndexGrid->tree(), "pscale", radiusWrapper);
grid->setName("Points");
for (auto leafIter = grid->tree().cbeginLeaf(); leafIter; ++leafIter) {
std::cout << "Leaf" << leafIter->origin() << std::endl;
const openvdb::points::AttributeArray& positionArray =
leafIter->constAttributeArray("P");
const openvdb::points::AttributeArray& radiusArray =
leafIter->constAttributeArray("pscale");
openvdb::points::AttributeHandle<openvdb::Vec3f> positionHandle(positionArray);
openvdb::points::AttributeHandle<float> radiusHandle(radiusArray);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
grid->transform().indexToWorld(voxelPosition + xyz);
float radius = radiusHandle.get(*indexIter);
std::cout << "* PointIndex=[" << *indexIter << "] ";
std::cout << "WorldPosition=" << worldPosition << " ";
std::cout << "Radius=" << radius << std::endl;
}
}
}
Output:
Leaf[0, 0, -8]
PointIndex=[0] WorldPosition=[-1, 6, -2] Radius=0.2
Leaf[0, 0, 0]
PointIndex=[0] WorldPosition=[0, 1, 0] Radius=0.0999924
PointIndex=[1] WorldPosition=[1.1, 1.25, 0.06] Radius=0.499992
PointIndex=[2] WorldPosition=[1.5, 3.5, 1] Radius=0.149996
Random Point Generation
This example demonstrates how to create a new point grid and to populate it with random point positions initialized inside a level set sphere.
#include <iostream>
int main()
{
openvdb::tools::createLevelSetSphere<openvdb::FloatGrid>(20.0,
openvdb::points::PointDataTree::Ptr pointTree(
pointTree->voxelizeActiveTiles();
using PositionAttribute = openvdb::points::TypedAttributeArray<
openvdb::Vec3f,
openvdb::points::FixedPointCodec<false>>;
openvdb::points::AttributeSet::Descriptor::Ptr descriptor(
openvdb::points::AttributeSet::Descriptor::create(positionType));
openvdb::Index voxelsPerLeaf = openvdb::points::PointDataGrid::TreeType::LeafNodeType::SIZE;
for (auto leafIter = pointTree->beginLeaf(); leafIter; ++leafIter) {
leafIter->initializeAttributes(descriptor, pointsPerLeaf);
offset += pointsPerVoxel;
leafIter->setOffsetOn(index, offset);
}
}
openvdb::points::PointDataGrid::Ptr points =
openvdb::points::PointDataGrid::create(pointTree);
points->setName("Points");
points->setTransform(sphereGrid->transform().copy());
std::mt19937 generator(0);
std::uniform_real_distribution<> distribution(-0.5, 0.5);
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
openvdb::points::AttributeArray& array = leafIter->attributeArray("P");
openvdb::points::AttributeWriteHandle<openvdb::Vec3f> handle(array);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
openvdb::Vec3f positionVoxelSpace(distribution(generator));
handle.set(*indexIter, positionVoxelSpace);
}
}
std::cout << "LeafCount=" << leafCount << std::endl;
std::cout << "PointCount=" << count << std::endl;
}
Output:
LeafCount=660
PointCount=2703360
Point Iteration, Groups and Filtering
This section demonstrates how to iterate over points and to use point groups and custom filters during iteration.
See the documentation describing iteration and filtering under OpenVDB Points Iteration for more information.
Point Iteration
Iterating over point attribute data is most easily done by iterating over the leaf nodes of a PointDataGrid and then the index indices of the attribute within the leaf and extracting the values from a handle bound to the attribute stored within the leaf.
This example demonstrates single-threaded, read-only iteration over all float values of an attribute called "name".
for (auto leafIter = pointTree.beginLeaf(); leafIter; ++leafIter) {
openvdb::points::AttributeArray& array =
leafIter->constAttributeArray("name");
openvdb::points::AttributeHandle<float> handle(array);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
float value = handle.get(*indexIter);
}
}
This example demonstrates single-threaded, read-write iteration for a similar float attribute by setting all values to be 5.0f.
for (auto leafIter = pointTree.beginLeaf(); leafIter; ++leafIter) {
openvdb::points::AttributeArray& array =
leafIter->attributeArray("name");
openvdb::points::AttributeWriteHandle<float> handle(array);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
handle.set(*indexIter, 5.0f);
}
}
Here is the same read-only example using TBB and a custom operator for reading values using multi-threaded access.
In this example, we also find the index of the attribute in the descriptor to avoid having to look this up each time (assuming that all leaf nodes share the same descriptor).
A similar approach can be used for multi-threaded writing.
struct ReadValueOp
{
void operator()(const openvdb::tree::LeafManager<
openvdb::points::PointDataTree>::LeafRange& range) const {
for (auto leafIter = range.begin(); leafIter; ++leafIter) {
for (auto indexIter = leafIter->beginIndexOn();
indexIter; ++indexIter) {
const openvdb::points::AttributeArray& array =
leafIter->constAttributeArray(mIndex);
openvdb::points::AttributeHandle<float> handle(array);
float value = handle.get(*indexIter);
}
}
}
};
auto leafIter = pointTree.cbeginLeaf();
if (!leafIter) {
std::cerr << "No Leaf Nodes" << std::endl;
}
auto descriptor = leafIter->attributeSet().descriptor();
if (index == openvdb::points::AttributeSet::INVALID_POS) {
std::cerr << "Invalid Attribute" << std::endl;
}
openvdb::tree::LeafManager<openvdb::points::PointDataTree> leafManager(
pointsTree);
tbb::parallel_for(leafManager.leafRange(),
op);
Tip: To run a multi-threaded operator as single-threaded for debugging, set the grainsize argument to a number larger than the number of leaf nodes (it defaults to 1).
tbb::parallel_for(leafManager.leafRange(1000000),
op);
Creating and Assigning Point Groups
Point groups in OpenVDB are analagous to Houdini point groups as an efficient way of tagging specific points to belong to a named group.
This example uses the data set generated in the Random Point Generation example.
openvdb::points::groupPointCount(points->tree(), "positiveY");
std::cout << "PointCount=" << count << std::endl;
std::cout << "EmptyGroupPointCount=" << groupCount << std::endl;
auto leafIter = points->tree().beginLeaf();
if (!leafIter) {
std::cerr << "No Leaf Nodes" << std::endl;
}
openvdb::points::AttributeSet::Descriptor::GroupIndex groupIndex =
leafIter->attributeSet().groupIndex("positiveY");
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
const openvdb::points::AttributeArray& positionArray =
leafIter->constAttributeArray("P");
openvdb::points::AttributeHandle<openvdb::Vec3f> positionHandle(
positionArray);
openvdb::points::GroupWriteHandle groupHandle =
leafIter->groupWriteHandle("positiveY");
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
openvdb::Vec3f voxelPosition = positionHandle.get(*indexIter);
openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
openvdb::Vec3f worldPosition =
points->transform().indexToWorld(voxelPosition + xyz);
if (worldPosition.
y() > 0.0f) {
groupHandle.set(*indexIter, true);
}
}
groupHandle.compact();
}
groupCount = openvdb::points::groupPointCount(points->tree(), "positiveY");
std::cout << "GroupPointCount=" << groupCount << std::endl;
Output:
PointCount=2703360
EmptyGroupPointCount=0
GroupPointCount=1463740
Point Filtering using Groups
One highly useful feature of groups is to be able to use them for performing filtered iteration.
Here is an example iterating over all the points in the same data set to compute the average position in Y.
double averageY(0.0);
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
const openvdb::points::AttributeArray& positionArray =
leafIter->constAttributeArray("P");
openvdb::points::AttributeHandle<openvdb::Vec3f> positionHandle(
positionArray);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
openvdb::Vec3f voxelPosition = positionHandle.get(*indexIter);
openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
openvdb::Vec3f worldPosition =
points->transform().indexToWorld(voxelPosition + xyz);
averageY += worldPosition.
y();
iterationCount++;
}
}
averageY /= double(count);
std::cout << "IterationCount=" << iterationCount << std::endl;
std::cout << "AveragePositionInY=" << averageY << std::endl;
Output:
IterationCount=2703360
AveragePositionInY=1.89564
And the same example filtering using the "positiveY" group during iteration.
iterationCount = 0;
double averageYPositive(0.0);
openvdb::points::GroupFilter filter("positiveY");
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
const openvdb::points::AttributeArray& positionArray =
leafIter->constAttributeArray("P");
openvdb::points::AttributeHandle<openvdb::Vec3f> positionHandle(
positionArray);
for (auto indexIter = leafIter->beginIndexOn(filter); indexIter; ++indexIter) {
openvdb::Vec3f voxelPosition = positionHandle.get(*indexIter);
openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
openvdb::Vec3f worldPosition =
points->transform().indexToWorld(voxelPosition + xyz);
averageYPositive += worldPosition.
y();
iterationCount++;
}
}
averageYPositive /= double(groupCount);
std::cout << "IterationCount=" << iterationCount << std::endl;
std::cout << "AveragePositivePositionInY=" << averageYPositive << std::endl;
Output:
IterationCount=1463740
AveragePositivePositionInY=11.373
This approach still performs this operation in two passes, (1) creating and assigning the groups and (2) iterating using the group.
Point Filtering using Custom Filters
For common operations, it is typically faster to sacrifice the flexibility of point groups for a custom filter. This is using the same data set from the previous example.
struct PositiveYFilter
{
using Handle = openvdb::points::AttributeHandle<openvdb::Vec3f>;
explicit PositiveYFilter(const openvdb::math::Transform& transform)
: mTransform(transform) { }
PositiveYFilter(const PositiveYFilter& filter)
: mTransform(filter.mTransform)
{
if (filter.mPositionHandle) {
mPositionHandle.reset(new Handle(*filter.mPositionHandle));
}
}
inline bool initialized()
const {
return bool(mPositionHandle); }
template <typename LeafT>
void reset(const LeafT& leaf) {
mPositionHandle.reset(new Handle(leaf.constAttributeArray("P")));
}
template <typename IterT>
bool valid(const IterT& indexIter) const {
openvdb::Vec3f voxelPosition = mPositionHandle->get(*indexIter);
openvdb::Vec3d xyz = indexIter.getCoord().asVec3d();
openvdb::Vec3f worldPosition =
mTransform.indexToWorld(voxelPosition + xyz);
return worldPosition.
y() > 0.0f;
}
const openvdb::math::Transform& mTransform;
Handle::UniquePtr mPositionHandle;
};
PositiveYFilter positiveYFilter(points->transform());
iterationCount = 0.0;
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
for (auto indexIter = leafIter->beginIndexOn(positiveYFilter);
indexIter; ++indexIter) {
iterationCount++;
}
}
std::cout << "IterationCount=" << iterationCount << std::endl;
Output:
Strided Point Attributes
Point attributes can have a stride greater than one in order to store multiple values with each attribute with each point.
Constant Stride Attributes
A stride can be constant so that each attribute has the same number of values. This example demonstrates using a hard-coded 10 samples per point in an attribute called "samples".
openvdb::points::TypedAttributeArray<float>::attributeType(), stride);
for (auto leafIter = points->tree().beginLeaf(); leafIter; ++leafIter) {
openvdb::points::AttributeArray& array(
leafIter->attributeArray("samples"));
openvdb::points::AttributeWriteHandle<float> handle(array);
for (auto indexIter = leafIter->beginIndexOn(); indexIter; ++indexIter) {
for (int i = 0; i < 10; i++) {
handle.set(*indexIter, i, float(i));
}
}
}
Moving Points in Space
As points are stored within voxels in an implicit spatially organised data structure, moving points in space requires re-bucketing the data.
Advecting Points
Advection uses a specified integration order (4 = runge-kutta 4th) as well as delta time and time-step parameters to advect the points in-place using the supplied velocity grid.
Moving Points with a Custom Deformer
A custom deformer generates the new position of each existing point in a point set. This can use any number of mechanisms to achieve this such as a static value, a hard-coded list of positions, a function that uses the existing position to compute the new one or a function that uses the index of the point within the leaf array in some other way. This example simply takes the input position and adds a Y offset. Note that it is also possible to configure a custom deformer to operate in index-space.
struct OffsetDeformer
{
OffsetDeformer(const openvdb::Vec3d& _offset)
: offset(_offset){ }
template <typename LeafIterT>
void reset(const LeafIterT&) { }
template <typename IndexIterT>
void apply(openvdb::Vec3d& position, const IndexIterT&) const
{
position += offset;
}
openvdb::Vec3d offset;
};
openvdb::Vec3d offset(0, -10, 0);
OffsetDeformer deformer(offset);