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.

#include <UT/UT_DSOVersion.h>
#include <OP/OP_OperatorTable.h>

These headers are required for all HDK OPs.

#include <PRM/PRM_Include.h>
#include <CHOP/PRM_ChopShared.h>
#include <CH/CH_LocalVariable.h>
#include <CHOP/CHOP_VariableList.h>

These headers are generally required for all CHOPs.

#include <UT/UT_Interrupt.h>
#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"),
    PRM_Name(0),
};

static PRM_Name springMethodItems[] = {
    PRM_Name("disp",    "Position"),
    PRM_Name("force",   "Force"),
    PRM_Name(0),
};

static PRM_ChoiceList springMethodMenu((PRM_ChoiceListType)
                                       (PRM_CHOICELIST_EXCLUSIVE | 
                                        PRM_CHOICELIST_REPLACE),
                                       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);

PRM_Template
CHOP_Spring::myTemplateList[] =
{
    PRM_Template(PRM_SWITCHER,  2, &PRMswitcherName, switcher),

    // 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),
    PRM_Template(PRM_ORD,       1, &names[3], PRMzeroDefaults,
                                   &springMethodMenu),
    PRM_Template(PRM_TOGGLE,    1, &names[4], PRMoneDefaults),
    PRM_Template(PRM_FLT,       1, &names[5], &initDispDefault, 0,
                                   &initDispRange),
    PRM_Template(PRM_FLT,       1, &names[6], &initVelDefault, 0,
                                   &initVelRange),
    PRM_Template(),
};

OP_TemplatePair CHOP_Spring::myTemplatePair(
    CHOP_Spring::myTemplateList, &CHOP_Node::myTemplatePair);

unsigned
CHOP_Spring::disableParms()
{
    unsigned changes = CHOP_Node::disableParms();
    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
};
CH_LocalVariable
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);

fpreal
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.

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

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

CHOP_Spring::~CHOP_Spring()
{
}

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.

OP_ERROR
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               mass;
    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.
    if (error() >= UT_ERROR_ABORT)
        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
ut_SpringData::loadStates(UT_IStream &is, int version)
{
    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
ut_SpringData::saveStates(UT_OStream &os)
{
    ut_RealtimeData::saveStates(os);

    os.write<fpreal64>(&myDn1);
    os.write<fpreal64>(&myDn2);
    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.

OP_ERROR
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               mass;
    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.

ut_RealtimeData *
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.
void newChopOperator(OP_OperatorTable *table)
{
    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".


Generated on Thu Jan 31 00:29:25 2013 for HDK by  doxygen 1.5.9