HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Extending hscript: Custom Expression Functions

General

The functions built-in to Houdini's hscript expression language can listed by typing exhelp in the textport. This section will show you how to add your own custom functions to Houdini.

Adding a custom expression can be done anywhere and is not confined to a particular part of the code. For example, if a custom SOP needs a specific expression function, that function can be added in the SOP code and later removed when it is no longer needed.

This document deals with the mechanism for adding custom expressions using Command Library extensions.

An expression function is added by specifying a callback function for evaluation. This callback function takes two arguments. The first is a place to store the result of the evaluation, the second is an array of arguments. These parameters are EV_SYMBOL types.

Each EV_SYMBOL contains the following data field:

struct EV_SYMBOL {
// ...
// ...
};

which is defined as follows

typedef union {
float fval;
char *sval;
void *data;

When you register your callback function, you will specify the type of each argument to the function. Make sure that your callback's reference from the union matches the argument type that you registered.

Here is a simple call back function. The function simply returns the maximum of two floating point values.

#include <EXPR/EXPR.h>
EV_START_FN(fn_max)
{
if (argv[0]->value.fval > argv[1]->value.fval)
result->value.fval = argv[0]->value.fval;
else
result->value.fval = argv[1]->value.fval;
}

There's a class in the expression library called EV_FUNCTION used to define the signature and behavior of a callback. This is the constructor of a callback entry:

EV_FUNCTION(unsigned flag = 0,
const char *name = 0,
int num_args = 0,
int result_type = EV_TYPEFLOAT,
const int *argTypes = 0,
EXPRfuncCallback callback = 0,
EXPRopDependencyCallback dependCB = 0,
EXPRopChangeRefCallback changeOpRefCB = 0,
int is_safe = 1,
bool is_threadsafe = false);

The only time you should ever set flag to anything but 0 is when your function is a time dependent function. For example, if you're hooking in a motion capture device, you may want the function to cause the user of the function to re-cook whenever time changes. In this case, you should make set the flag to CH_EXPRTIME (found in CH_Support.h). If you wish to dynamically control the time dependency status as a result of running your function, you can also modify the func_flags parameter of your callback function (declared in the EV_START_FN macro).

The return type should be a type that you feel comfortable with. Usually, the return type is either one of EV_TYPEFLOAT or EV_TYPESTRING. The argument types are also usually these types. The only time that you might want to have a different type is if you extend the types in the expression library. However, that subject is beyond the scope of this document.

Specify functions for dependCB and changeOpRefCB if any of your function's arguments are used to specify paths to a named item (eg. nodes, parameters, channels, etc.). These functions are used by Houdini to update explicit references in expressions when nodes are renamed. The dependCB method should add expression dependencies by calling OP_Node::addExprOpDependency(), OP_Node::addExprOpDependency1From2(), or OP_Node::addExprOpParmDependency() for the relevant arguments. The changeOpRefCB method should call OP_Node::changeExprOpRef(), OP_Node::changeExprOpRef1From2(), or OP_Node::changeExprOpParmRef() for the relevant arguments. In both of these callbacks, the EV_SYMBOL entry for an argument will be non-NULL only if the argument is an explicit string.

Specify 0 for is_safe to prevent the function from being run when the code comes from the help browser.

The easiest way of installing functions is to create a table containing all the definitions, then in the DSO install function, you can simply traverse all the table entries and install them. For example, if we wanted to add the function described above:

#include <UT/UT_DSOVersion.h> // needed for every custom DSO
static int maxArgTypes[] = { EV_TYPEFLOAT, EV_TYPEFLOAT };
static EV_FUNCTION theFunctionTable[] = {
EV_FUNCTION(0, // The flag
"max", // The name of the function
2, // The number of arguments
EV_TYPEFLOAT, // My return type
maxArgTypes, // The argument types for the function
fn_max), // The callback
EV_FUNCTION() // Null entry to indicate it's the end of the table
};
void
{
for (int i = 0; theFunctionTable[i].name; i++)
ev_AddFunction(&theFunctionTable[i]);
}

Functions installed with ev_AddFunction can later be removed with ev_DeleteFunction.

There are really only four predominant types in the expression library, vectors, matrices, strings, and floats. All values are stored in the EV_TOKENVALUE structure.

To store a float, simply reference the fval element of the value field.

Strings are stored in the sval element of the value field. A string is responsible for allocating space for itself. If your function returns a string value, you must allocate space for the string using malloc() or strdup(). You do not have to worry about freeing the space, that is done automatically. If you do have to free the space, make sure to set the sval element to NULL.

Be careful with strings as the sval element may be a NULL pointer. In this case, it's up to you to determine how to interpret the value.

Vector types use the ev_Vector class to hold their data (EX_Vector.h). There are methods to query the size of the vector and to set values. The vector is stored as a void pointer in the value.data field of a symbol, so to get the vector out of a symbol, you would use something like:

ev_Vector *vector = (ev_Vector *)arg[0]->value.data;

So, for example, the function to do a sum of two vectors might be written as:

EV_START_FN(fn_vsum)
{
ev_Vector *v0 = (ev_Vector *)arg[0]->value.data;
ev_Vector *v1 = (ev_Vector *)arg[1]->value.data;
ev_Vector *sum = (ev_Vector *)result->value.data;
int len0, len1, i;
len0 = v0->getSize();
len1 = v1->getSize();
if (len0 > len1)
{
// Here, v0 has more elements than v1
sum->grow(len0);
for (i = 0; i < len1; i++)
sum->setValue(i, v0->fastGet(i)+v1->fastGet(i));
// Now fill out the rest
for (; i < len0; i++)
sum->setValue(i, v0->vastGet(i));
}
else
{
// Here, v1 has more elements than v0 (or the same)
sum->grow(len1);
for (i = 0; i < len0; i++)
sum->setValue(i, v0->fastGet(i)+v1->fastGet(i));
// Now fill out the rest
for (; i < len1; i++)
sum->setValue(i, v1->vastGet(i));
}
}

The matrix type is similar to the vector type, except that it uses the ev_Matrix class (found in EX_Matrix.h) to store its data.

Create a file called "exprhelp" anywhere within the Houdini search path (i.e. /usr/local/houdini/help, $HOME/houdini/help, $HIP/houdini/help, etc.) to contain the help for your commands. Start a help entry with a line containing an open brace followed by a line with the name of the function and its parameters. Finish a help entry with a line containing a close brace. For example:

{
vsum vec1 vec2
Compute the sum to two vectors.
}

All help files found in the Houdini search path are all merged together when displaying help.

Some expression functions, such as bbox and centroid have defines such as D_XMAX, D_XMIN, etc. From the viewpoint of the expression, these are integer parameters that have a specific value. Your new expression will already support all of the Houdini defines since these are global defines, so a user using D_XMIN will send 0 to your function, D_YMIN 1, and D_XMAX 3.

To define new global constants create them with CH_ExprDefine and install them using CH_Manager::addExpressionDefine.

static CH_ExprDefine centroidDefTable[] = {
{ "D_X", D_X },
{ "D_Y", D_Y},
{ "D_Z", D_Z},
{ 0, 0 },
};
void
{
CH_Manager *chman;
for (int i = 0; centroidDefTable[i].label; i++)
{
chman->addExpressionDefine(&centroidDefTable[i]);
}
}

Other examples can be found in expr/functions.C.

Data Dependencies

In addition to the name dependency handling described in the previous section, data dependencies also need to be explicitly marked if you evaluate a node or parameter. To do this, you should call the static OP_Node::addExtraInputToEvalChannel() methods for any node or parameter that is evaluated.

For DOP simulations, you should call OP_Node::addExtraInputToEvalChannel() to the micro-node returned by DOP_Parent::simMicroNode().

// Add a dependency onto a SOP_Node
// Add a dependency onto the simulation data of a OBJ_DopNet