HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
APEX/apex_pointtransforms.h
/*
* Copyright (c) 2026
* Side Effects Software Inc. All rights reserved.
*
* Redistribution and use of Houdini Development Kit samples in source and
* binary forms, with or without modification, are permitted provided that the
* following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. The name of Side Effects Software may not be used to endorse or
* promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE `AS IS' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#include <GU/GU_Detail.h>
#include <GA/GA_Names.h>
namespace apex_hdk
{
using namespace apex;
// APEX_GenericFunctionRunData supplies a RunData object as the first
// operator() argument, where a callback can keep derived state such as a cache.
// These examples also use named variadic Matrix4 ports: the point `name`
// attribute connects each graph port to the matching point in the geometry.
// SetPointTransforms writes those matrices to geometry; GetPointTransforms
// reads the point data back into the named ports.
class SetPointTransforms : public APEX_GenericFunctionRunData<
SetPointTransforms,
Geometry,
const VariadicArg<Matrix4>>
{
public:
// Geometry is modified in place; transforms is a read-only set of named
// Matrix4 inputs. The `*` on geo marks its graph port as in-place.
static constexpr UT_StringLit funcname = "hdk::SetPointTransformsHDK";
static constexpr const char *argnames[] = {"*geo", "transforms"};
struct RunData
{
// APEX keeps this state for the callback so the point-name lookup can
// be reused. The table is indexed by point offset and stores the
// matching input transform index, or -1 when no input matches.
RunData() = default;
~RunData() = default;
RunData(const RunData &other) = delete;
const RunData &operator=(const RunData &other) = delete;
GA_DataId myNameDataId = GA_INVALID_DATAID;
UT_Array<exint> myLookupHandle;
// Rebuild the lookup when the point name attribute changes or is
// added or removed. An unchanged missing attribute needs no recache.
bool needsRecache(GU_Detail *gdp)
{
if (name_h.isInvalid())
{
if (myNameDataId == GA_INVALID_DATAID)
return false;
myNameDataId = GA_INVALID_DATAID;
return true;
}
return name_h.getDataId() != myNameDataId;
}
// Map each named transform input to its point offset. The table stays
// indexed by offset so the geometry loop can do a direct lookup.
void cacheLookup(GU_Detail *gdp, const UT_StringArray &names)
{
myLookupHandle.clear();
myLookupHandle.setSize(exint(gdp->getNumPointOffsets()));
myLookupHandle.constant(-1);
if (name_h.isValid())
{
exint idx = 0;
for (auto &pt_name : names)
{
auto ptoff = lut->getStringOffset(pt_name);
if (ptoff != GA_INVALID_OFFSET)
myLookupHandle[ptoff] = idx;
idx++;
}
myNameDataId = name_h->getDataId();
}
else
myNameDataId = GA_INVALID_DATAID;
}
};
// Register the per-callback RunData type with APEX.
APEX_DEF_TYPE_RUNDATA(SetPointTransforms)
void operator()(RunData &rundata, Geometry &geo, const VariadicArg<Matrix4> &transforms) const
{
GU_Detail *gdp = geo.gdp();
// This example writes the 3x3 matrix portion to `transform` and the
// translation to the built-in point position attribute `P`.
GA_RWHandleM3D transform_h(gdp, GA_ATTRIB_POINT, "transform");
if (transform_h.isInvalid())
return;
if (rundata.needsRecache(gdp))
rundata.cacheLookup(gdp, transforms.names());
if (rundata.myNameDataId == GA_INVALID_DATAID
|| rundata.myLookupHandle.size() < exint(gdp->getNumPointOffsets()))
return;
auto body = [&](const GA_Range &r)
{
for (auto ri = r.begin(); ri != r.end(); ++ri)
{
GA_Offset ptoff = *ri;
exint idx = rundata.myLookupHandle[ptoff];
if (idx < 0)
continue;
const Matrix4 &xform = *transforms[idx];
Vector3 pos;
xform.getTranslates(pos);
gdp->setPos3(ptoff, pos);
Matrix3 m3 = Matrix3(xform);
transform_h.set(ptoff, m3);
}
};
// Bump data IDs and the detail metadata cache count after the direct
// geometry edits so Houdini and downstream nodes see the new values.
transform_h->bumpDataId();
gdp->getP()->bumpDataId();
}
};
// This read-side callback reconstructs one Matrix4 for each named output from
// the point position and the point's 3x3 `transform` value.
class GetPointTransforms : public APEX_GenericFunctionRunData<
GetPointTransforms,
VariadicArg<Matrix4>,
const Geometry>
{
public:
// The named Matrix4 ports are outputs and geo is read-only input geometry.
static constexpr UT_StringLit funcname = "hdk::GetPointTransformsHDK";
static constexpr const char *argnames[] = {"transforms", "geo"};
struct RunData
{
// Keep one offset per requested name, in port order. Invalid offsets
// preserve slots for names that have no matching point.
GA_OffsetArray myPointOffsets;
GA_DataId myNameDataId = GA_INVALID_DATAID;
// Rebuild the name-to-offset list when the point name attribute
// changes or is added or removed.
bool needsRecache(const GU_Detail *gdp)
{
if (name_h.isInvalid())
{
if (myNameDataId == GA_INVALID_DATAID)
return false;
myNameDataId = GA_INVALID_DATAID;
return true;
}
return name_h.getDataId() != myNameDataId;
}
// Store offsets in the same order as the named output ports.
void cacheLookup(const GU_Detail *gdp, const UT_StringArray &names)
{
myPointOffsets.clear();
myPointOffsets.setCapacity(names.size());
if (name_h.isValid())
{
for (auto &pt_name : names)
{
auto ptoff = lut->getStringOffset(pt_name);
myPointOffsets.append(ptoff);
}
myNameDataId = name_h->getDataId();
}
else
myNameDataId = GA_INVALID_DATAID;
}
};
// Register this callback's per-instance lookup state with APEX.
APEX_DEF_TYPE_RUNDATA(GetPointTransforms)
void operator()(
RunData &rundata,
VariadicArg<Matrix4> &transforms,
const Geometry &geo) const
{
const GU_Detail *gdp = &*geo;
if (rundata.needsRecache(gdp))
rundata.cacheLookup(gdp, transforms.names());
int idx = 0;
GA_ROHandleM3D h_transform(gdp, GA_ATTRIB_POINT, "transform");
if (h_transform.isInvalid())
return;
// Keep the output index aligned when a requested point is missing.
for (auto &ptoff : rundata.myPointOffsets)
{
if (ptoff == GA_INVALID_OFFSET)
{
++idx;
continue;
}
Vector3 P = gdp->getPos3(ptoff);
Matrix3 m3 = h_transform.get(ptoff);
Matrix4 m = Matrix4(m3);
UT_ASSERT(idx < transforms.size());
*transforms[idx++] = m;
}
}
};
} // namespace apex_hdk