HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Implementing a New Operator

Introduction

The most common reason for writing a new object operator is to implement a new transformation algorithm. This is achieved by computing a new transform matrix. As briefly explained in Object Cooking, the transform matrix calculation happens in OBJ_Node::cookMyObj() method, and you can overload it to implement own computation. However, when implementing a new object operator, often it is not necessary to dramatically alter this method, but rather it is sufficient to overload one of the following virtual methods, which compute a sub-component of the whole matrix:

On rare occasions, when overloading these methods is not sufficient, you will need to write own cookMyObj() method and you can base it on the skeleton provided in Object Cooking. This is illustrated in OBJ_WorldAlign.C example and described in Overriding cookMyObj() section below.

Note
Unlike other node contexts, you need to call flags().setTimeDep(true) in these method overrides if you compute a value based on the evaluation time explicitly. This is because objects break down the computations into smaller pieces than the entire cookMyObj() process.

Object Node Example

You can find an example of a simple implementation of an object node in the OBJ_Shake.C file in the toolkit samples. This example implements an object that computes a randomly jittered translation matrix and appends it to the chain of other transforms to achieve an earthquake effect.

The shake object derives from OBJ_Geometry and inherits all of its parameters and adds own "jitter" parameter. The following code defines a list of parameters that the operator wants to add:

// the name of the parameter
static PRM_Name OBJjitter("jitter", "Jitter scale");
// the list of the parameters
static PRM_Template templatelist[] =
{
// one parameter with three float components, named "jitter",
// and the default value of 1 (in each component)
PRM_Template() // terminator
};

The following function retrieves the parameters from the base class and concatenates them with own parameters. The final list returned by this function will be passed to the OP_OperatorTable::newOperator() during the call that registers this operator.

OBJ_Shake::buildTemplatePair(OP_TemplatePair *prevstuff)
{
OP_TemplatePair *shake, *geo;
shake = new OP_TemplatePair(templatelist, prevstuff);
shake);
return geo;
}

Now that we defined the jitter parameter, we can evaluate it in the code using the following member methods:

float JX(float t) { return evalFloat("jitter", &shakeIndirect[0], 0, t); }
float JY(float t) { return evalFloat("jitter", &shakeIndirect[0], 1, t); }
float JZ(float t) { return evalFloat("jitter", &shakeIndirect[0], 2, t); }

The shakeIndirect array is a cache for the index number of the "jitter" parameter value. This cache is initiated during the first call, and is reused during the subsequent calls. This speeds up evaluation by avoiding the search for the "jitter" name in the list in the OP_Parameters::evalFloat() call.

The base class, OBJ_Geometry, also uses a class static parameter index cache, which this derived class shares for the base class parameters. This is fine in this particular example, but if your derived class were to have a fully customized template list that changes the indices of these base class parameters, you would have to provide a different parameter index cache by overridding the OBJ_Geometry::getIndirect() method.

For example, you could do the following:

enum OBJ_ShakeIndex
{
I_SHAKE_JITTER = I_N_GEO_INDICES,
I_N_SHAKE_INDICES // should always be last in the list
};
float JX(float t) { return evalFloat("jitter", &getIndirect()[I_SHAKE_JITTER], 0, t); }
float JY(float t) { return evalFloat("jitter", &getIndirect()[I_SHAKE_JITTER], 1, t); }
float JZ(float t) { return evalFloat("jitter", &getIndirect()[I_SHAKE_JITTER], 2, t); }
virtual int *getIndirect() const { return shakeIndirect; }

with the following in your constructor:

// shakeIndirect is initialized to an array big enough for all our
// parameters (including those from the base class), each initialized
// to -1 -- ie not accessed yet.
if (!shakeIndirect) shakeIndirect = allocIndirect(I_N_SHAKE_INDICES);

There is a standard method signature for a node instance factory function that creates the instances. It is used by houdini to create a new node:

OBJ_Shake::myConstructor(OP_Network *net, const char *name, OP_Operator *op)
{
return new OBJ_Shake(net, name, op);
}

Most of the time this factory function will simply create a new instance of the class you are implementing.

Now that both parameter list and factory function are available, you can register the new operator with Houdini. When a dynamic library is loaded by Houdini, it searches for the newObjectOperator() function, and if found, it calls it. This mechanism is used to register the new operator in Houdini with the OP_OperatorTable::addOperator() method:

void
{
table->addOperator(new OP_Operator("hdk_shake", "Shake",
OBJ_Shake::myConstructor,
OBJ_Shake::buildTemplatePair(0),
OBJ_Shake::theChildTableName,
0, 1,
0));
}

The "hdk_shake" is a unique name of the new object operator. The "hdk_" prefix is used to avoid name collisions in the future, when a native operator by the same name could be added.

At this point, Houdini will be able to create a new "hdk_shake" node that has a given set of parameters, but this node will not behave much differently than the standard geometry node (whose behavior it inherited). To make the behavior different, the following method alters the computation of the transform matrices:

int
OBJ_Shake::applyInputIndependentTransform(OP_Context &context, UT_DMatrix4 &mat)
{
const float t = context.getTime();
float jx, jy, jz;
unsigned seed;
int modified;
// call the base class, as well
return modified;
// compute the jitters in x, y, and z direction
jx = JX(t); jy = JY(t); jz = JZ(t);
jx *= 2*UTfastRandom(seed) - 1.0;
seed ^= 0xdeadbeef;
jy *= 2*UTfastRandom(seed) - 1.0;
seed ^= 0xfadedcab;
jz *= 2*UTfastRandom(seed) - 1.0;
// apply the jitter to the matrix passed in the argument
mat.pretranslate(jx, jy, jz);
// since the jitter value is frame dependent and this object won't
// be flagged as time dependent through any other method, we need to
// explicitly flag it here.
// NB: this flag gets reset at the beginning of every cook, unless
// tranform caching is enabled by the object parameter.
// When transforms are cached, this flag is cleared before the cook on
// which there was a miss that invalidated all cached transforms for
// that object.
flags().setTimeDep(true);
// returning 1 indicates that 'mat' has changed
return 1;
}

The OBJ_Node::applyInputIndependentTransform() is overloaded because the translation jitters depend only on the node's parameter and not on any external nodes. The OBJ_Node::getParmTransform() was not overloaded because "jitter" parameter is not a standard translation, rotation, or scale parameter, but that method could conceivably be overloaded instead of OBJ_Node::applyInputIndependentTransform().

Notice that this method also calls setTimeDep() because the randomness of the jittering depends on the time, and when the time changes, the object needs to recompute to calculate a new transformation.

Overriding cookMyObj()

In some cases it may be insufficient to overload the virtual methods that are invoked from OBJ_Node::cookMyObj(), and you may need to overload the OBJ_Node::cookMyObj() itself. The OBJ_WorldAlign.C implements an operator that tries to align the orientation of the object to the world axes. Although, this could be achieved by overloading the OBJ_Node::buildLookAt() method, for the purpose of illustrating the technique, this example implements it by overloading OBJ_Node::cookMyObj():

OBJ_WorldAlign::cookMyObj(OP_Context &context)
{
OP_ERROR errorstatus;
UT_DMatrix4 rotation4;
// base class
errorstatus = OBJ_Geometry::cookMyObj(context);
// get rid of the rotation component in the matrices
myWorldXform.extractRotate(rotation);
if( ! rotation.invert() )
{
// multiply the local and the world matrices by the inverse of the
// rotation that they contain, which cancels it out
rotation4 = rotation;
myWorldXform = rotation4 * myWorldXform;
myXform = rotation4 * myXform;
}
return errorstatus;
}

In the above code, the main point of overloading the OBJ_Node::cookMyObj() method is to ultimately alter the OBJ_Node::myXform and OBJ_Node::myWorldXform matrices for the local and global transformations. Calling the base class OBJ_Geometry::cookMyObj() performs all the calculations inherited from the base class and it also dirties the world inverse matrix, which is recalculated the next time it is needed.