OpenVDB  12.0.0
Houdini Cookbook

This cookbook provides code snippets that illustrate the use of some new tools that simplify the construction of operators in Houdini. It also shows how to write operators that use OpenVDB.

Contents

General operator construction

This section gives usage examples for some general helper classes that aid in the construction of Houdini operators. These helper classes are independent of OpenVDB and can be used in the implementation of any type of operator.

ParmFactory and ParmList

The ParmFactory provides a simplified interface to define the parameters of an operator. Invoking the get method on a ParmFactory produces a new PRM_Template describing a single parameter. For example,

#include <houdini_utils/ParmFactory.h>
...
PRM_Template groupParm =
houdini_utils::ParmFactory(PRM_STRING, "group", "Group")
.setTooltip("Specify a subset of the input VDB grids to be processed.")
"The subset of VDB grids to be processed"
" (see [specifying volumes|/model/volumes#group] for details"
" on selecting grids)")
.get();
PRM_Template toleranceParm =
houdini_utils::ParmFactory(PRM_FLT_J, "tolerance", "Pruning Tolerance")
.setDefault(0.01)
.setRange(PRM_RANGE_RESTRICTED, 0, PRM_RANGE_UI, 1)
.get();

Note that, using a ParmFactory, one need only specify those attributes of a parameter (setDefaults, setTooltip, etc.) that have non-default values.

By default, a parameter’s tooltip is used to describe the parameter on the Help page for its operator. More detailed documentation in Houdini’s wiki markup format can be added with setDocumentation, as in the example above. Call setDocumentation with a null pointer or with an empty string to exclude a parameter from the Help page.

ParmFactory objects may be added directly to a ParmList, which among other things ensures that the list of templates is properly null-terminated. Typically, the ParmList is populated at the time the operator is registered, as in the following example:

#include <houdini_utils/ParmFactory.h>
void
newSopOperator(OP_OperatorTable* table)
{
// Define a string-valued group name pattern parameter and add it to the list.
parms.add(houdini_utils::ParmFactory(PRM_STRING, "group", "Group")
.setTooltip("Specify a subset of the input VDB grids to be processed.")
.setChoiceList(&houdini_utils::PrimGroupMenu));
// Define a menu of verbosity levels.
parms.add(houdini_utils::ParmFactory(PRM_ORD, "verbose", "Verbosity")
.setDefault(PRMoneDefaults)
.setChoiceListItems(PRM_CHOICELIST_SINGLE, {
"quiet", "Quiet", // token, label
"verbose", "Verbose",
"verbose2", "More Verbose",
}));
// Define a menu from a dynamically-constructed list of items.
{
std::vector<std::string> items;
for (int i = 0; i < openvdb::NUM_GRID_CLASSES; ++i) {
items.push_back(openvdb::GridBase::gridClassToString(cls)); // token
items.push_back(openvdb::GridBase::gridClassToMenuName(cls)); // label
}
parms.add(houdini_utils::ParmFactory(PRM_ORD, "gridclass", "Grid Class")
.setDefault(PRMzeroDefaults)
.setChoiceListItems(PRM_CHOICELIST_SINGLE, items));
}
...
}

Switchers

The ParmList provides a convenient way of defining switchers (tab menus):

parms.beginSwitcher("switcher");
parms.addFolder("Tree Topology");
parms.add(houdini_utils::ParmFactory(PRM_HEADING, "nodes", "Nodes"));
parms.add(houdini_utils::ParmFactory(PRM_TOGGLE, "viewnodes", "View"));
...
parms.addFolder("Isosurface");
parms.add(houdini_utils::ParmFactory(PRM_HEADING, "surfacing", "Surfacing"));
parms.add(houdini_utils::ParmFactory(PRM_TOGGLE, "extractmesh", "Extract Mesh"));
...
parms.endSwitcher();

The above generates the following UI:

tabmenu.png

Switchers can also be nested:

parms.beginSwitcher("switcher");
parms.addFolder("A");
parms.beginSwitcher("nested_switcher");
parms.addFolder("1");
parms.addFolder("2");
parms.addFolder("3");
parms.endSwitcher();
parms.addFolder("B");
...
parms.endSwitcher();

Multi-Parms

Multi-parms are dynamically-sized parameters that consist of a variable number of child instances. Each child instance is defined by a second ParmList that itself consists of multiple parameters. These parameters’ tokens must include a # character, which is typically placed at the end of the token.

...
// Build the multi-parm's parameter list.
houdini_utils::ParmList multiParms;
multiParms.add(houdini_utils::ParmFactory(PRM_STRING, "gridname#", "Grid name")
.setTooltip("Specify a name for this grid."));
...
// Create the multi-parm itself.
parms.add(houdini_utils::ParmFactory(PRM_MULTITYPE_LIST, "gridlist", "Grids")
.setMultiparms(multiParms)
.setDefault(PRMoneDefaults));
...

The above generates the following UI:

multiparm.png

A multi-parm’s parameters are accessed by iterating through each child instance:

UT_String gridNameStr;
for (int i = 1, N = evalInt("gridlist", 0, 0); i <= N; ++i) {
evalStringInst("gridname#", &i, gridNameStr, 0, time);
...
}

Note that evaluating the multi-parm gives the number of child instances, and that the instances are numbered starting from one.

OpFactory

The OpFactory is used in conjunction with a ParmList to register a new operator by adding it to the OP_OperatorTable. Among other things, the OpFactory ensures that the operator’s type name follows a consistent naming scheme, that its Help URL is set correctly and that its inputs are labeled. Continuing with the earlier newSopOperator example,

#include <houdini_utils/ParmFactory.h>
void
newSopOperator(OP_OperatorTable* table)
{
...
// Register this operator.
MySOP::factory, parms, *table)
.addInput("VDB grids to process") // input 0
.addOptionalInput("Reference geometry"); // input 1
}

The first argument to the OpFactory constructor is an instance of the OpPolicy class (or a subclass thereof). OpPolicy objects allow for customization of certain behaviors of the OpFactory. The base class specifies a policy for converting an English operator name like "My SOP", which appears in menus and other UI elements, into an operator type name. The default policy is simply to call UT_String::forceValidVariableName on the English name.

If the policy does not specify the URL of a Help page, or if the OpFactory is constructed without an OpPolicy, then documentation for the operator in Houdini’s wiki markup format can be provided with setDocumentation. By default, documentation for the operator’s parameters is generated automatically and appended to the text provided with setDocumentation.

When a particular OpPolicy is to be used to register multiple operators, it might be convenient to subclass OpFactory itself and provide a constructor that automatically initializes the base class with the desired policy.

If an operator ever needs to be renamed, call OpFactory::addAlias with the old name. This will help to ensure that scene files in which the operator was saved with the old name can still be read:

houdini_utils::OpFactory("My Renamed SOP", MySOP::factory, parms, *table)
.addInput("VDB grids to process")
.addAlias("My SOP"); // old name

If the operator name changed as a result of an OpPolicy change, supply the old operator type name directly, with addAliasVerbatim:

houdini_utils::OpFactory(houdini_utils::MyNewOpPolicy(), "My SOP",
MySOP::factory, parms, *table)
.addAliasVerbatim("My_Old_SOP_Type"); // old operator type name

Among other OpFactory features, OpFactory::setObsoleteParms accepts an additional ParmList of parameters that are no longer in use but that might still exist in older scene files:

#include <houdini_utils/ParmFactory.h>
void
newSopOperator(OP_OperatorTable* table)
{
// This boolean "draw" parameter has been replaced with a multi-state
// "drawmode" parameter.
houdini_utils::ParmList obsoleteParms;
obsoleteParms.add(houdini_utils::ParmFactory(PRM_TOGGLE, "draw", "Draw"));
parms.add(houdini_utils::ParmFactory(PRM_ORD, "drawmode", "Draw")
.setChoiceListItems(PRM_CHOICELIST_SINGLE, {
"none", "Don't Draw",
"wireframe", "Draw Wireframe",
"shaded", "Draw Shaded",
}));
// Register this operator.
houdini_utils::OpFactory("My SOP", MySOP::factory, parms, *table)
.addInput("Input geometry")
.setObsoleteParms(obsoleteParms);
}

Override OP_Node::resolveObsoleteParms to convert the values of obsolete parameters into values of current parameters where appropriate.

ScopedInputLock

A ScopedInputLock locks the inputs to an operator and then automatically unlocks them when it (the lock object) goes out of scope, even if an exception is thrown.

#include <houdini_utils/ParmFactory.h>
OP_ERROR
MySOP::cookMySop(OP_Context& context)
{
try {
houdini_utils::ScopedInputLock lock(*this, context);
//
// do cook work
//
} catch (std::exception& e) {
addError(SOP_MESSAGE, e.what());
}
return error();
}

OpenVDB SOP construction

OpenVDB SOPs are derived from the SOP_NodeVDB base class which, among other things, adds guide geometry and node-specific info text.

#include <houdini_utils/ParmFactory.h>
#include <UT/UT_Interrupt.h>
class SOP_DW_OpenVDBTemplate: public openvdb_houdini::SOP_NodeVDB
{
public:
SOP_DW_OpenVDBTemplate(OP_Network*, const char* name, OP_Operator*);
~SOP_DW_OpenVDBTemplate() override {};
static OP_Node* factory(OP_Network*, const char* name, OP_Operator*);
// Return true for a given input if the connector to the input
// should be drawn dashed rather than solid.
int isRefInput(unsigned idx) const override { return (idx == 1); }
protected:
OP_ERROR cookVDBSop(OP_Context&) override;
bool updateParmsFlags() override;
};

Selecting grids

A typical SOP will have at least one group name parameter for each of its inputs. These parameters should be defined as follows (note the use of spare data to specify the input):

// Define a string-valued group name pattern parameter.
parms.add(houdini_utils::ParmFactory(PRM_STRING, "group", "Group")
.setTooltip("Specify a subset of the input VDB grids to be processed.")
.setChoiceList(&houdini_utils::PrimGroupMenu));
// Define a group name parameter associated with this operator's second input.
parms.add(houdini_utils::ParmFactory(PRM_STRING, "group", "Group")
.setTooltip("Specify a subset of the input VDB grids to be processed.")
.setSpareData(&SOP_Node::theSecondInput));

Associated with each parameter is a menu of primitive group names. Users can select one or more groups from the menu, or create new groups on the fly using Houdini’s @attr=value syntax. For example, entering @name="density*" as the group name creates a new group comprising all input primitives whose name begins with density. Entering @vdb_value_type=float creates a new group of input grid primitives whose data type is float. Users may enter multiple space-separated group names or grouping expressions.

Iterating over grids

In cookMySOP, the string value of a group parameter is used to construct a GA_PrimitiveGroup. (SOP_NodeVDB provides a convenience method, SOP_NodeVDB::matchGroup, to simplify this step and to handle errors in a standard way.) The GA_PrimitiveGroup so constructed may be iterated over using either a VdbPrimCIterator, for read-only access, or a VdbPrimIterator, for read/write access:

OP_ERROR
MySOP::cookVDBSOP(OP_Context& context)
{
try {
houdini_utils::ScopedInputLock lock(*this, context);
const fpreal time = context.getTime();
// This does a deep copy of native Houdini primitives
// but only a shallow copy of VDB grids.
duplicateSource(0, context);
// Get the group of grids to process.
UT_String groupStr;
evalString(groupStr, "group", 0, time);
const GA_PrimitiveGroup* group = matchGroup(*gdp, groupStr.toStdString());
// Get other UI parameters.
int verbose = evalInt("verbose", 0, time);
UT_AutoInterrupt progress("Processing VDB grids");
// For each VDB primitive in the selected group...
for (openvdb_houdini::VdbPrimIterator it(gdp, group); it; ++it) {
if (progress.wasInterrupted()) {
throw std::runtime_error("processing was interrupted");
}
GU_PrimVDB* vdbPrim = *it;
// Optionally get the primitive's name (or, if the name is empty,
// the primitive's index).
const UT_String gridName = it.getPrimitiveNameOrIndex();
// If this primitive's grid is shared with other primitives, make
// a deep copy of it. If the grid is not going to be modified
// in place (or when using GEOvdbProcessTypedGrid()--see below),
// skip this step.
vdbPrim->makeGridUnique();
openvdb_houdini::Grid& grid = vdbPrim->getGrid();
// Process the grid.
// (Your code goes here.)
// In cases where it is not possible to process the primitive's
// grid in place, replace the grid with a new grid:
// openvdb_houdini::GridPtr outputGrid = ...;
// vdbPrim->setGrid(*outputGrid);
}
} catch (std::exception& e) {
addError(SOP_MESSAGE, e.what());
}
return error();
}

Processing grids of different types

Recall that a Grid is a container for a transform, metadata and a Tree, and that the Tree holds voxel data of a specific type (bool, float, vec3s, etc.). Whenever possible, try to write generic grid processing code. That is, write code that can handle grids of more than one type (int, float and double, say, instead of just float) or, ideally, grids of arbitrary type.

Writing generic code can be tricky, but convenience functions exist to make the job easier. Use GEOvdbProcessTypedGrid to call a method or methods on a primitive’s grid, regardless of its type:

MyGridProcessor proc; // functor (see explanation below)
// Call a method on the primitive's grid, regardless of the grid's type.
// Note that by default, GEOvdbProcessTypedGrid() calls makeGridUnique()
// on the primitive if it is non-const.
GEOvdbProcessTypedGrid(*vdbPrim, proc);

GEOvdbProcessTypedGrid accepts a primitive and a functor (an object for which the call operator, operator()(), is defined). The functor’s call operator must be templated on a single type (the grid type) and must accept a single argument (a reference to a grid of the template type). The operator’s return value is ignored, so it’s best to declare it void. The following is a simple example of a functor that satisfies these conditions:

struct PruneOp
{
template<typename GridT>
void operator()(GridT& grid) const { grid.prune(); }
};

PruneOp can call the prune method on a grid of any type, but it’s necessary to know the specific type, because prune takes an optional tolerance argument whose type is the type of the grid’s voxel values. (Note that grids also have a pruneGrid method that doesn’t require knowledge of the voxel value type.)

Because a functor is an object, it can have member variables. This makes it possible for the functor to process more than one grid at a time, even though it gets called with only one grid. This example shows how one might compute the CSG union of two level set grids, A and B, of the same type:

#include <openvdb/tools/Composite.h> // for csgUnion()
struct CSGUnionOp
{
// Pointer to the B grid
// (non-const, because CSG operations modify both the A and B grids)
template<typename GridT>
void operator()(GridT& aGrid) const
{
// Cast the generic B grid pointer to point to a grid of
// the same type as the A grid.
if (GridT* bGrid = UTvdbGridCast<GridT>(bGridBase)) {
// Compute the union, storing the result in the A grid
// and emptying the B grid.
openvdb::tools::csgUnion(aGrid, *bGrid);
}
}
};
OP_ERROR
MySOP::cookVDBSop(OP_Context& context)
{
...
// Retrieve the A and B grids from primitives on the input detail(s)
// (non-const, because CSG operations modify both the A and B grids).
&aGrid = aPrim->getGrid(),
&bGrid = bPrim->getGrid();
// CSG operations require the A and B grids to have the same type.
if (aGrid.type() != bGrid.type()) {
addError(SOP_MESSAGE, "grids have different types");
} else {
CSGUnionOp proc;
// Hand the functor a generic pointer to the B grid
// (whose concrete type we don't know yet).
proc.bGridBase = &bGrid;
// Apply the CSG operation, overwriting grid A (and emptying grid B).
if (!GEOvdbProcessTypedGrid(*aPrim, proc)) {
addError(SOP_MESSAGE, "failed to compute CSG union");
}
...
}
}

Certain operations might make sense only for grids with scalar or with vector voxel values. Variants of GEOvdbProcessTypedGrid exist to handle those cases. For example, GEOvdbProcessTypedGridScalar and GEOvdbProcessTypedGridVec3 invoke a functor only on grids of scalar or 3-vector types, respectively, and they return false for and ignore grids of all other types.