HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Creating a CHOP Filter

This example is a copy of the Spring CHOP. It adds lag and overshoot motion to a position channel, as if the motion were resisted by a spring. The Spring CHOP can work in Time Slice mode for realtime processing.

Example Walkthrough

These code fragments can be found in the example HDK code in CHOP_Spring.C.

These headers are required for all HDK OPs.

#include <CHOP/CHOP_VariableList.h>

These headers are generally required for all CHOPs.

#include <UT/UT_IStream.h>
#include "CHOP_Spring.h"

UT_Interrupt is used for querying if the user interrupted the operation while cooking. UT_IStream is used by Time Slice mode to save out intermediate data across sessions.

CHOP_SWITCHER(7, "Spring");
static PRM_Name names[] = {
PRM_Name("springk", "Spring Constant"),
PRM_Name("mass", "Mass"),
PRM_Name("dampingk", "Damping Constant"),
PRM_Name("method", "Input Effect"),
PRM_Name("condfromchan", "Initial Conditions From Channel"),
PRM_Name("initpos", "Initial Position"),
PRM_Name("initspeed", "Initial Speed"),
};
static PRM_Name springMethodItems[] = {
PRM_Name("disp", "Position"),
PRM_Name("force", "Force"),
};
static PRM_ChoiceList springMethodMenu((PRM_ChoiceListType)
springMethodItems);
static PRM_Range springConstantRange(PRM_RANGE_RESTRICTED, 0.0,
PRM_RANGE_UI, 1000.0);
static PRM_Range massRange(PRM_RANGE_UI, 0.1,
PRM_RANGE_UI, 10.0);
static PRM_Range dampingConstantRange(PRM_RANGE_RESTRICTED, 0.0,
PRM_RANGE_UI, 10.0);
static PRM_Range initDispRange(PRM_RANGE_UI, -10.0,
PRM_RANGE_UI, 10.0);
static PRM_Range initVelRange(PRM_RANGE_UI, -100.0,
PRM_RANGE_UI, 100.0);
static PRM_Default springConstantDefault(100.0);
static PRM_Default massDefault(1.0);
static PRM_Default dampingConstantDefault(1.0);
static PRM_Default initDispDefault(0.0);
static PRM_Default initVelDefault(0.0);
CHOP_Spring::myTemplateList[] =
{
// First Page
PRM_Template(PRM_FLT, 1, &names[0], &springConstantDefault, 0,
&springConstantRange),
PRM_Template(PRM_FLT, 1, &names[1], &massDefault, 0,
&massRange),
PRM_Template(PRM_FLT, 1, &names[2], &dampingConstantDefault, 0,
&dampingConstantRange),
&springMethodMenu),
PRM_Template(PRM_FLT, 1, &names[5], &initDispDefault, 0,
&initDispRange),
PRM_Template(PRM_FLT, 1, &names[6], &initVelDefault, 0,
&initVelRange),
};
OP_TemplatePair CHOP_Spring::myTemplatePair(
CHOP_Spring::myTemplateList, &CHOP_Node::myTemplatePair);
bool
CHOP_Spring::updateParmsFlags()
{
bool changes = CHOP_Node::updateParmsFlags();
bool grab = GRAB_INITIAL();
changes |= enableParm("initpos", !grab);
changes |= enableParm("initspeed", !grab);
return changes;
}

This code defines the parameter dialog for the Spring CHOP. In addition to the spring constants, there is an option to model the input channel as either position or force, and an option to specify the initial spring conditions with parameters or by computing them from the initial input samples.

// 2 local variables, 'C' (the currently cooking track), 'NC' total # tracks.
enum
{
VAR_C = 200,
VAR_NC = 201
};
CHOP_Spring::myVariableList[] = {
{ "C", VAR_C, 0 },
{ "NC", VAR_NC, 0 },
{ 0, 0, 0 }
};
OP_VariablePair CHOP_Spring::myVariablePair(
CHOP_Spring::myVariableList, &CHOP_Node::myVariablePair);
CHOP_Spring::evalVariableValue(fpreal &v, int index, int thread)
{
switch(index)
{
case VAR_C:
// C marks the parameter as channel dependent - it must be re-eval'd
// for each track processed.
myChannelDependent=1;
v = (fpreal)my_C;
return true;
case VAR_NC:
v = (fpreal)my_NC;
return true;
}
return CHOP_Node::evalVariableValue(v, index, thread);
}

Most CHOPs have the C and NC local variables defined for the currently cooking channel and the total number of channels. These values are modified by the cook method and stashed in member variables in the class. To optimize the number of parameter evaluations, myChannelDependent is used to determine if the spring parameters need to be re-evaluated for each track.

CHOP_Spring::myConstructor(OP_Network *net,
const char *name,
{
return new CHOP_Spring(net, name, op);
}
CHOP_Spring::CHOP_Spring( OP_Network *net,
const char *name,
: CHOP_Realtime(net, name, op)
{
myParmBase = getParmList()->getParmIndex( names[0].getToken() );
mySteady = 0;
}
{
}

The Spring CHOP derives from CHOP_Realtime rather than CHOP_Node, which adds the ability to cook in Time Slice mode. mySteady is used as part of time slice cooking.

First, we'll look at the normal, non-timeslice cook method.

CHOP_Spring::cookMyChop(OP_Context &context)
{
const CL_Clip *clip = 0;
const CL_Track *track = 0;
CL_Track *new_track = 0;
int force_method;
int i, j,length, num_tracks, animated_parms;
fpreal spring_constant;
fpreal d1,d2,f,inc,d;
fpreal damping_constant;
fpreal initial_displacement;
fpreal initial_velocity;
fpreal acc, vel;
UT_Interrupt *boss;
int stop;
int count = 0xFFFF;
bool grab_init = GRAB_INITIAL();
// Copy the structure of the input, but not the data itself.
clip = copyInput(context, 0, 0, 1);
if (!clip)
return error();
force_method = METHOD();
// Initialize local variables
my_NC = clip->getNumTracks();
my_C= 0;
// evaluate all parameters and check if any are animated per channel with C
myChannelDependent=0;
spring_constant = SPRING_CONSTANT(context.getTime());
mass = MASS(context.getTime());
damping_constant = DAMPING_CONSTANT(context.getTime());
animated_parms = myChannelDependent;
inc = 1.0 / myClip->getSampleRate();
// If 'grab initial' is true, we determine the initial values from the
// input channel data instead of using the parameter settings (using
// the position and slope of the track at the first sample).
if(!grab_init)
{
initial_displacement = INITIAL_DISPLACEMENT(context.getTime());
initial_velocity = INITIAL_VELOCITY(context.getTime());
}
// An evaluation error occurred; exit.
return error();
// Mass must be > 0.
if (mass < 0.001)
{
mass = 0.001;
SET_MASS(context.getTime(), mass);
}

The beginning of the cook method evaluates all parameters, copies the structure of the input clip into the output clip, and determines if any errors occurred.

It also check to see if any of the spring constants used the local variable 'C', which would require them to be re-evaluated for each track.

// Begin the interruptable operation
boss = UTgetInterrupt();
stop = 0;
if(boss->opStart("Calculating Spring"))
{
if (clip)
{
num_tracks = clip->getNumTracks();
length = clip->getTrackLength();
// Loop over all the tracks
for (i=0; i<num_tracks; i++)
{
// update the local variable 'C' with the track number
my_C = i;
track = clip->getTrack(i);
new_track = myClip->getTrack(i);
// If the track is not scoped, copy it and continue to the next
if (!isScoped(track->getName()))
{
new_track = *track;
continue;
}
if(grab_init || animated_parms)
{
// re-evaluate parameters if one of them was determined to
// depend on the local var 'C' (track number)
if(animated_parms)
{
spring_constant = SPRING_CONSTANT(context.getTime());
mass = MASS(context.getTime());
if (mass < 0.001)
mass = 0.001;
damping_constant = DAMPING_CONSTANT(context.getTime());
}
// If determining the position and speed from the track,
// evaluate the first 2 samples and difference them.
if(grab_init)
{
initial_displacement = clip->evaluateSingle(track,0);
initial_velocity=(clip->evaluateSingle(track,1) -
initial_displacement);
}
}
// Run the spring algorithm on the track's data.
d1 = initial_displacement;
d2 = d1 - initial_velocity * inc;

The next part of the cook method begins looping over each input track. If a track is not scoped, it is copied as-is and the rest of the processing is avoided. Otherwise, the spring constants are re-evaluated if required ('C' used) and the initial conditions are computed if the user isn't specifying them via parameters.

for(j=0; j<length; j++)
{
// Periodically check for interrupts.
if(count--==0 && boss->opInterrupt())
{
stop = 1;
break;
}
// run the spring equation
f = track->getData()[j];
if(!force_method)
f *= spring_constant;
vel = (d1-d2) / inc;
acc = (f - vel*damping_constant - d1*spring_constant)/mass;
vel += acc * inc;
d = d1 + vel * inc;
new_track->getData()[j] = d;
// update the previous displacements
d2 = d1;
d1 = d;
}
if(stop || boss->opInterrupt())
{
stop = 1;
break;
}
}
}
}
// opEnd must always be called, even if opStart() returned 0.
boss->opEnd();
return error();
}

The last part of the code loops over the samples in the input track and applies the simple spring equation to them, assigning the results to the output track.

Now, the next part of the file deals with the realtime processing of the spring effect. If you are creating a filter that doesn't work in time slice mode, or works in realtime without any changes to the cookMyChop() method, then you can skip to the bottom where the node is registered. Otherwise, read on.

// ---------------------------------------------------------------------------
// Realtime data block for stashing intermediate values between realtime cooks
// Only used in Time Slice mode.
class ut_SpringData : public ut_RealtimeData
{
public:
ut_SpringData(const char *name, fpreal d, fpreal v);
virtual ~ut_SpringData() {}
fpreal myDn1;
fpreal myDn2;
virtual bool loadStates(UT_IStream &is, int version);
virtual bool saveStates(UT_OStream &os);
};
ut_SpringData::ut_SpringData(const char *name, fpreal d1, fpreal d2)
: ut_RealtimeData(name),
myDn1(d1),
myDn2(d2)
{
}
bool
{
if (!ut_RealtimeData::loadStates(is, version))
return false;
if (!is.read<fpreal64>(&myDn1))
return false;
if (!is.read<fpreal64>(&myDn2))
return false;
return true;
}
bool
{
return true;
}

This helper class defines the Spring CHOP's instance of a realtime data block. It must derive from the ut_RealtimeBlock base class. This class is used to stash intermediate data that is needed between cooks in order to keep the effect running seamlessly between realtime updates. In this case, we need to stash the previous two displacements.

A realtime data block also needs load and save methods to store these conditions in the hip file, so that the CHOP's values don't explode off to infinity or enter some other bad state when the file is opened and the CHOP cooked again (user input, like audio, controller motion or a periodic timer).

int
CHOP_Spring::isSteady() const
{
// 'Steady' indicates that the CHOP has reached a steady state and can
// stop cooking every frame. An input must change in order to begin
// springing again.
return mySteady;
}

Realtime CHOPs can reach a 'steady state' where they can stop cooking every frame. A change in the input is then needed to start the CHOP cooking again.

In this example, a flag is used to indicate when a steady state has been reached. With a spring, it's when the springing motion has settled to a rest position.

CHOP_Spring::cookMySlice(OP_Context &context, int start, int end)
{
const CL_Clip *clip = inputClip(0,context);
const CL_Track *track = 0;
CL_Track *new_track = 0;
int force_method;
int i, j;
fpreal spring_constant;
fpreal d1,d2,f,t,inc,d, acc,vel,oldp;
fpreal damping_constant;
ut_SpringData *block;
fpreal delta;
int animated_parms;
force_method = METHOD();
my_NC = clip->getNumTracks();
my_C= 0;
// Again, evaluate the parameters and see if they depend on C
myChannelDependent=0;
spring_constant = SPRING_CONSTANT(context.getTime());
mass = MASS(context.getTime());
damping_constant = DAMPING_CONSTANT(context.getTime());
animated_parms = myChannelDependent;
inc = 1.0 / myClip->getSampleRate();
if (mass < 0.001)
mass = 0.001;
mySteady = 1;

The realtime cook method is called cookMySlice() rather than cookMyChop(). In addition to the context, the cook frame interval is passed as well, through start and end.

The initial part of the realtime cook method looks almost the same as the normal cook method, although we don't copy anything from the input clip. The output clip is already set up correctly for the interval required.

for(i=0; i<myClip->getNumTracks(); i++)
{
my_C = i;
track = clip->getTrack(i);
new_track = myClip->getTrack(i);
// If this track isn't scoped, just copy the data.
if(!isScoped(new_track->getName()))
{
clip->evaluateTime(track,
myClip->getTime(start+myClip->getStart()),
myClip->getTime(end+myClip->getStart()),
new_track->getData(), myClip->getTrackLength());
continue;
}
// Re-evaluate the spring parameters if one of them depends on C
if(animated_parms)
{
spring_constant = SPRING_CONSTANT(context.getTime());
mass = MASS(context.getTime());
if (mass < 0.001)
mass = 0.001;
damping_constant = DAMPING_CONSTANT(context.getTime());
}

The next part of the code which respects scoping, and re-evaluates the spring constants if C is used, also looks similar.

// Create or grab the realtime data block associated with this track.
// This will keep our results from the previous cook, in this case,
// the previous 2 displacements.
block = (ut_SpringData *) getDataBlock(i);
d1 = block->myDn1;
d2 = block->myDn2;

Where the initial conditions come from is quite a bit different, however. The CHOP_Realtime::getDataBlock() method is used grab the realtime data block for this track (possibly creating one if it doesn't exist). The initial conditions are then taken from there.

// Loop over each sample in the track.
for(j=0; j<myClip->getTrackLength(); j++)
{
t = myClip->getTime(myClip->getStart() + j);
oldp = f = clip->evaluateSingleTime(track, t);
// Run the spring equation
if(!force_method)
f *= spring_constant;
else
oldp /=spring_constant;
vel = (d1-d2) / inc;
acc = (f - vel*damping_constant - d1*spring_constant)/mass;
vel += acc * inc;
d = d1 + vel * inc;
delta = SYSabs(oldp - d);
if (delta > 0.001)
mySteady = 0;
new_track->getData()[j] = d;
d2 = d1;
d1 = d;
}

Again, the spring equation is run on the input samples and written to the output track.

// update the displacements in the realtime data block for the next cook
// to use.
block->myDn1 = d1;
block->myDn2 = d2;
}
return error();
}

When all the samples have been processed for the track, the data needed to smoothly continue the algorithm are written to the realtime data block. This ensures that the next iteration of the realtime cook gets the proper initial conditions.

CHOP_Spring::newRealtimeDataBlock(const char *name,
const CL_Track *track,
fpreal t)
{
fpreal d, d1, v, rate;
// This creates a new realtime data block to stash intermediate values into.
// In this case, the previous two displacements.
if(GRAB_INITIAL() && track)
{
const CL_Clip *clip = track ? track->getClip() : 0;
d = clip->evaluateSingle(track,clip->getIndex(t));
v = clip->evaluateSingle(track,clip->getIndex(t)+1) - d;
}
else
{
d = INITIAL_DISPLACEMENT(t);
v = INITIAL_VELOCITY(t);
}
// The n-1 displacement is extrapolated from the slope at n and the
// displacement (position) at n.
rate = myClip->getSampleRate();
if(rate != 0.0)
d1 = d - v/rate;
else
d1 = d;
return new ut_SpringData(name, d,d1);
}

This method must be overridden from CHOP_Realtime in order to create a new realtime data block for getDataBlock() in cookMySlice(). Realtime data blocks are cached between cooks, so it is only called when an existing data block cannot be found for a track.

In this example, the initial conditions for the spring are set using the parameters or computed from the input track, in the same way that they were in cookMyChop(). These conditions are stashed in a new ut_SpringData subclass of a realtime data block.

// install the chop.
{
table->addOperator(new OP_Operator("hdk_spring", // node name
"HDK Spring", // pretty name
CHOP_Spring::myConstructor,
&CHOP_Spring::myTemplatePair,
1, // min inputs
1, // max inputs
&CHOP_Spring::myVariablePair));
}

This registers the CHOP with Houdini as a 1 input filter named "HDK Spring".