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

This example is an exact copy of the Blend CHOP found in Houdini. It takes several inputs, a control input and multiple blend inputs. Each track in the control input corresponds to a single blend input. The output is a weighted blend of the tracks in the blend inputs, controlled by the weights in the control clip. The sum of the weights in each control track at a given frame are normalized to one. This method is similar to the one in the Blend SOP.

There is also a differencing mode where the first blend input is assumed to be a rest position, and the control tracks control the weighting of the second and subsequent blend inputs. If these weights sum to less than one, the rest position is weighted so that it completes the blend (weights sum to one). This method is generally used for posing.

Example Walkthrough

These code fragments are taken from CHOP_Blend.C.

These headers are required for any HDK CHOP.

#include <CHOP/CHOP_VariableList.h>

These headers are used by most CHOPs.

#include "CHOP_Blend.h"

These headers are used by this particular CHOP; UT_Interrupt to check for user interruption while cooking.

CHOP_SWITCHER(2,"Blend");
static PRM_Name methodItems[] = {
PRM_Name("prop", "Proportional"),
PRM_Name("diff", "Difference"),
};
methodItems);
static PRM_Name names[] = {
PRM_Name("method", "Method"),
PRM_Name("firstweight", "Omit First Weight Channel"),
};
CHOP_Blend::myTemplateList[] =
{
PRM_Template(PRM_ORD, 1, &names[0], PRMoneDefaults,&methodMenu),
};
OP_TemplatePair CHOP_Blend::myTemplatePair(
CHOP_Blend::myTemplateList, &CHOP_Node::myTemplatePair);
bool
CHOP_Blend::updateParmsFlags()
{
bool changes = CHOP_Node::updateParmsFlags();
// can only omit the first weight when differencing.
changes |= enableParm("firstweight", GETDIFFERENCE());
return changes;
}

This chunk of code defines the parameter dialog for the blend CHOP. It has two parameters, the first a menu for normal/differencing mode, and the second, a toggle to omit the first weight in differencing mode. The toggle is disabled if not in differencing mode.

CHOP_Blend::myVariableList[] = {
{ 0 }
};
OP_VariablePair CHOP_Blend::myVariablePair(
CHOP_Blend::myVariableList, &CHOP_Node::myVariablePair);

The blend CHOP has no local variables.

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

The blend CHOP derives directly from CHOP_Node.

const CL_Clip *
CHOP_Blend::getCacheInputClip(int j)
{
// Convenience method to return the clip cached by the method
// CHOP_Blend::findInputClips();
return (j>=0 && j<myInputClip.entries()) ?
myInputClip(j) : 0;
}

Because the blend CHOP has two distinct types of input clips, a control clip and blend clips, this private method makes it a little easier to access the blend clips.

CHOP_Blend::cookMyChop(OP_Context &context)
{
const CL_Clip *blendclip;
const CL_Clip *clip;
const CL_Track *track, *blend;
int num_motion_tracks;
int num_clips;
fpreal weight, *data, *total;
int i,j,k,samples;
int difference;
int fweight;
short int percent = -1;
int stopped = 0;
fpreal adjust[2] = {1.0, -1.0};
const fpreal *src, *w;
difference = GETDIFFERENCE();
if(difference)
fweight = FIRST_WEIGHT();
else
fweight = 0;
// This copies the attributes of the clip without copying the tracks
// (length, start, sample rate, rotation order).
blendclip = copyInputAttributes(context);
if(!blendclip)
return error();
// Determine which clips are being blended together (the first is a control
// clip containing weights)
num_clips = findInputClips(context, blendclip);
// Determine the tracks that are being blended together.
num_motion_tracks = findFirstAvailableTracks(context);
samples = myClip->getTrackLength();
// Create a working array for blending containing the sum of all the blend
// weights for a given sample.
myTotalArray.resize(samples);
myTotalArray.entries(samples);
total = myTotalArray.array();

The first part of the cook method sets up the output clip, gathers the blend clips and control tracks, and resets the total weight array to zero.

if(boss->opStart("Blending Channels"))
{
for(i=0; i<num_motion_tracks; i++)
{
// layer the motion tracks, by looping through the clips for
// each track
// Zero out the track's data and grab a pointer to it for processing
myClip->getTrack(i)->constant(0);
data = myClip->getTrack(i)->getData();
// Start off with a constant track.
myTotalArray.constant(difference ? 1.0 : 0.0);
// When differencing, subtract the contributions of the weights from
// the initial weight of '1' for the first blend input.
// Otherwise, add all weights together.
for(j=difference?1:0; j<num_clips; j++)
{
clip = getCacheInputClip(j+1);
if(!clip)
continue;
// Grab the control track for the blend track we're
// processing. It contains an array of weights.
if(difference && fweight)
blend = blendclip->getTrack(j-1);
else
blend = blendclip->getTrack(j);
track = clip->getTrack(i);
if(!track || !blend)
continue;
// go through all samples and blend into the result.
w = blend->getData();
// Optimization for input clips that share the same frame range
// as our output clip -- access the data directly.
if (myClip->isSameRange(*clip))
{
src = track->getData();
for(k=0; k<samples; k++)
{
if (w[k])
{
// Add the weighted sample into the output data.
data[k] += w[k]*src[k];
// add or subtract the weight from the total.
// If differencing is used, adjust[] = -1, else 1.
total[k] += w[k]*adjust[difference];
}
}
}
else
{
// Slightly slower; evaluate at each sample, but otherwise
// the same as the first case.
for(k=0; k<samples; k++)
{
time = myClip->getTime(k + myClip->getStart());
if (w[k])
{
data[k] += w[k] *
clip->evaluateSingleTime(track, time);
total[k] += w[k]*adjust[difference];
}
}
}
// Check for user interrupts.
if(boss->opInterrupt())
{
stopped = 1;
break;
}
}

For each track in the blend inputs, the input tracks are weighted by the corresponding control track and summed into the output track. When differencing, the first blend input is ignored until all the other weights can be determined.

// Now normalize the results to 1.
if(!stopped)
{
if(!difference)
{
// when not difference, just divide the samples by the
// total weight value for each sample.
j = myAvailableTracks(i);
if ( j!=-1 && getCacheInputClip(j))
{
track = getCacheInputClip(j)->getTrack(i);
blend = blendclip->getTrack(j);
if(blend)
w = blend->getData();
else
continue;
}
else
track = 0;
for(k=0; k<samples; k++)
{
if(!UTequalZero(total[k], 0.001))
{
// normalize so all weights add to 1
data[k] /= total[k];
}
else
{
// hm... weights all zero. use 1st available input.
if(track && blend)
{
time = myClip->getTime(k + myClip->getStart());
val = clip->evaluateSingleTime(track,time);
data[k] -= w[k]*val;
total[k]-= w[k];
weight = 1.0 - total[k];
total[k]+= weight;
data[k] += weight*val;
data[k] /= total[k];
}
}
}
}

When in normal blending mode, normalize the output samples as if the summed weights added up to 1. In the case where the total weight is zero, the first blend input with that track is used to resolve the sample.

else
{
// when differencing, add in the tracks from the first blend
// input using the remaining weight value (the second and
// subsequent blend clips subtract their weights from 1).
j = myAvailableTracks(i);
if (j != -1)
{
clip = getCacheInputClip(j);
track = clip ? clip->getTrack(i) : 0;
if (track)
{
if (myClip->isSameRange(*clip))
{
const fpreal *src = track->getData();
for(k=0; k<samples; k++)
data[k] += src[k] * total[k];
}
else
{
for(k=0; k<samples; k++)
{
time= myClip->getTime(k+myClip->getStart());
val = clip->evaluateSingleTime(track, time);
data[k] += val * total[k];
}
}
}
}
if(boss->opInterrupt(percent))
{
stopped = 1;
break;
}
}
}
if(stopped)
break;
}
}
boss->opEnd();
return error();
}

When in differencing mode, the normalization pass is slightly more complicated. The first blend input's weight is determined by the total weight; whatever weight is required to make the weights sum to 1 is what the first input is weighted with. In this case, the total was subtracted away from 1.

After the normalization pass, processing continues with the next set of blend tracks, until all have been processed.

int
CHOP_Blend::findInputClips(OP_Context &context, const CL_Clip *blendclip)
{
int i, num_clips;
// The number of clips we use is the number of tracks in the control input.
num_clips = blendclip->getNumTracks();
// When using differencing, you can omit the first channel's weight
// assumed to be one always) and blend in other clips.
if(GETDIFFERENCE() && FIRST_WEIGHT())
num_clips ++;
// more weights than clips? Ignore them.
if (num_clips >= nInputs())
num_clips = nInputs()-1;
// marshall the blending clips into a list.
myInputClip.resize(num_clips+1);
myInputClip.entries(num_clips+1);
for (i=0; i<=num_clips; i++)
myInputClip(i) = inputClip(i, context);
return num_clips;
}

This method puts all the blend clips into a private array called myInputClip.

int
CHOP_Blend::findFirstAvailableTracks(OP_Context &context)
{
const CL_Track *track;
const CL_Clip *clip;
int i, j;
int num_motion_tracks;
// This creates a union of all tracks from the blending clips, and creates
// these tracks in the output clip.
num_motion_tracks = 0;
for(i=1; i<nInputs(); i++)
{
clip = inputClip(i, context);
if(!clip)
continue;
if(clip->getNumTracks() > num_motion_tracks)
num_motion_tracks = clip->getNumTracks();
}
// myAvailableTracks contains the index of the first blend input to contain
// each track, normally 1, unless some blend clips have more tracks than
// others.
myAvailableTracks.resize(num_motion_tracks);
myAvailableTracks.entries(num_motion_tracks);
for(i=0; i<num_motion_tracks; i++)
{
track = 0;
j = 1;
while( !track && j<nInputs())
{
clip = inputClip(j, context);
if(!clip)
{
j++;
continue;
}
track = clip->getTrack(i);
if(!track)
j++;
}
myAvailableTracks(i) = track ? j: -1;
}
// Create new output tracks for each input track that will be blended.
// Copy everything except the track's data.
for(i=0; i<num_motion_tracks; i++)
{
j = myAvailableTracks(i);
if (j == -1)
continue;
track = inputClip(j, context)->getTrack(i);
if (track)
myClip->dupTrackInfo(track);
}
return num_motion_tracks;
}

This method creates output tracks from the union of all input tracks, and keeps track of the first blend input corresponding to each output track.

// install the chop.
{
table->addOperator(new OP_Operator("hdk_blend", // node name
"HDK Blend", // pretty name
CHOP_Blend::myConstructor,
&CHOP_Blend::myTemplatePair,
2, // min inputs
9999, // max inputs
&CHOP_Blend::myVariablePair));
}

This registers the HDK CHOP with Houdini. The blend CHOP has a minimum of 2 inputs, and can take an unlimited number of inputs ('9999').