All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Geometry Introduction

Table Of Contents

Geometry Class Libraries

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.

Geometry Class Hierarchy

There are additional geometry libraries which may be of use for more specialized uses:

Geometry Structures

The main types/classes for Houdini geometry are:

  • GU_Detail
    The container class.
  • GA_Attribute
    The base class for all attribute type implementations (ATIs).
  • GA_Offset
    The internal offset for an element's data in the attribute data pages.
  • GA_Index
    The allocation order for an element.
  • GA_IndexMap
    A class managing the GA_Index <-> GA_Offset association for a particular element type: point, vertex, primitive or detail.
  • GEO_Primitive
    A primitive object. Examples of these are GEO_PrimPoly, GEO_PrimSphere. Each primitive manages a list of zero or more vertices (GA_Offsets in the vertex GA_IndexMap). Each vertex has an associated reference to a point (GA_Offset in the point GA_IndexMap) which may be shared across multiple vertices.
Geometry Structure

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).

Introduction To Geometry 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:

  • name
    The name of the attribute (i.e. P or N or temperature)
  • scope
    A scope (GA_SCOPE_PUBLIC, GA_SCOPE_PRIVATE, GA_SCOPE_GROUP) identifies a name space for the attribute as well as some behaviors. See GA_AttributeScope.
  • type
    A reference to the factory object which allocated it.
  • type info
    A hint on how to interpret the stored data. See GA_TypeInfo.
  • options
    A UT_Options object that can store various name, value pairs.
  • AIFs accessors
    Virtual methods to access Attribute Interfaces (AIFs) for manipulating the attribute data. Examples include: GA_AIFCopyData, GA_AIFTuple, GA_AIFMath.

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.

Example Code

Attribute Types

New 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:

  • "numeric"
    Numeric tuple data. Supported storage types are: GA_STORE_UINT8, GA_STORE_INT8, GA_STORE_INT16, GA_STORE_INT32, GA_STORE_REAL16, GA_STORE_REAL32 and GA_STORE_REAL64. Attributes of this type support the GA_AIFTuple interface. However, for most uses, handles like GA_ROHandleV3, GA_RWHandleV3, GA_ROHandleF, and GA_RWHandleF are easier and faster to use.
    Integer storage types, by default, are set as GA_TYPE_NONARITHMETIC_INTEGER.
  • "arraydata"
    Variable-length arrays of numeric tuples. This implements GA_AIFNumericArray, but is easier to access with handles like GA_ROHandleFA and GA_RWHandleFA, for simple operations.
  • "string"
    An integer handle to a shared table of strings is stored on each geometric element. Attributes of this type support both the GA_AIFStringTuple and GA_AIFSharedStringTuple interfaces. However, for most uses, GA_ROHandleS, GA_RWHandleS, or GA_RWBatchHandleS are easier and faster to use.
  • "stringarray"
    Variable-length arrays of string tuples. This implements GA_AIFStringArray and GA_AIFSharedStringArray, but is easier to access with GA_ROHandleSA and GA_RWHandleSA, for simple operations.
  • "indexpair"
    Associated with each element a series of <index, value> pairs. The index refers to an index into a shared table of "objects" and the values can be numeric tuples. For example, in the "boneCapture" attribute, the objects are capture region paths, and the values are weights. Attributes of this type support the GA_AIFIndexPair interface, but can also be manipulated using the GA_AIFTuple interface.
  • "blob"
    A generalization of the "string" attribute where the shared table consists of arbitrary objects instead of strings. In fact, "string" attributes are just a specialization of "blob" attributes. Attributes of this type support the GA_AIFBlob interface.
  • "blobarray"
    Variable-length arrays of blob tuples. This implements GA_AIFBlobArray.
  • "blinddata"
    Each element is allocated a chunk of typeless data which can be used to store arbitrary data. Attributes of this type support the GA_AIFBlindData interface.

String Attributes

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:

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.

SOP Local Variables

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.

Attribute Interpolation

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.

sampleNormal(const GEO_Detail &gdp, GA_Offset primoff, UT_Vector3 &primuvw, const GA_ROHandleV3 &normalattrib)
// Get the primitive, and the interpolation vertex offsets and weights.
const GEO_Primitive *prim = gdp.getGEOPrimitive(primoff);
UT_Array<GA_Offset> offsetarray;
UT_FloatArray weightarray;
prim->computeInteriorPointWeights(offsetarray, weightarray, primuvw.x(), primuvw.y(), primuvw.z());
// Do the weighted average.
GA_AttributeOwner owner = normalattrib->getOwner();
UT_Vector3 normal(0,0,0);
for (exint i = 0; i < offsetarray.size(); ++i)
// Assuming either a point or vertex normal attribute
GA_Offset offset = offsetarray(i);
if (owner == GA_ATTRIB_POINT)
offset = gdp.vertexPoint(offset);
normal += weightarray(i) * normalattrib.get(offset);
return normal;
interpolatePoint(const GEO_Detail &source, const GEO_Primitive *srcprim, UT_Vector3 &primuvw,
GEO_Detail &dest, GA_Offset destptoff, const GA_AttributeRefMap &refmap)
// Get the interpolation vertex offsets and weights.
UT_Array<GA_Offset> offsetarray;
UT_FloatArray weightarray;
srcprim->computeInteriorPointWeights(offsetarray, weightarray, primuvw.x(), primuvw.y(), primuvw.z());
refmap.startSum(sumpt, GA_ATTRIB_POINT, destptoff);
for (exint i = 0; i < offsetarray.size(); ++i)
refmap.addSumValue(sumpt, GA_ATTRIB_POINT, destptoff, GA_ATTRIB_POINT, source.vertexPoint(offsetarray(i)), weightarray(i));
refmap.finishSum(sumpt, GA_ATTRIB_POINT, destptoff);
// Special case for point position on quadrics, volumes, etc.,
// by excluding the types we know are fine.
GA_PrimitiveTypeId type = srcprim->getTypeId();
if (!( type == GA_PRIMPOLY || type == GA_PRIMPOLYSOUP ||
type == GA_PRIMMESH || type == GA_PRIMNURBCURVE ||
type == GA_PRIMNURBSURF || type == GA_PRIMPART ||
UT_Vector4 ptpos;
srcprim->evaluateInteriorPoint(ptpos, primuvw.x(), primuvw.y(), primuvw.z());
dest.setPos4(destptoff, ptpos);

Attribute Caveats

  • The attributes of a single type of element (point, vertex, primitive, detail) in a single scope (public, private, group) must all have unique names. This means that it is not possible to have two attributes that have the same name but differ in type.
  • At the current time, attribute names are restricted to being valid C style variable names. That means that the first character must be a letter or an underscore.
  • At the current time, the same attribute cannot exist on point and and vertex elements simultaneously. For example, if there is a point attribute "Cd" and you try to add a vertex attribute "Cd", then the point attribute will be converted/promoted to a vertex attribute, regardless of the type. An exception is if a GA_ReuseStrategy is used to specifically override this behaviour.

Geometry Primitives

There are several sub-classes of GEO_Primitive.

Face Primitives

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 .

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

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

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:

    radius = 1 + (taper - 1)*v

    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.

    See Also
    GEO_PrimTube::getTaper(), GEO_PrimTube::setTaper(), Matrix Classes
  • endcaps
    This flag specifies that the tube should have implicit end-caps. With end-caps, the parametric coordinates of the tube are split into thirds in v. The first third for the bottom end-cap, the last third for the top end cap.
    See Also
    GEO_PrimTube::endcaps(), GEO_PrimTube::setEndCaps()

Volume Primitives

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

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 (See: Creating Custom POPs)

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:

GU_PrimParticle *partsys;
float one = 1;
if (partsys = GU_PrimParticle::build(&gdp, 4))
GA_Primitive::const_iterator it;
// Initially all particles spring from (3,1,1):
gdp->setPos3(it.getPointOffset(), 3, 1, 1);
while (!it.atEnd());

Metaball Primitives

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:

    fpreal wyvill(fpreal r) { return -4/9*r^3 + 17/9*r^2 - 22/9*r + 1 }
    fpreal elendt(fpreal r) { return -4/3*r^2 + 34/9*r + 1 }
    fpreal blinn(fpreal r) {
    const fpreal blinn_radius = 9;
    const fpreal blinn_min = SYSexp(-blinn_radius);
    const fpreal blinn_scale = 1/(1 - blinn_min);
    return (SYSexp(-blinn_radius*r) - blinn_min)*blinn_scale;
    fpreal links(fpreal r) {
    if (r < 1/9)
    return 1 - 3*r;
    fpreal r2 = SYSsqrt(r);
    return 3/2 * (1-r2)*(1-r2);
    fpreal hart(fpreal r) {
    // Provided by John Hart
    fpreal r2 = SYSsqrt(r);
    return -6*r^7 + 15*r^5 - 10*r^3 + 1;
    fpreal prman(fpreal r) {
    return -r^3 + 3*r^2 - 3*r + 1;
    See Also
    GEO_MetaPrim::setMetaKernel(), GEO_MetaPrim::getMetaKernel(), Custom Metaball Kernels
  • weight
    The weight of the metaball is a scale on the kernel function's density. This scale may be negative (a metaball which takes away from the surface).
    See Also
    GEO_MetaPrim::setWeight(), GEO_MetaPrim::getWeight()

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.

See Also
GEO_PrimMetaSQuad::setXYexp(), GEO_PrimMetaSQuad::setZexp()

Metaball Expressions

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:

  • integers represent primitive numbers of metaball primitives
  • strings refer to primitive groups (GA_PrimitiveGroup)
  • The min() function returns the minimum density of the arguments
  • The max() function returns the maximum density of the arguments
  • The sum() function returns the sum of the arguments

For 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:

dumpExpression(const TS_MetaExpression *expr)
if (expr->isSum())
else if (expr->isMin())
else if (expr->isMax())
const TS_ExpressionList &kids = *expr->getAllKids();
for (exint i = 0; i < kids.size(); ++i)
if (i > 0)
// Get the Nth child of the expression
const TS_MetaPrimitive *mprim = kids(i)->isPrimitive();
if (mprim)
// Each TS_Expression has a back-pointer to the GEO_Primitive
const GEO_Primitive *gprim = mprim->getGeoPrimPtr();
// Print the number of the primitive
printf("%d", gprim->getMapIndex());
// Recurse into a new expression

The expression for a GU_Detail is stored on a detail attribute string metaExpression. You can access this directly:

expr->ref(); // Reference count the expression

Alternatively, you can use the GEO_MetaExpression class. With a GEO_MetaExpression, you can easily evaluate attributes for the blended surface:

expr.computeAttributes(P, result, handles);

Adding a Primitive

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:

void addPolygon(GU_Detail* gdp)
const exint num_points = 3;
// Set positions for the points of the polygon
UT_Vector3 positions[num_points];
positions[0].assign(0, 0, 0);
positions[1].assign(10, 0, 0);
positions[2].assign(0, 10, 0);
// Add new points to the gdp. This is optional - you can also
// reuse existing points and add them to the polygon instead.
GA_Offset startptoff = gdp->appendPointBlock(num_points);
for (exint point_idx = 0; point_idx < num_points; ++point_idx)
GA_Offset ptoff = startptoff + point_idx;
// Append a new point
// Set its coordinates with a default w component of 1.0.
// To set a non-default w component, use setPos4() here instead.
gdp->setPos3(ptoff, positions[point_idx][0], positions[point_idx][1], positions[point_idx][2]);
// Append a new polygon primitive. Primitive types are defined in GEO_PrimType.h
// Append vertices that form this primitive. We make use of the
// monotonicity guarantee of index maps to avoid keeping a separate array
// of added point offsets.
for (exint point_idx = 0; point_idx < num_points; ++point_idx)
prim_poly_ptr->appendVertex(first_ptoff + point_idx);
// Close the primitive to make it a polygon instead of a polyline.
The GA_Offset value returned by appendPrimitive() will be 1 greater than the GA_Offset of the last added primitive provided that there are no other intervening geometry operations (and similar for each index map). This "monotonicity" guarantee implies that one can make optimizations by keeping track of contiguous GA_Offset ranges when one is performing simple append operations like in the above example.

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:

u_order, v_order, do_wrap_u, do_wrap_v,
do_interp_u, do_interp_v, GEO_PATCH_QUADS, do_append_points);
// The knot vectors can be accessed as follows:
GA_Basis* curr_basis = nurb->getUBasis();
int knot_vector_len = curr_basis->getLength();
GA_KnotVector &knot_vector = curr_basis->getKnotVector();
// For the points, you can call the standard method on a detail. Please note
// that the points may not necessarily belong to the NURB you have just
// created if the gdp contains other primitives.
UT_Vector4 temp_vec;
gdp->setPos4(gdp->pointOffset(point_index), temp_vec);


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:

void groupsExample(GU_Detail* gdp)
GA_PointGroup *point_group;
GA_PointGroup *copied_group;
GA_PointGroup *found_group;
const char* const copied_group_name = "__copied_group__";
// Create a new point group.
point_group = gdp->newPointGroup("__test_group__", false);
// Get the first primitive in the detail (this assumes it exists).
GEO_Primitive* first_prim = gdp->primitives()[0];
// Add the points of the first primitive to the group.
// A more conventional way to add items to groups is by
// simply calling the GA_ElementGroup::addOffset() function:
// Make a copy of the group and give it a new name
copied_group = gdp->copyGroup(*point_group, copied_group_name);
// Try to find the group we have just copied:
found_group = gdp->findPointGroup(copied_group_name);
// You can also clear the group as following:
// Destroy the group. It can be destroyed either by passing in a pointer,
// or by passing in a group name.
// Destroy the copied group by name.


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:

GA_Offset pt0, pt1;
int i;
// ...
// an undirected edge between pt0 and pt1
GA_Edge edge1(pt0, pt1);
// a directed edge along a primitive from vertex i to vertex i+1
GA_Edge edge2(prim->getPointOffset(i), prim->getPointOffset(i + 1));
edge_group.add(edge2, prim);

Note when using edge groups, instances of the GA_EdgeGroup class describe a group of undirected edges.

Copying Elements Between Details

GU_Detail's base class provides several methods for copying points and primitives between details:

  • The GEO_Detail::copy() method copies all points and primitives into a detail. It will first remove all existing points and primitives from the destination detail before copying if GEO_COPY_ONCE or GEO_COPY_START is specified for the parameter, 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.
  • The GEO_Detail::merge() method copies a group of primitives into a detail. Only the points referenced by the primitives will be copied. All points and all primitives are copied if no group is specified.
  • The GEO_Detail::mergePoints() method copies a group of points into a detail. All points are copied if no group is specified.

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.

Data IDs

In a detail owned by a SOP, 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 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:

  • The two attributes / primitive lists must correspond with index maps that have all the same active offsets, i.e. all active offsets in one index map must be active in the other, and vice versa.
  • The relevant data regarding each active offset in one must be exactly the same as the relevant data regarding the same (active) offset in the other. For attributes, e.g., a 64-bit floating-point attribute and a 32-bit floating-point attribute are different, and any values being different makes them different. For primitive lists, the primitive data must be exactly equivalent, so adding a vertex, changing a NURBS curve's basis, changing the vertex order (e.g. reversing), or changing the transform on a quadric, volume, or packed primitive, all count as changes making the primitive lists different. For edge groups, the edges must be equivalent.
  • For two edge groups, since edge groups are ordered sets of point offset pairs with optional primitive pointers, the point offsets and primitive offsets in one edge group must correspond with the point offsets and primitive offsets in the other edge group, and all edges in one must be in the other. This means that adding points or primitives at the end of a detail won't affect an edge group, but adding or removing points or primitives in or before those referenced by an edge group could affect the edge group, so the data ID before can't be the same as after.

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:

To bump a single attribute's data ID, one can call:


This also works from attribute handles like GA_RWHandleV3 and GA_RWHandleS.

At this time, it's not recommended to try to do SOP cooking optimizations based on whether data IDs have changed since a previous cook, but in concept, this is possible, though untested. To read the data ID of an attribute, the primitive list, or an edge group, call getDataId().