HDK
|
The main geometry libraries in Houdini are:
Geometry is stored in a container class (GU_Detail, GEO_Detail/GD_Detail, GA_Detail). The container contains attribute data arrays for points (shared between primitives), vertices (unique), primitives, and the container itself. The detail container maintains a list of GA_Primitive objects, one for each primitive in the detail. Each primitive maintains a list of vertices. Each vertex has a single reference to a point. Unlike primitives, points and vertices are described entirely by their attribute values and have no separate allocations per point or vertex.
Each element has a unique offset into the attribute arrays (GA_Offset). The GA_Offsets are immutable except under a defragment operation. That is, as elements are added or destroyed, the GA_Offset for an element will remain the same. Each element also has an "ordered" index (GA_Index). The GA_Index of an element will change if elements before it (in order) are deleted. As a note, the GA_Offset for a primitive is also used to lookup the GA_Primitive object in the detail's primitive list.
The offset/index mapping is maintained by the detail in a GA_IndexMap structure. An index map keeps track of the offsets and the indices for all the elements in a detail.
Attribute data is stored in Attribute Type Implementation (ATI) classes. Each ATI class maintains arrays of data for each element in the geometry. The ATI provides interfaces (AIFs) for accessing the data. Different ATIs are able to store data in different ways, but still provide a consistent interface to access the data. Some ATIs include GA_ATINumeric, GA_ATIString, GA_ATIIndexPair. Some AIFs include GA_AIFTuple, GA_AIFSharedStringTuple, GA_AIFInterp. Numeric and string attributes can be accessed more easily and directly using the classes in GA_Handle.h (e.g. GA_ROHandleV3, GA_RWHandleS), and GA_PageHandle.h (e.g. GA_RWPageHandleV3).
The attribute data for each element is accessed using its GA_Offset. That is, the GA_Offset represents the offset in the ATI array.
The detail class stores the attributes in an attribute set object. This object consists of four dictionaries (vertex, point, primitive and detail). Attributes are uniquely identified by name within each dictionary. That is, it's possible to have a point attribute named "width" and a primitive attribute named "width", but it is not possible to have two distinct point attributes with the same name.
Attributes have a "scope" to determine visibility. Only attributes with a public scope are presented to the user.
The container class (GA_Detail) also maintains element (point, vertex, primitive) and edge group information. Groups may be flagged as "internal" and will not be visible to users. Element groups just refer to a GA_ATIGroupBool attribute (bit array) to store membership information.
Optionally, detail can also contain topological information. In all cases, you can find out which vertices are referenced by a primitive and which points are referenced by a vertex. However, with minimal topological information (usually present in Houdini), you can also find out which vertices reference a point (multiple vertices can reference the same shared point) and also which primitive a vertex belongs to.
There are additional geometry libraries which may be of use for more specialized uses:
The main types/classes for Houdini geometry are:
This diagram shows the geometry structure for a simple mesh of triangle primitives.
The detail (GA_Detail) container contains an index map for points, (GA_Detail::getPointMap()), vertices (GA_Detail::getVertexMap()), primitives (GA_Detail::getPrimitiveMap()), and, not shown here, because it is always trivial, one for the detail itself (GA_Detail::getGlobalMap()), a collection of attributes, and a collection of groups.
Each primitive in the diagram has 3 vertices, and thus 3 unique vertex GA_Offset values (and thus 3 unique vertex GA_Index values). Note that in the diagram above, each vertex is labeled with a linear index local to each primitive with the global GA_Index appearing next to it in brackets. Each vertex references a point by GA_Offset, which we map to a corresponding GA_Index in the diagram for simplicity. In this example, points GA_Index(0) and GA_Index(3) are shared between the two polygons, while points GA_Index(1) and GA_Index(2) are each referenced only by a single polygon.
This sharing of points provides an opportunity for the vertices in a primitive to have both shared attribute data (point attributes) and unique attribute data (vertex attributes).
A geometry attribute is an instance of a subclass of GA_Attribute. These subclasses are typically referred to as ATIs (Attribute Type Implementations) because they implement a particular GA_AttributeType.
Each ATI has a common interface inherited from GA_Attribute:
P
or N
or temperature
)The list of attributes are maintained by the GA_Detail. Because the attributes are maintained at the global level, attributes are uniform across element types. That is, if one point has the Cd
attribute, then all points in the detail will have the Cd
attribute. That is not to say, however, that the attribute necessarily allocates memory for every point.
There are separate attributes maintained for each element type: point, vertex, primitive, and even detail (global). It is therefore possible to have a primitive and point attribute with the same name. For example, a detail can have a "Cd" (diffuse color) attribute on both primitive and point objects. It is up to the individual operations to resolve precedence in such cases. Generally it's typical to prefer finer grained attributes over coarser grained variants (i.e. vertex before point before primitive before detail). By default, however, trying to create a point attribute with the same name as a vertex attribute or vice versa will promote the original to the new type, to reduce problems that can emerge when point and vertex attributes have the same name. This behaviour can be overridden using a GA_ReuseStrategy.
v
and life
attributes to control birth/death of a simple particle systemNew attribute types may be created with the HDK, and so attribute types are identified by a unique token. The current standard types that might be created by a user are:
String attributes implement two main interfaces: GA_AIFStringTuple and GA_AIFSharedStringTuple. Either may be used, but it is generally more efficient to use the latter as it allows code to make assumptions about how the strings are managed. That said, many operations fall back to GA_AIFStringTuple when GA_AIFSharedStringTuple is not available, making it possible to define a custom string attribute type that does not use a shared table.
The GA_AIFSharedStringTuple interface provides access to the shared string table and the integer handles stored for each element. Some methods of interest include:
bool GA_AIFSharedStringTuple::extractStrings(const GA_Attribute *attrib, UT_StringArray &strings, UT_IntArray &handles) const
bool GA_AIFSharedStringTuple::getTableEntries(const GA_Attribute *attrib) const
GA_StringIndexType GA_AIFSharedStringTuple::getHandle(const GA_Attribute *attrib, GA_Offset ai, int tuple_index=0) const
bool GA_AIFSharedStringTuple::setHandle(const GA_Attribute *attrib, GA_Offset ai, GA_StringIndexType handle, int tuple_index) const
Of course, in order to set handles explicitly, you need to be able to add strings to the shared table. The strings in the table are reference counted, so to facilitate this, we provide GA_AIFSharedStringTuple::StringBuffer. An instance of this class can be used to add strings to the table and will clean up any dangling references when it is destroyed.
For simple operations, GA_ROHandleS, GA_RWHandleS, or GA_RWBatchHandleS are much easier to use, and faster, avoiding the virtual calls of the AIFs.
Users may note that some attributes magically create local variables in SOPs. This is not so magical.
GEO_Detail::addVariableName() creates a mapping between an attribute and a variable name. This mapping is kept as a detail string attribute. You can also call GEO_Detail::removeVariableName() to remove a mapping, or GEO_Detail::getVariableNameMap() to retrieve the list of all mappings in place. An example of this can be found in SOP/SOP_BrushHairLen.C.
You can use GEO_Detail::getVariableNameMap() and parse the results to perform local variable mapping, or, you can let the methods on SOP_Node do the work for you. SOP/SOP_Flatten.C makes use of the SOP_Node::setVariableOrder(), SOP_Node::setCurGdh() and SOP_Node::setupLocalVars(), SOP_Node::resetLocalVarRefs() methods to automatically bind attributes to their local variables.
There are various methods in the geometry libraries to interpolate attribute point or vertex attribute values within a primitive. Many of these interfaces require a destination vertex offset, even if there are only point attributes being interpolated, which would necessitate creating and destroying a temporary vertex referencing that point, which can be slow, and may require copying the entire detail if the detail is const. This approach is no longer recommended.
GEO_Primitive::computeInteriorPointWeights()
makes it possible to get an array of the source vertex offsets and an array of their corresponding weights for the interpolation. This makes it possible to do just the desired interpolation without modifying the source detail. The disadvantage is that this will not work for interpolating point positions in primitives like spheres, tubes, and volumes, where positions cannot be represented as linear combinations of the point position, so a special case is needed for interpolating positions if those types of primitives may be present. GEO_Primitive::evaluateInteriorPoint(UT_Vector4 &pos, fpreal u, fpreal v, fpreal w = 0)
can be used for this.
Here's a pair of examples demonstrating this.
There are several sub-classes of GEO_Primitive.
Face primitives store their vertex offsets in a 1D list that is compressed if "trivial", namely each being one more than the last, e.g. 3,4,5,6. If the face is closed, the last vertex in the list will connect to the first vertex.
There are three main sub-classes of the GEO_Face base class. GEO_PrimPoly is a sub-class which uses an implicit linear basis to represent polygons. GEO_Curve gets further refined to GEO_PrimNURBCurve (NURBS curve) and GEO_PrimRBezCurve (rational Bezier curve).
Each GEO_PrimPoly usually adds an extra 48 bytes of memory to its GA_Detail: 8 bytes for the pointer to it in the GA_Detail's GA_PrimitiveList, 8 bytes for the virtual function table pointer, 8 bytes for the reference to the primitive GA_IndexMap of its detail, 8 bytes for its GA_Offset, and 16 bytes for a GA_OffsetList if the vertex offsets are trivial. The flag indicating whether the polygon is closed is one of the bits in the GA_OffsetList. Because of the reference to the GA_IndexMap, in GA_Primitive, primitives themselves cannot be shared between details, so the memory and time overhead from copying a large number of GEO_PrimPoly primitives can be large. This problem is mitigated with Polygon Soup Primitives .
To avoid the overhead of copying a large number of GEO_PrimPoly primitives, GEO_PrimPolySoup is a primitive that represents vertex offset lists for multiple polygons in a way that the list data can be shared between details. If a GEO_PrimPolySoup represents only one polygon, it's quite inefficient, but it can be more efficient than GEO_PrimPoly for even 8 polygons or possibly less, with the savings being the greatest for thousands or millions of polygons.
Being a single primitive for multiple polygons, there is the disadvantage that a whole GEO_PrimPolySoup will have only a single set of primitive attribute values, so they won't vary per polygon, but this can be an advantage if there were only a few unique sets of primitive attribute values as polygons.
GEO_PrimPolySoup also has the advantage that if vertex attribute modification isn't needed after creation, what would be multiple polygon vertices can optionally be represented as a single vertex in the detail, so long as the vertices have the same attribute values for all attributes. This can save memory when vertex attributes all match for most vertices referencing the same point.
Polygon soups can be easily created in parallel using GU_PrimPolySoup::build()
, which has an option to either have shared vertices or have unique vertices for each polygon.
The polygons in a polygon soup can be accessed similar to GEO_PrimPoly primitives using GEO_PrimPolySoup::PolygonIterator, with functions like getPos3()
, getPointOffset()
, getVertexOffset()
, and computeNormal()
. It is often possible to template a function to support both GEO_PrimPoly and PolygonIterator, avoiding the need to fully duplicate a function to support polygon soups. To a lesser extent, this is sometimes possible with GEO_Hull::Poly and GEO_PrimTetrahedron::Face.
Patch primitives store their vertex offsets in a 2D matrix. There are two closed flags indicating whether the primitive is closed in the u
or v
directions. For example, a grid would be open in both u
and v
while a tube would be closed in one of the directions and a torus would be closed in both parametric directions.
There are three main sub-classes of the GEO_Hull base class (mirroring Face Primitives). GEO_PrimMesh represents a linear mesh, while GEO_PrimNURBSurf (NURBS surface) and GEO_PrimRBezSurf (rational Bezier surface) can be used to represent higher order surfaces.
Parametric coordinates are well defined for these primitives.
Quadric primitives have a single vertex representing the center of the primitive. They also store a 3x3 transformation matrix (the center of the primitive is determined by the position of the vertex' point).
The sub-classes are
The tube primitive has two additional intrinsic parameters:
taper
The taper is used to change the radius over the height of the tube. The radius is determined by:
Where v is the parametric v
coordinate. So, with a taper of 1, the tube is a cylinder. With a taper of 1, the tube becomes a cone (with the apex at v == 1
).
Of course, this is the radius prior to transformation by the rotation/scale matrix.
v
. The first third for the bottom end-cap, the last third for the top end cap.
The volume primitive is like a quadric primitive in that it has a single vertex and a rotation/scale matrix. The volume primitive also has a scalar voxel field which stores values over the volume.
The volume before the rotation/scale matrix occupies the bounding box (-1, -1, -1) to (1, 1, 1).
More detail on volumes can be found in Volumes in Houdini.
Particle primitives are usually no longer needed, and using "disconnected" points, points that are not referenced by any primitives, is frequently much faster and less memory intensive. Mantra supports rendering points as spheres or circles, optionally scaled by a pscale point attribute, and most particle simulations support disconnected points.
Each GU_Detail may have multiple particle primitives and Each particle primitive also contains a GEO_PartRender object that specifies how the particles from that primitive should be rendered.
For example, to build a particle system with 4 particles rendered as motion blurred rounded tubes:
The Houdini geometry format supports quadric (GEO_PrimMetaBall) and superquadric (GEO_PrimMetaSQuad) metaballs. Meta-primitives are subclassed from GEO_PrimMeta (as well as GEO_Primitive). Like Quadric Primitives, metaballs have a single vertex and a transform matrix. However, metaballs also have two intrinsic parameters
kernel
The kernel is a function of the metaball radius. The kernel is one at the center and zero at the outside radius of the metaball. The shape of the interpolation curve is different for each kernel type. The factory metaball kernel functions are: In all metaball functions, r
is the radius squared:
Superquadrics (http://en.wikipedia.org/wiki/Superquadrics) have two additional intrinsic parameters to control the shape of the superquadric. The XY and Z exponents are used to change the shape of the superquadric.
By default, all metaballs in the GU_Detail will blend together by adding their density fields. However, it's possible to make an expression which defines how the metaprimitives are supposed to blend.
The expression uses a simply grammar which is defined http://www.sidefx.com/docs/current/nodes/sop/metaball, but in brief, has 2 primitive types and 3 functions defined:
min()
function returns the minimum density of the argumentsmax()
function returns the maximum density of the argumentssum()
function returns the sum of the argumentsFor example: sum(max("group1", "group2"), max("group3", "group4"), 0, 1)
Internally, the metaball expression is represented using the TS_MetaExpression class. This is an example of how to evaluate the expression:
The expression for a GU_Detail is stored on a detail attribute string metaExpression
. You can access this directly:
Alternatively, you can use the GEO_MetaExpression class. With a GEO_MetaExpression, you can easily evaluate attributes for the blended surface:
For simple primitives such as polygons, adding a primitive to the detail is usually accomplished by calling a GEO_Detail::appendPrimitive() function. Note that it can either copy a primitive from another primitive, or add an empty primitive of the specified type, which then has to be filled in with data. An example below illustrates an addition of a simple polygon to an existing detail:
For more complex primitive types, it is often more convenient to use a number of helper builder functions provided in the same GU class which corresponds to the primitive type you would like to build, such as GEO_PrimPoly::build(), GU_PrimMesh::build() or GU_PrimSphere::build().
For example, to append a NURBS primitive you can use the GU_PrimNURBSurf::build() function, which builds the primitive for you, adhering to the specified parameters, and returns a pointer to the new NURB surface. You still need to fill in the points and the knot vector, however:
Houdini's geometry also supports groups - a named subset of items within the gdp, each marked as belonging to a group. There are several group types, including point, primitive, vertex and edge groups, all of which derive from the base group type GA_Group.
Most of the geometry manipulation functions in GU_Detail and GEO_Detail have an optional group parameter, which, if not null, will force the function to perform its operations only on items belonging to a specific group instead of working with all the items in a detail. Most of the group operations, such as creating, deleting, and modifying them, can be found in GA/GA_Detail.h.
You can also remove any groups that have become unused by calling GA_Detail::destroyAllEmptyGroups().
An example which shows how to manipulate groups can be found below. Although it uses point groups, operating on primitive groups follows the same principles:
Edges are not explicitly represented as elements in GU_Detail and thus do not have persistent attribute data associated with them. The GA_Edge class is used by many functions to describe an undirected edge. An instance of this class can be used to represent an undirected segment between two points. When an edge is added to an edge group (GA_EdgeGroup), a primitive pointer can be specified to constrain that entry to only that primitive. For example:
Note when using edge groups, instances of the GA_EdgeGroup class describe a group of undirected edges.
GU_Detail's base class provides several methods for copying points and primitives between details:
method
. Multiple details can be merged by specifying GEO_COPY_ADD for method
except for the first and last detail which should use GEO_COPY_START and GEO_COPY_END respectively. See the SOP_HDKObject example.These methods will retain all attribute values for the merged elements. Attributes from the source detail will be created in the destination for the classes of copied elements.
In a detail, each attribute, each edge group, and the primitive list is assigned a monotone-increasing globally unique ID, (until int64 overflows). This allows any code examining the detail to see if data may have changed since last examining, to avoid redoing work, if possible. When data IDs are properly managed, it can result in significant performance gains, especially when playing back in the viewport if the topology isn't changing.
When calling SOP_Node::duplicateSource(), by default, all data IDs are copied, "cloned", when the attributes are copied, indicating that they have the same data. However, because SOPs need to be changed to explicitly bump the data IDs of any data that has changed, by default, all data IDs are bumped at the end of each SOP cook, unless the SOP has done: mySopFlags.setManagesDataIDs(true);
. Effectively, when that flag hasn't been set on a single node that's cooked on every frame, it may negate all possible benefit for all downstream nodes. When that flag is set, however, the SOP becomes responsible for managing its data IDs, and incorrectly management of the data IDs could make code make incorrect decisions based on the suggestion that data hasn't changed, e.g. the viewport may not update an attribute when playing back or data may or may not be copied when cooking other SOPs. If that is not a risk you would like to debug, please do not use data IDs, and leave the flag off.
If you accept the risk involved with data IDs, the criteria for when two attributes, edge groups, or primitive lists can have the same data ID are:
If data IDs are enabled for the SOP, it is responsible for ensuring that any functions it calls that may modify attributes, primitives, or edge groups are accounted-for. Either those functions must bump data IDs as needed internally, or the SOP must know what attributes or edge groups may have been modified and whether any primitives were modified, to bump the data IDs itself. This can be quite difficult at the moment, because there is very little documentation on which functions bump data IDs internally, so caution is advised. Most HDK SOP examples have been updated to manage their data IDs, though that code isn't guaranteed to be perfect.
To bump all point attribute data IDs, upon adding or removing points, (assuming no edge groups are affected), one can call:
To bump all vertex and primitive data IDs, upon adding or removing primitives with vertices, (assuming no edge groups are affected), one can do:
If edge groups might be affected, one can call:
Higher-level functions also exist. See GA_Detail::bumpDataIdsForRewire() and GA_Detail::bumpDataIdsForAddOrRemove().
To bump a single attribute's data ID, one can call:
This also works from attribute handles like GA_RWHandleV3 and GA_RWHandleS.
If carefully done, it is possible to do SOP cooking optimizations based on whether particular input geometry data IDs have changed since a previous cook. To read the data ID of an attribute, the primitive list, or an edge group, call getDataId()
.
For GA_Detail modification's outside of cooking code paths, they always require explicit bumping of the appropriate data id's as described above. This is especially important if this GA_Detail is passed to something else that uses data id's for optimization. A simply way to fix such situations then is to call both GA_Detail::bumpAllDataIds() and GA_Detail::incrementMetaCacheCount() whenever modifying geometry outside of cooking code paths (ie. mimicking what is normally done for you after cookMySop() finishes). If you want better performance though, you should do fine grained bumping of the data ids depending on what is modified. Be careful to still always call incrementMetaCacheCount() however.