HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
SOP/SOP_BouncyAgent.C
/*
* Copyright (c) 2020
* 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.
*
*----------------------------------------------------------------------------
*/
/*! @file SOP_BouncyAgent.C
@brief Demonstrates example for creating a procedural agent primitive.
The node creates an agent primitive for every point from its input geometry
that uses the same shared agent definition. The agent definition itself is a
unit polygonal sphere that has skin weights bound to two joints (a parent and
a child) with animation that bounces it in TY.
*/
#include <OP/OP_Operator.h>
#include <PRM/PRM_Type.h>
#include <CH/CH_Manager.h>
#include <GU/GU_Agent.h>
#include <GU/GU_AgentRig.h>
#include <GA/GA_Names.h>
#include <CL/CL_Clip.h>
#include <CL/CL_Track.h>
#include <UT/UT_Array.h>
#include <UT/UT_String.h>
#include <stdlib.h>
using namespace HDK_Sample;
// Provide entry point for installing this SOP.
void
{
"hdk_bouncyagent",
"BouncyAgent",
1, // min inputs
1 // max inputs
);
table->addOperator(op);
}
OP_Network *net, const char *name, OP_Operator *op)
{
return new SOP_BouncyAgent(net, name, op);
}
// SOP Parameters.
static PRM_Name sopAgentName("agentname", "Agent Name");
static PRM_Default sopAgentNameDef(0, "agent1");
static PRM_Name sopHeight("height", "Height");
static PRM_Default sopHeightDef(5.0);
static PRM_Name sopClipLength("cliplength", "Clip Length"); // seconds
static PRM_Name sopClipOffset("clipoffset", "Clip Offset"); // seconds
static PRM_Default sopClipOffsetDef(0, "$T");
static PRM_Range sopClipOffsetRange(PRM_RANGE_UI, 0, PRM_RANGE_UI, 10);
static PRM_Name sopEnableBlendshapes("enableblendshapes",
"Enable Blendshapes");
static PRM_Name sopReload("reload", "Reload");
{
PRM_Template(PRM_ALPHASTRING, 1, &sopAgentName, &sopAgentNameDef),
PRM_Template(PRM_FLT_J, 1, &sopHeight, &sopHeightDef),
PRM_Template(PRM_FLT_J, 1, &sopClipLength, PRMoneDefaults,
PRM_Template(PRM_FLT_J, 1, &sopClipOffset, &sopClipOffsetDef,
0, &sopClipOffsetRange),
PRM_Template(PRM_TOGGLE_J, 1, &sopEnableBlendshapes, PRMzeroDefaults),
PRM_Template(PRM_CALLBACK, 1, &sopReload, 0, 0, 0,
&SOP_BouncyAgent::onReload),
PRM_Template() // sentinel
};
// Constructor
OP_Network *net, const char *name, OP_Operator *op)
: SOP_Node(net, name, op)
{
// This indicates that this SOP manually manages its data IDs,
// so that Houdini can identify what attributes may have changed,
// e.g. to reduce work for the viewport, or other SOPs that
// check whether data IDs have changed.
// By default, (i.e. if this line weren't here), all data IDs
// would be bumped after the SOP cook, to indicate that
// everything might have changed.
// If some data IDs don't get bumped properly, the viewport
// may not update, or SOPs that check data IDs
// may not cook correctly, so be *very* careful!
mySopFlags.setManagesDataIDs(true);
}
// Destructor
SOP_BouncyAgent::~SOP_BouncyAgent()
{
}
enum
{
// Set up exclusive rig index range for joints
SOP_JOINT_END = SOP_CHILD_RIG_INDEX + 1
};
static constexpr UT_StringLit theChannel1Name("channel1");
static constexpr UT_StringLit theChannel2Name("channel2");
// Create the rig.
// Rigs define the names of the transforms in a tree hierarchy.
sopCreateRig(const char *path, bool enable_blendshapes)
{
UT_String rig_name = path;
rig_name += "?rig";
UT_StringArray transforms;
transforms.append("skin"); // SOP_SKIN_RIG_INDEX
transforms.append("parent"); // SOP_PARENT_RIG_INDEX
transforms.append("child"); // SOP_CHILD_RIG_INDEX
UT_IntArray child_counts;
child_counts.append(0); // 'skin' has 0 children
child_counts.append(1); // 'parent' has 1 child
child_counts.append(0); // 'child' has 0 children
UT_IntArray children;
// 'skin' has no children so we don't need to append anything for it
children.append(SOP_CHILD_RIG_INDEX); // 'parent' has 'child' as the child
// 'child' has no children so we don't need to append anything for it
// now construct the rig
if (!rig->construct(transforms, child_counts, children))
return nullptr;
// Define a couple channels for use with the blendshape deformer.
if (enable_blendshapes)
{
channels.append(theChannel1Name.asHolder());
channels.append(theChannel2Name.asHolder());
default_values.appendMultiple(0.0, 2);
// These channels are not associated with any joint.
UT_IntArray transforms;
transforms.appendMultiple(-1, 2);
if (!rig->addChannels(channels, default_values, transforms, errors))
return nullptr;
}
return rig;
}
static GU_Detail *
sopCreateSphere(bool for_default)
{
GU_Detail *shape = new GU_Detail;
GU_PrimSphereParms sphere(shape);
if (for_default)
{
sphere.freq = 2;
sphere.type = GEO_PATCH_TRIANGLE;
}
else
{
// Position and scale the collision sphere to account for the
// deformation animation.
sphere.xform.scale(1.5, 1, 1.5);
sphere.xform.translate(0, 1, 0.5);
}
return shape;
}
/// Create a blendshape input from a sparse subset of the base shape's points.
static void
sopAddBlendshapeInput(const GU_Detail &src, int increment,
const UT_StringRef &channel_name, GU_DetailHandle &gdh,
UT_StringHolder &shape_name)
{
shape_name.format("{}.blendshape.{}", GU_AGENT_LAYER_DEFAULT,
channel_name);
GU_Detail *gdp = new GU_Detail;
gdh.allocateAndSet(gdp);
// The 'id' attribute is used to match up points from the base shape.
GA_RWHandleI id_attrib =
UT_Matrix4D xform(1.0);
xform.scale(1.5);
const GA_Size n = src.getNumPoints() / increment;
GA_Offset ptoff = gdp->appendPointBlock(n);
for (GA_Index i = 0; i < n; ++i, ++ptoff)
{
const GA_Index src_idx = i * increment;
UT_Vector3 pos = src.getPos3(src.pointOffset(src_idx)) * xform;
gdp->setPos3(ptoff, pos);
id_attrib.set(ptoff, src_idx);
}
}
static void
sopAddBlendshapes(const GU_AgentRig &rig, GU_AgentShapeLib &shapelib,
GU_DetailHandle &base_shape)
{
GU_DetailHandleAutoWriteLock base_shape_gdp(base_shape);
// Create the input shapes and add them to the shape library.
UT_StringHolder shape1_name;
sopAddBlendshapeInput(*base_shape_gdp, 3, theChannel1Name.asRef(), shape1,
shape1_name);
UT_StringHolder shape2_name;
sopAddBlendshapeInput(*base_shape_gdp, 4, theChannel2Name.asRef(), shape2,
shape2_name);
shapelib.addShape(shape1_name, shape1);
shapelib.addShape(shape2_name, shape2);
// Add these shapes as inputs for the base shape, so that the blendshape
// deformer can locate them.
UT_StringArray shape_names;
shape_names.append(shape1_name);
shape_names.append(shape2_name);
UT_StringArray channel_names;
channel_names.append(theChannel1Name.asHolder());
channel_names.append(theChannel2Name.asHolder());
GU_AgentBlendShapeUtils::addInputsToBaseShape(*base_shape_gdp, shape_names,
channel_names);
}
// For simplicity, we bind all points to all the transforms except for
// SOP_SKIN_RIG_INDEX.
static void
sopAddSkinWeights(const GU_AgentRig &rig, const GU_DetailHandle &shape)
{
GU_Detail *gdp = gdl.getGdp();
// Create skinning attribute with 2 regions and each point is bound to both
// rig transforms.
int num_regions = (SOP_JOINT_END - SOP_JOINT_BEGIN);
GEO_Detail::geo_NPairs num_pairs_per_pt(num_regions);
GA_RWAttributeRef capt = gdp->addPointCaptureAttribute(num_pairs_per_pt);
int regions_i = -1;
capt, regions_i);
regions->setObjectCount(num_regions);
// Tell the skinning attribute the names of the rig transforms. This needs
// to be done after calling regions->setObjectCount().
for (int i = 0; i < num_regions; ++i)
paths.setPath(i, rig.transformName(SOP_JOINT_BEGIN + i));
// Set up the rest transforms for the skin weight bindings. For efficiency,
// these are actually stored in the skin weight bindings as the INVERSE of
// the world space rest transform of the joint.
for (int i = 0; i < num_regions; ++i)
{
// Recall that the unit sphere is created at the origin. So to position
// the parent joint at the base of the sphere, it should be a position
// (0, -1, 0). However, since these are actually inverses, we'll
// translate by the opposite sign instead.
if (i == 0)
r.myXform.translate(0, +1.0, 0);
else
r.myXform.translate(0, -1.0, 0);
regions->setObjectValues(i, regions_i, r.floatPtr(),
}
// Set up the weights
weights->setEntries(capt, num_regions);
for (GA_Offset ptoff : gdp->getPointRange())
{
for (int i = 0; i < num_regions; ++i)
{
// Set the region index that the point is captured by.
// Note that these index into 'paths' above.
weights->setIndex(capt, ptoff, /*entry*/i, /*region*/i);
// Set the weight that the point is captured by transform i.
// Notice that all weights for the point should sum to 1.
fpreal weight = 1.0/num_regions;
weights->setData(capt, ptoff, /*entry*/i, weight);
}
}
}
// Default convention is to prefix the shape name by the layer name
#define SOP_DEFAULT_SKIN_NAME GU_AGENT_LAYER_DEFAULT".skin"
#define SOP_COLLISION_SKIN_NAME GU_AGENT_LAYER_COLLISION".skin"
// Create the shape library which contains a list of all the shape geometries
// that can be attached to the rig transforms.
sopCreateShapeLib(const char *path, const GU_AgentRig &rig,
bool enable_blendshapes)
{
UT_String shapelib_name = path;
shapelib_name += "?shapelib";
GU_DetailHandle skin_geo;
skin_geo.allocateAndSet(sopCreateSphere(true), /*own*/true);
sopAddSkinWeights(rig, skin_geo);
if (enable_blendshapes)
sopAddBlendshapes(rig, *shapelib, skin_geo);
shapelib->addShape(SOP_DEFAULT_SKIN_NAME, skin_geo);
// The collision geometry is intended to be simplified versions of the
// default layer shapes. The bounding box for the default layer shape is
// computed from the corresponding collision layer shape.
GU_DetailHandle coll_geo;
coll_geo.allocateAndSet(sopCreateSphere(false), /*own*/true);
shapelib->addShape(SOP_COLLISION_SKIN_NAME, coll_geo);
return shapelib;
}
// Create a default layer with the sphere as the geometry bound to the rig.
// Layers assign geometry from the shapelib to be used for the rig
// transforms.
// Agents must have at least 2 layers:
// GU_AGENT_LAYER_DEFAULT ("default"
// - Used for display/render
// GU_AGENT_LAYER_COLLISION ("collision")
// - Simple geometry to be used for the bounding box that
// encompasses the corresponding shape in default layer for all
// possible local deformations.
// In general, it can have more layers and we can set which of those we use
// for the default and collision.
sopCreateDefaultLayer(
const char *path,
const GU_AgentRigPtr &rig,
const GU_AgentShapeLibPtr &shapelib,
bool enable_blendshapes)
{
UT_StringArray shape_names;
UT_IntArray transform_indices;
// Simply bind the skin geometry to the 'skin' transform which we know is
// transform index 0.
transform_indices.append(SOP_SKIN_RIG_INDEX);
// Use either the normal linear skin deformer, or the blendshape + skin
// deformer.
// For a shape that is rigidly transformed, no deformer is needed (nullptr)
if (enable_blendshapes)
else
UT_String unique_name = path;
unique_name += "?default_layer";
= GU_AgentLayer::addLayer(unique_name, rig, shapelib);
if (!layer->construct(shape_names, transform_indices, deformers))
return nullptr;
layer->setName(layer_name);
return layer;
}
// See also comments for sopCreateDefaultLayer().
sopCreateCollisionLayer(
const char *path,
const GU_AgentRigPtr &rig,
const GU_AgentShapeLibPtr &shapelib)
{
UT_StringArray shape_names;
UT_IntArray transform_indices;
UT_Array<bool> deforming;
// For character rigs, the collision shapes are typically attached to the
// joint transforms so that they can proxy for skin deformation.
transform_indices.append(SOP_PARENT_RIG_INDEX);
deforming.append(false); // has NO skin weights
UT_String unique_name = path;
unique_name += "?collision_layer";
layer = GU_AgentLayer::addLayer(unique_name, rig, shapelib);
if (!layer->construct(shape_names, transform_indices, deforming))
return nullptr;
layer->setName(layer_name);
return layer;
}
static fpreal*
sopAddTrack(CL_Clip &chans, const GU_AgentRig &rig, int i, const char *trs_name)
{
str.sprintf("%s:%s", rig.transformName(i).buffer(), trs_name);
return chans.addTrack(str.buffer())->getData();
}
// Create some bouncy animation for the agent
sopCreateBounceClip(CL_Clip &chans, const GU_AgentRigPtr &rig, fpreal height,
bool enable_blendshapes)
{
int num_samples = chans.getTrackLength();
// Set the ty of the 'parent' transform that bounces. Note that these
// transforms here are in local space. The valid channel names are:
// Translate: tx ty tz
// Rotate: rx ry rz (euler angles in degrees, XYZ rotation order)
// Scale: sx sy sz
fpreal *ty = sopAddTrack(chans, *rig, SOP_PARENT_RIG_INDEX, "ty");
fpreal *sy = sopAddTrack(chans, *rig, SOP_PARENT_RIG_INDEX, "sy");
for (int i = 0; i < num_samples; ++i)
{
ty[i] = height * sin(i * M_PI / (num_samples-1));
// add some squash and stretch
sy[i] = 1.0 - SYSabs(0.5 * sin(i * M_PI / (num_samples-1)));
}
// Set the ty of the 'child' transform. Note that these transforms here are
// in local space.
fpreal *tz = sopAddTrack(chans, *rig, SOP_CHILD_RIG_INDEX, "tz");
ty = sopAddTrack(chans, *rig, SOP_CHILD_RIG_INDEX, "ty");
for (int i = 0; i < num_samples; ++i)
{
ty[i] = 2.0; // sphere diameter
tz[i] = 1.5 * sin(i * M_PI / (num_samples-1)); // sway a bit forwards
}
// Set up channels to drive the blendshapes.
if (enable_blendshapes)
{
fpreal *chan1 = chans.addTrack(theChannel1Name.asHolder())->getData();
fpreal *chan2 = chans.addTrack(theChannel2Name.asHolder())->getData();
for (int i = 0; i < num_samples; ++i)
{
chan1[i] = 0.5 + 0.5 * SYScos(i * 2 * M_PI / num_samples + M_PI);
chan2[i] = 0.5 + 0.5 * SYScos(i * 2 * M_PI / num_samples);
}
}
// Finally load the agent clip from the CL_Clip animation we created
if (!clip)
return nullptr;
clip->load(chans);
return clip;
}
static constexpr UT_StringLit theCustomDataItemType("bouncyagentdata");
static constexpr UT_StringLit theValueToken("myvalue");
static constexpr UT_StringLit theNameToken("myname");
/// Example implementation of a custom data item that can be added to a
/// GU_AgentDefinition.
{
public:
GU_BouncyAgentCustomData() : myValue(0) {}
: GU_AgentCustomDataItem(), myName(name), myValue(value)
{
}
{
}
const UT_StringHolder &dataItemType() const override
{
return theCustomDataItemType.asHolder();
}
int64 getMemoryUsage(bool inclusive) const override
{
int64 mem = inclusive ? sizeof(*this) : 0;
mem += myName.getMemoryUsage(false);
return mem;
}
const UT_StringHolder &name() const override { return myName; }
int value() const { return myValue; }
/// @{
/// Implements serialization to and from JSON.
/// For debugging, save to an ASCII (.geo) geometry file.
bool load(UT_JSONParser &p) override
{
for (auto it = p.beginMap(); !it.atEnd(); ++it)
{
if (!p.parseKey(key))
return false;
if (key == theValueToken.asHolder())
{
if (!p.parseInt(myValue))
return false;
}
else if (key == theNameToken.asHolder())
{
if (!p.parseString(myName))
return false;
}
else
{
p.addWarning("Unknown key '%s'", key.buffer());
if (!p.skipNextObject())
return false;
}
}
return true;
}
bool save(UT_JSONWriter &w) const override
{
bool ok = true;
ok = ok && w.jsonBeginMap();
ok = ok && w.jsonKey(theNameToken.asHolder());
ok = ok && w.jsonValue(myName);
ok = ok && w.jsonKey(theValueToken.asHolder());
ok = ok && w.jsonValue(myValue);
ok = ok && w.jsonEndMap();
return ok;
}
/// @}
private:
int64 myValue;
};
/// Entry point for registering GU_BouncyAgentCustomData.
void
{
theCustomDataItemType.asHolder(), GU_BouncyAgentCustomData::construct);
}
// Enable this to debug our definition
#define SOP_SAVE_AGENT_DEFINITION 0
// Create the agent definition
SOP_BouncyAgent::createDefinition(fpreal t) const
{
// Typically, the definition is loaded from disk which has a filename for
// each of the different parts. Since we're doing this procedurally, we're
// going to just make up some arbitrary unique names using our node path.
getFullPath(path);
const bool enable_blendshapes = BLENDSHAPES(t);
GU_AgentRigPtr rig = sopCreateRig(path, enable_blendshapes);
if (!rig)
return nullptr;
sopCreateShapeLib(path, *rig, enable_blendshapes);
if (!shapelib)
return nullptr;
GU_AgentLayerPtr default_layer =
sopCreateDefaultLayer(path, rig, shapelib, enable_blendshapes);
if (!default_layer)
return nullptr;
GU_AgentLayerPtr collision_layer = sopCreateCollisionLayer(path,
rig, shapelib);
if (!collision_layer)
return nullptr;
CL_Clip chans(CHgetManager()->getSample(CLIPLENGTH(t)));
chans.setSampleRate(CHgetManager()->getSamplesPerSec());
sopCreateBounceClip(chans, rig, HEIGHT(t), enable_blendshapes);
if (!clip)
return nullptr;
#if SOP_SAVE_AGENT_DEFINITION
// Once we have the definition, we can save out the files for loading with
// the Agent SOP as well. Or, we can examine them for debugging purposes.
{
UT_AutoJSONWriter writer("bouncy_rig.rig", /*binary*/false);
rig->save(writer);
}
{
UT_AutoJSONWriter writer("bouncy_shapelib.bgeo", /*binary*/true);
shapelib->save(writer);
}
{
UT_AutoJSONWriter writer("bouncy_layer.default.lay", /*binary*/false);
default_layer->save(writer);
}
{
UT_AutoJSONWriter writer("bouncy_layer.collision.lay", /*binary*/false);
collision_layer->save(writer);
}
{
chans.save("bouncy_bounce.bclip");
}
#endif
// The agent definition is used to create agent primitives. Many agent
// primitives can share the same definition.
GU_AgentDefinitionPtr def(new GU_AgentDefinition(rig, shapelib));
// The definition has a number of layers that be can assigned
def->addLayer(default_layer);
def->addLayer(collision_layer);
// ... and a number of clips that can be assigned to specific agent prims
def->addClip(clip);
// Add some custom data for demonstration purposes.
def->addCustomDataItem(new GU_BouncyAgentCustomData("mycustomdata", 42));
return def;
}
/*static*/ int
SOP_BouncyAgent::onReload(
void *data, int index, fpreal t, const PRM_Template *tplate)
{
SOP_BouncyAgent* sop = static_cast<SOP_BouncyAgent*>(data);
if (!sop->getHardLock()) // only allow reloading if we're not locked
{
sop->myDefinition.reset();
sop->forceRecook();
}
return 1;
}
// Compute the output geometry for the SOP.
{
fpreal t = context.getTime();
// We must lock our inputs before we try to access their geometry.
// OP_AutoLockInputs will automatically unlock our inputs when we return.
// NOTE: Don't call unlockInputs yourself when using this!
OP_AutoLockInputs inputs(this);
if (inputs.lock(context) >= UT_ERROR_ABORT)
return error();
// Duplicate the input geometry, but only if it was changed
int input_changed;
duplicateChangedSource(/*input*/0, context, &input_changed);
// Detect if we need to rebuild the agent definition. For simplicity, we'll
// rebuild if any of our agent parameters changed. Note the use of the
// bitwise or operator to ensure that isParmDirty() is always called for
// all parameters.
bool agent_changed = isParmDirty(sopAgentName.getToken(), t);
agent_changed = isParmDirty(sopHeight.getToken(), t);
agent_changed |= isParmDirty(sopClipLength.getToken(), t);
agent_changed |= isParmDirty(sopEnableBlendshapes.getToken(), t);
if (!myDefinition)
{
agent_changed = true;
input_changed = true;
}
if (agent_changed)
{
myDefinition = createDefinition(t);
if (!myDefinition)
{
addError(SOP_MESSAGE, "Failed to create definition");
return error();
}
}
if (input_changed)
{
// Delete all the primitives, keeping only the points
// Create the agent primitives
myPrims.clear();
for (GA_Offset ptoff : gdp->getPointRange())
{
myPrims.append(GU_Agent::agent(*gdp, ptoff));
}
// Bumping these 2 attribute owners is what we need to do when adding
// pack agent prims because it has a single vertex.
gdp->getPrimitiveList().bumpDataId(); // modified primitives
}
if (agent_changed || input_changed)
{
// Create a name attribute for the agents
"name", 1));
GU_AgentLayerConstPtr current_layer =
myDefinition->layer(UTmakeUnsafeRef(GU_AGENT_LAYER_DEFAULT));
GU_AgentLayerConstPtr collision_layer =
// Set the agent definition to the agent prims
UT_String agent_name;
AGENTNAME(agent_name, t);
int name_i = 0;
for (GU_PrimPacked *pack : myPrims)
{
GU_Agent* agent = UTverify_cast<GU_Agent*>(pack->hardenImplementation());
agent->setDefinition(pack, myDefinition);
agent->setCurrentLayer(pack, current_layer);
agent->setCollisionLayer(pack, collision_layer);
// We only have 1 clip that can be used in the definition here.
clips.append(myDefinition->clip(0).name());
agent->setClipsByNames(pack, clips);
// Convention for the agent primitive names is agentname_0,
// agentname_1, agentname_2, etc.
name.sprintf("%s_%d", agent_name.buffer(), name_i);
name_attrib.set(pack->getMapOffset(), name.buffer());
++name_i;
}
// Mark what modified
name_attrib.bumpDataId();
}
// Set the clip information for the agents. In general, agents can be set
// to evaluate an blended array of clips to evaluated at a specific clip
// offset.
for (GU_PrimPacked *pack : myPrims)
{
GU_Agent* agent = UTverify_cast<GU_Agent*>(pack->hardenImplementation());
agent->setClipTime(pack, /*clip index*/0, CLIPOFFSET(t));
}
gdp->getPrimitiveList().bumpDataId(); // we modified primitives
return error();
}
// Provide input labels.
const char *
SOP_BouncyAgent::inputLabel(unsigned /*input_index*/) const
{
return "Points to attach agents";
}