HDK
|
One of the design constraints for the design of the GA library was to minimize effort required to port code from the previous GB incarnation. While this older interface can be used, it does not always provide the most efficient means of working with the geometry data.
This document gives guidance on the best practices when re-writing code to use the new architecture.
The GA library doesn't store point (GEO_Point) or vertex (GEO_Vertex) objects. However, for easy porting, GA_Detail in older versions of Houdini (prior to 14.0) could create persistent GEO_Point objects on the fly.
This was done any time you asked for a GEO_Point.
However, all operations can be done without creating GEO_Point objects (since GEO_Point objects really just store a back pointer to the point GA_IndexMap and a GA_Offset).
Rather than using GEO_Point objects, try to use the GA_Offset of the point.
This will:
The following table lists some of the common mappings from the old to new methods which work with GA_Offsets.
Old method | New methods | Remarks |
GEO_Detail::appendPoint() | GEO_Detail::appendPointOffset() or GEO_Detail::appendPointCopy() | |
GEO_Detail::deletePoint() | GA_Detail::destroyPointOffset() | Methods in GA_Detail consistently use the term "destroy" instead of "delete" |
GEO_Vertex::getPt() | GA_GBVertex::getPointOffset() or GA_GBVertex::getPointIndex() | |
UT_Vector4 pos4 = ppt->getPos() | UT_Vector4 pos4 = gdp->getPos4(point_offset) | For efficiency, use getPos3() where the w component should not be affected. |
ppt->setPos(pos4) | gdp->setPos4(pos4) | For efficiency, use setPos3() where the w component should not be affected. |
One common place that GEO_Point objects are created is in the GA_FOR_ALL_GPOINTS() macro. There is a special version of the macro which doesn't construct the GEO_Point objects called GA_FOR_ALL_GPOINTS_NC(). So, it should be relatively easy to sweep code to change macros.
However, there is a caveat. The NC macro creates a temporary object which gets re-used, so the underlying code should NOT hold onto references to the GEO_Point pointer. For example, if you are building an array of GEO_Point pointers and plan to sort them, you cannot use the NC macro.
For improved safety, the NC versions of the macros limit the scope of the loop variable to within the loop itself. It is also an "iterator" like object which provides an operator->() and get() to retrieve the underlying element pointer.
Attributes in the GA library can provide their own AIF interfaces. For example, the numeric type provides an AIFTuple interface, as does the index pair attribute. To a user, both of these attributes can appear as a tuple of numeric data.
However, processing data using the AIF interface invokes at least one virtual thunk for each call (since the AIF classes are all virtual), and in practice often two virtual calls are required. Thus, GA provides an "efficient" attribute accessor for numeric (and string) data which is tightly coupled to the numeric interface.
In fact, there are multiple classes to access attribute data. Each has their advantages and disadvantages. It's important to try to choose the appropriate interface.
The GA_AttributeRefMap class allows you to process all attributes on vertices, points, primitives (and detail) objects within one detail or even across different details.
The GA_AttributeRefMap will create a mapping between destination and source attributes (which may exist on different details).
When operating in an "object-centric" mode (i.e. blending two points as objects), you often want to process all the attributes. The GA_AttributeRefMap will use the AIFs provided by the attributes to perform operations between all attributes in the map.
When constructing the GA_AttributeRefMap, you must first bind the details, then add attributes, and then perform operations on elements.
There are several ways of adding attributes to a bound GA_AttributeRefMap:
For example, let's say you want to split an edge, creating a new vertex, but you want to interpolate the two vertices on either side of the edge:
You can also avoid the necessity of specifying the destination attribute explicitly for every operation by using a GA_AttributeRefMapDestHandle:
The GA_Handle template classes provide an efficient way to get/set numeric or string data.
You can create handles on the stack by binding them to an attribute. The type of access desired is part of the handle type: HandleI for integer, HandleF for float, and HandleV3 for the very common vector triple, and HandleS for strings.
If the handle cannot be bound it won't be valid and attempts to access it will crash. You can use isValid() to determine if it bound successfully. To bind, the attribute must exist, it must be in a type convertible to the desired handle, and the attribute must be large enough. For example, a HandleV3 will bind to a 3-float attribute, but not to a 2-float attribute. Note HandleV3 will also bind to a 4-float attribute, ignoring the fourth float.
Because handles are often tied to a specific named attribute, they often are named attribname_h.
These classes are tightly coupled to the numeric and string attribute types. They will not work with index pair or other attribute types. However, they minimize virtual calls and can access data very efficiently. When you are certain you will be working with the standard Houdini numeric/string attributes, you can use GA_Handle for the most efficient code.
For example, to add a timestep of the "v" attribute to the "P" attribute, the code might look like:
GA_Attribute data is constructed using "paged" arrays. That is, each array is typically an array of pages of data. By working on pages of data, you can take advantage of data locality even more than GA_Handle. Instead of working on individual elements, you can work on blocks of the array. You can also avoid the virtual function call for each access as it will be amortized to once per new page.
This will be most efficient if your iterator proceeds in sequential order over the offsets. GA_Iterator::blockAdvance(), intended for this purpose, provides contiguous blocks of offsets constrained to occupy the same page. If you need random access, GA_Handle is usually a superior interface.
Wherever possible, the page handle acquires a direct pointer to the underlying data. However, this should not be relied on - if the types differ it will be marshalled into a temporary buffer and flushed out whenever the handle is destroyed or changes pages. Extreme care should be taken if you have more than one page handle pointing to the same attribute - things may work with native types that don't if the attribute is fpreal16!
Because handles are often tied to a specific named attribute, they often are named attribname_ph.
For example, to add a timestep of the "v" attribute to the "P" attribute, the code might look like:
This section discusses best practices when writing parallel algorithms for GA.
The GA library works on pages of data rather than large contiguous arrays. When data is written to the array, pages may be allocated or cleared. For thread safety, the GA library only allows a single thread to write to a single page. When writing threaded algorithms, each thread must iterate over pages using a GA_PageIterator. The page iterator then can provide a GA_Iterator which will iterate over the elements in the page (either blocked or individual).
There are typically two methods to write threaded code for GA
When writing threaded code, both of these methods require having a GA_SplittableRange object. The GA_SplittableRange should be constructed before splitting into threads. There is some cost (both memory and performance) in constructing a splittable range.
Typical single threaded code using the TBB paradigm would look something like:
However, not all GA_Range objects are splittable, so this code may end up single threaded, even though the intent was to have threaded code. Also, the above code is not thread safe since a GA_Range iterator is not guaranteed to work on single pages.
The correct way to ensure threading is to use GA_SplittableRange:
For foreach-style algorithms, the simplest approach is to use GAparallelForEachPage() which provides a higher-level API that is convenient to use with lambdas. It invokes the body approximately once per worker thread, and provides a GA_PageIterator with load-balanced iteration over the pages in the range (which is advantageous if some pages are more expensive to process than others).
Similar to the TBB paradigm, UT_ThreadedAlgorithm relies on having a GA_SplittableRange to operate correctly. The easiest way to ensure proper splittable ranges is to make the "partial" method take a GA_SplittableRange const reference (instead of a GA_Range). Then, in the partial method, use the page iterator to iterate over pages using the UT_JobInfo for load balancing. For example:
Some algorithms are not amenable to working within page boundaries. If your algorithm can guarantee that threads will write to unique offsets you can now use the GA_AutoHardenForThreading class to prepare the attribute for generic threaded write access. For paged attributes, instantiating this class will ensure all pages are "hardened" and can be written by multiple threads. Note that groups are written to with bit operations, so writing to group offsets is not threadsafe as bitwise updates are not threadsafe.
Using ranges may be problematic with this approach though since ranges will still adhere to division along pages. You will likely have to roll your own iteration methods. For example:
Note that there is a cost associated to hardening attributes for threading. There may be significant work required to prepare an attribute for this kind of threaded access, and further work on the destructor to optimize the attribute for paged access. Where possible, paged threading should likely be used.
Until Houdini 14, it was necessary for a custom primitive class to derive from both GEO_Primitive and GU_Primitive. Standard practice was to have a GEO_CustomPrim class derive from GEO_Primitive, and then a GU_CustomPrim class deriving from both GEO_CustomPrim and GU_Primitive, with only this GU_CustomPrim class ever being instantiated. This is no longer the case, and now you just need one class deriving from GEO_Primitive.
There are several methods to be implemented for the GEO interface:
bool GA_Primitive::isDegenerate() const
void GA_Primitive::copySubclassData(const GA_Primitive *src)
GA_DereferenceStatus GA_Primitive::dereferencePoint(GA_Offset point, bool dry_run)
GA_DereferenceStatus GA_Primitive::dereferencePoints(const GA_RangeMemberQuery &pt_q, bool dry_run)
dry_run
, the primitive should not perform any changes, but just return the status.GA_Size GA_Primitive::getVertexCount() const
GA_Offset GA_Primitive::getVertexOffset(GA_Size index) const
index
is in the range of 0 to getVertexCount()
.const GA_PrimitiveJSON *GA_Primitive::getJSON() const
void GA_Primitive::swapVertexOffsets(const GA_Defragment &defrag)
int GEO_Primitive::getBBox(UT_BoundingBox *box) const
void GEO_Primitive::reverse()
UT_Vector3 GEO_Primitive::computeNormal() const
void GEO_Primitive::copyPrimitive(const GEO_Primitive *src, GEO_Point **ptredirect)
int GEO_Primitive::detachPoints(GA_PointGroup &grp)
void GEO_Primitive::copyOffsetPrimitive(const GEO_Primitive *src, int base)
bool GEO_Primitive::evaluatePointRefMap(GA_Offset result_vtx, GA_AttributeRefMap &map, fpreal u, fpreal v=0, uint du=0, uint dv=0) const = 0
int GEO_Primitive::evaluatePointV4( UT_Vector4 &pos, float u, float v = 0, unsigned du=0, unsigned dv=0) const
Other virtuals that need to be implemented, formally inherited fromxi GU_Primitive, but now inherited from GEO_Primitive, are:
int64 getMemoryUsage() const
const GA_PrimitiveDefinition &getTypeDef() const
Return the definitition of the primitive.
GEO_Primitive *convert(GU_ConvertParms &parms, GA_PointGroup *usedpts)
Convert this primitive into one or more other primitives. If the conversion parameters has a deletion group, the primitive should add itself to that group, otherwise, it should delete itself from the detail. For example:if (GA_PrimitiveGroup *group = parms.getDeletePrimitives())
group->add(this);
else
getParent()->deletePrimitive(*this, usedpts != NULL);
GEO_Primitive *convertNew(GU_ConvertParms &parms)
Convert to a new primitive (or multiple primitives), keeping this primitive intact.
void *castTo (void) const
Return the GU_Primitive pointer
const GEO_Primitive *castToGeo(void) const
Return the GEO_Primitive pointer
void normal(NormalComp &output) const
Add the primitive's normal to all points referenced by the primitive. For example, for a polygon, you might have code like:UT_Vector3 nml = computeNormal();
for (i = 0; i < getVertexCount(); ++i)
output.add(getPointOffset(i), nml);
int intersectRay(const UT_Vector3 &o, const UT_Vector3 &d, float tmax, float tol, float *distance, UT_Vector3 *pos, UT_Vector3 *nml, int accurate, float *u, float *v, int ignoretrim) const
Intersect a ray with the primitive.
GU_RayIntersect *createRayCache(int &persistent)
Create a cache structure which can be used to accelerate ray-tracing.
Primitives are registered using the DSO/DLL hook
newGeometryPrim(GA_PrimitiveFactory*)
.
This method should add the definition to the given GA_PrimitiveFactory.
Please see the HDK_Sample::GU_PrimTetra sample for an example of this code.
Once you have a custom primitive defined, you will likely want to add a mechanism for the user to create and use these primitives. Most likely you will need to create a SOP to create and manipulate your new primitive.
When tesselating a GU_Detail using the GT library (as is done for the GL viewport rendering), it's possible to provide Houdini with a tesselation of your primitive. This doesn't rely on the convert mechanism and can be much more light-weight than creating Houdini geometry.
The GT_GEOPrimCollector class lets you either generate a single GT primitive for each GEO primitive, or to collect multiple GEO primitives to generate a single GT primitive. For example, a GEO_PrimNURBSurf might generate a single GT_PrimPatch, while GEO_PrimPoly's might be collected to generate a single GT_PrimPolygonMesh.
When your subclass constructs, it must pass a GA_PrimitiveTypeId. This tells the GT tesselator to pass any primitives of that type-id to your collector.
There are three methods used in collecting:
GT_GEOPrimCollectData *beginCollecting(const GT_GEODetailListHandle &geometry, const GT_RefineParms *parms) const
collect
and endCollect
methods.GT_PrimitiveHandle collect(const GT_GEODetailListHandle &geometry, const GEO_Primitive const prim_list, int nsegments, GT_GEOPrimCollectData *data) const
endCollecting
method.data
passed in is the data returned by beginCollecting
.GT_PrimitiveHandle endCollecting(const GT_GEODetailListHandle &geometry, GT_GEOPrimCollectData *data) const
This method is called after the last primitive in the detail has been passed to the collect
method. The method can either return a primitive or an empty GT_PrimitiveHandle.
Note: The endCollecting()
method should not delete the collecting data.
Houdini 12 introduced a new .geo/.bgeo file format. The ASCII format is stored in vanilla JSON, so reading a .geo file in Python should be as simple as:
However, for efficiency, Houdini also comes with a "binary" JSON implementation. There are several ways to use/understand the binary JSON implementation
Loading/parsing a geo/bgeo file is relatively easy. However, interpreting the schema may be a little more difficult. There is a Python implementation that interprets the schema provided in $HH/public/hgeo. This is intended as a reference implementation and may not be efficient enough to be used in production.
If you run the module as a program, the application will load any .geo/.bgeo files specified on the command line (or $HH/geo/defbgeo.bgeo if non are specified), load the geometry and print out information about the file. For example:
Note that by running
hython
instead of python
, the hjson
module becomes available.
The schema supports various fields for storing numeric tuple data values, regardless of whether dealing with an ASCII or binary file.
For the purposes of this description, we'll focus on a simple example with an attribute tuple of size 4 and 5 elements with values [X0,Y0,Z0,W0]..[X4,Y4,Z4,W4], respectively, and a tiny page size of 2.
The simplest representation, and that used by default for ASCII files, is as an array of structs.
This representation is both highly readable and easy to parse, but not particularly efficient. An alternative, packed page, representation is preferred for binary files.
The pages are sequential in the flat "rawpagedata" array, with each page packed as per the "packing" field.
For example, a packing of [4] yields
while a packing of [1,1,1,1] yields
The "packing" field may be omitted, and, if missing, is equivalent to [<size>].
An optional "constantpageflags" field may also be used to mark constant pages for each subvector.
Suppose, in our simple example, we have X0 == X1, Y0 == Y1 and Z0 == Z1. Then we could have
A subvector entry in the "constantpageflags" array with no constant pages may also be represented as an empty array. The following is equivalent to the previous example