HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Multi-input Wipe Example

This example implements a transition between two overlapping sequences. It uses an overexposure wipe, with a bloom on the images. Two faders are used to control the transition.

Multi-input Wipe Example

These headers are required for all HDK-based OPs.

#include <SYS/SYS_Floor.h>
#include <SYS/SYS_Math.h>

These headers are used for SYSceil(), and SYSabs() and SYSsqrt().

#include <TIL/TIL_Plane.h>
#include <TIL/TIL_Region.h>
#include <TIL/TIL_Tile.h>

Since this COP works with regions, we need TIL_Region. As it requests floating point regions, it requires TIL_Plane to create a duplicate plane of the cooked plane with a FP32 data format type, to be passed to COP2_Node::inputRegion().

Finally, TIL_Tile is needed for most COPs and COP2_CookAreaInfo is needed whenever COP2_Node::getInputDependenciesForOutputArea() is overridden.

COP2_MULTI_SWITCHER(5, "Blend");
static PRM_Name names[] =
{
PRM_Name("fadera", "A Fader"),
PRM_Name("faderb", "B Fader"),
PRM_Name("boostval", "Overexposure Boost"),
PRM_Name("bloomblur", "Bloom Blur"),
PRM_Name("fademode", "Fade Rate"),
};
#define FADE_LINEAR 0
#define FADE_SQUARE 1
#define FADE_ROOT 2
static PRM_Name fadeItems[] =
{
PRM_Name("linear", "Linear"),
PRM_Name("squared", "Squared"),
PRM_Name("root", "Square Root"),
};
static PRM_ChoiceList fadeMenu(PRM_CHOICELIST_SINGLE, fadeItems);
static PRM_Range wipeRange(PRM_RANGE_RESTRICTED, 0.0f,
static PRM_Range boostRange(PRM_RANGE_UI, 0.0f, PRM_RANGE_UI, 1.0f);
static PRM_Range blurRange(PRM_RANGE_RESTRICTED, 0.0f, PRM_RANGE_UI, 20.0f);
static PRM_Default boostValue(0.9);
static PRM_Default boostBlur(9);
COP2_MultiInputWipe::myTemplateList[] =
{
// comp page.
&wipeRange),
&wipeRange),
PRM_Template(PRM_FLT, TOOL_PARM, 1, &names[2], &boostValue, 0,
&boostRange),
PRM_Template(PRM_FLT, TOOL_PARM, 1, &names[3], &boostBlur, 0,
&blurRange),
&fadeMenu),
};
OP_TemplatePair COP2_MultiInputWipe::myTemplatePair(
COP2_MultiInputWipe::myTemplateList,

This code defines the parameter dialog for the COP. It has two faders, which are sliders, a boost and a blur parameter, and a menu to select the type of transition rate.

OP_VariablePair COP2_MultiInputWipe::myVariablePair( 0,
const char * COP2_MultiInputWipe::myInputLabels[] =
{
"Wipe A",
"Wipe B",
0
};

This node does not have any local variables. The inputs are labeled so they correspond to the fader sliders' names.

COP2_MultiInputWipe::myConstructor( OP_Network *net,
const char *name,
{
return new COP2_MultiInputWipe(net, name, op);
}
COP2_MultiInputWipe::COP2_MultiInputWipe(OP_Network *parent,
const char *name,
OP_Operator *entry)
: COP2_MultiBase(parent, name, entry)
{
}
{
}

This node derives from COP2_MultiBase, which provides a 'Merge' page full of parameters that deal with combining two sequences into one. This resolves issues that arise when the two sequences do not exactly match, such as having different planes, frame ranges, resolutions or frame rates.

class cop2_MultiInputWipeData : public COP2_ContextData
{
public:
cop2_MultiInputWipeData() {}
virtual ~cop2_MultiInputWipeData() {}
// Stashed parameters and data
float myFaderA;
float myFaderB;
float myBoostA;
float myBoostB;
int myBlurRadA;
int myBlurRadB;
float myBlurA;
float myBlurB;
bool myPassA;
bool myPassB;
}};
COP2_MultiInputWipe::newContextData(const TIL_Plane *, int , float t,
int xres, int , int , int )
{
cop2_MultiInputWipeData *data = new cop2_MultiInputWipeData();
int fademode;
float blur;
float boost;
// Compute the cross fader values and stash them in the context data
data->myFaderA = evalFloat("fadera",0,t);
data->myFaderB = evalFloat("faderb",0,t);
fademode = evalInt("fademode", 0, t);
if(fademode == FADE_SQUARE)
{
data->myFaderA *= data->myFaderA;
data->myFaderB *= data->myFaderB;
}
else if(fademode == FADE_ROOT)
{
data->myFaderA = SYSsqrt(data->myFaderA);
data->myFaderB = SYSsqrt(data->myFaderB);
}
// See if we can pass through one input or the other, without modification.
data->myPassA = (data->myFaderA == 1.0f && data->myFaderB == 0.0f);
data->myPassB = (data->myFaderB == 1.0f && data->myFaderA == 0.0f);
// determine how much of a boost each image will be getting.
boost = evalFloat("boostval", 0, t) * 0.5f;
data->myBoostA = data->myFaderB * boost;
data->myBoostB = data->myFaderA * boost;
// Blur needs to be adjusted if we aren't cooking at full res.
blur = evalFloat("bloomblur", 0, t) * getXScaleFactor(xres);
// Set up how much each input will be blurred
data->myBlurA = data->myFaderB * blur;
data->myBlurB = data->myFaderA * blur;
data->myBlurRadA = (int)SYSceil(data->myBlurA * 0.5f);
data->myBlurRadB = (int)SYSceil(data->myBlurB * 0.5f);
return data;
}

Here a new COP2_ContextData type is defined, which holds the values of our parameters, as well as some pre-computed data. The newContextData() method creates an instance, populates it with values, and returns the new context data instance.

void
COP2_MultiInputWipe::computeImageBounds(COP2_Context &context)
{
cop2_MultiInputWipeData *data =
static_cast<cop2_MultiInputWipeData *>(context.data());
bool init = false;
int x1,y1,x2,y2;
int ix1, ix2, iy1, iy2;
x1 = 0;
y1 = 0;
x2 = context.myXres-1;
y2 = context.myYres-1;
// The image bounds are a union of all input bounds.
for(int i=0; i<nInputs(); i++)
{
if(getInputBounds(i, context, ix1, iy1, ix2, iy2))
{
if(!init)
{
x1 = ix1;
y1 = iy1;
x2 = ix2;
y2 = iy2;
init = true;
}
else
{
if(ix1 < x1) x1 = ix1;
if(ix2 > x2) x2 = ix2;
if(iy1 < y1) y1 = iy1;
if(iy2 > y2) y2 = iy2;
}
}
}
// Finally, we need to expand the bounds by the maximum blur radius.
if(!data->myPassA && !data->myPassB)
{
int brad = SYSmax(data->myBlurRadA, data->myBlurRadB);
x1 -= brad;
y1 -= brad;
x2 += brad;
y2 += brad;
}
context.setImageBounds(x1,y1,x2,y2);
}

This algorithm uses a blur in the transition, so it's useful to expand the canvas size to include the blur. The canvas size also needs to contain all the bounds of the inputs so that the output image isn't cropped improperly.

void
COP2_MultiInputWipe::getInputDependenciesForOutputArea(
COP2_CookAreaInfo &output_area,
const COP2_CookAreaList &input_areas,
COP2_CookAreaList &needed_areas)
{
cop2_MultiInputWipeData *cdata;
COP2_Context *context;
const TIL_Sequence *mask_sequence;
const TIL_Plane *mask_plane = 0;
// If we're bypassed then we don't depend on any of the other inputs.
if (getBypass())
{
area = makeOutputAreaDependOnMyPlane(0, output_area, input_areas,
needed_areas);
return;
}
context = output_area.getNodeContextData();
cdata = static_cast<cop2_MultiInputWipeData *>(context->data());
// Add a dependency on the first wipe input.
if(!data->myPassB)
{
area = makeOutputAreaDependOnMyPlane(0, output_area, input_areas,
needed_areas);
// The area needs to be expanded by the blur radius for input A
if(area)
area->expandNeededArea( cdata->myBlurRadA, cdata->myBlurRadA,
cdata->myBlurRadA, cdata->myBlurRadA);
}
// Add a dependency on the second wipe input, and expand its area as well.
if(!data->myPassA)
{
area = makeOutputAreaDependOnMyPlane(1, output_area, input_areas,
needed_areas);
if(area)
area->expandNeededArea( cdata->myBlurRadB, cdata->myBlurRadB,
cdata->myBlurRadB, cdata->myBlurRadB);
}
}

This method tells the COP cooking scheduler which input subregions are needed to cook the subregion in output_area. In this case, we need the image from both input A and B corresponding to the plane in output_area. Due to the blur, the COP needs neighboring pixels, so the bounds of each input area is expanded to accommodate this (by the blur radius).

int
COP2_MultiInputWipe::passThrough(COP2_Context &context,
const TIL_Plane *plane, int,
int, float t,
int xstart, int ystart)
{
cop2_MultiInputWipeData *data =
static_cast<cop2_MultiInputWipeData *>(context.data());
const TIL_Sequence *inputseq = 0;
// If one of the faders is at 1 and the other at 0, we might be able to
// pass one of the images through as-is.
if(data->myPassA)
inputseq = inputInfo(0);
else if(data->myPassB)
inputseq = inputInfo(1);
if(inputseq)
{
// Now check if it's possible to pass the tile through as-is, based on
// whether the input tile and output tile match exactly.
const TIL_Plane *inputplane = inputseq->getPlane(plane->getName());
if(inputplane)
{
int xres,yres;
int ixres, iyres;
mySequence.getRes(xres,yres);
inputseq->getRes(ixres,iyres);
if(plane->isCompatible(*inputplane) && // planes match
ixres == xres && iyres == yres && // resolutions match
inputseq->getImageIndex(t) != -1 && // within frame range
isTileAlignedWithInput(0,context, xstart,ystart)) // tiles match
{
return 1;
}
}
}
// Otherwise, cook.
return 0;
}
void
COP2_MultiInputWipe::passThroughTiles(COP2_Context &context,
const TIL_Plane *plane, int array_index,
float t, int xstart, int ystart,
TIL_TileList *&tiles,
int block, bool *mask, bool *blocked)
{
// This method is only called if passThrough() returns non-zero.
cop2_MultiInputWipeData *data =
static_cast<cop2_MultiInputWipeData *>(context.data());
bool iblocked = false;
// If we're not crossfading, just return one of the images as-is.
if(data->myPassA)
{
tiles = passInputTile(0,context, plane, array_index, t, xstart,
ystart, block, &iblocked, mask);
}
else if(data->myPassB)
{
tiles = passInputTile(1, context, plane, array_index, t, xstart,
ystart, block, &iblocked, mask);
}
// In the rare case that a tile could not be accessed, check if it was
// blocked and set our blocked flag, if passed in.
if(!tiles && iblocked && blocked)
blocked = true;
}

These methods are an optimization that can pass tiles through the node without modification. In this example, when one fader is at 1 and the other at 0, the result is exactly the same as one of the inputs. As most transitions only occur for a fraction of the two sequences, this is a substantial optimization to the node.

In order to pass a tile through, two methods are overridden: COP2_Node::passThrough() and COP2_Node::passThroughTiles(). The first method returns true (non-zero) if the tiles specified can be passed through as-is. The second method does the actual passing of the tiles, using COP2_Node::passInputTile().

It is important that the output tile match the input tile being passed through, in terms of data format and position in the canvas. The frame and plane must also exist in the input that is being passed through. For most single input filters, this is the case. For multi-input filters, it's quite possible that a mismatch in these tile attributes will occur.

COP2_MultiInputWipe::cookMyTile(COP2_Context &context, TIL_TileList *tilelist)
{
cop2_MultiInputWipeData *data =
static_cast<cop2_MultiInputWipeData *>(context.data());
TIL_Region *aregion, *bregion;
int arad, brad;
TIL_Plane fpplane(*context.myPlane);
bool init = false;
arad = data->myBlurRadA;
brad = data->myBlurRadB;
// Request 32b FP data from the inputs
fpplane.setScoped(1);
fpplane.setFormat(TILE_FLOAT32);
// Grab a region from input 1, run the operation on it, and release it.
if(data->myFaderA != 0.0f)
{
aregion = inputRegion(0, context, &fpplane, 0, context.getTime(),
tilelist->myX1 - arad,
tilelist->myY1 - arad,
tilelist->myX2 + arad,
tilelist->myY2 + arad, TIL_HOLD);
if(aregion)
{
boostAndBlur(tilelist, aregion, data->myFaderA,
data->myBoostA, arad, data->myBlurA, false);
init = true;
releaseRegion(aregion);
}
}
// Repeat for input 2.
if(data->myFaderB != 0.0f)
{
bregion = inputRegion(1, context, &fpplane, 0, context.getTime(),
tilelist->myX1 - brad,
tilelist->myY1 - brad,
tilelist->myX2 + brad,
tilelist->myY2 + brad, TIL_HOLD);
if(bregion)
{
boostAndBlur(tilelist, bregion, data->myFaderB,
data->myBoostB, brad, data->myBlurB, init);
init = true;
releaseRegion(bregion);
}
}
// If neither input was processed (A=0, B=0), clear the tiles to constant
// black.
if(!init)
tilelist->clearToBlack();
return error();
}

The next method, cookMyTile(), actually processes the image data. It uses a FP32 version of the cook plane to access the input regions, which converts the data to FP32 for easier processing. For each input that is required (its fader is not zero), the region is requested, and the private method boostAndBlur() is called. This method applies the bright and bloom effect to the input, and either assigns it to the tiles (first pass) or adds it in (based on the init variable). Each region is then released.

If both faders were zero, then the tiles are cleared to black, as no images would be seen.

void
COP2_MultiInputWipe::boostAndBlur(TIL_TileList *tiles, TIL_Region *input,
float fade, float boost, int rad, float blur,
bool add)
{
int ti, x,y, i,j, idx;
int w,h;
int stride;
TIL_Tile *itr;
float *src, *scan;
float vedge;
float sum, hsum;
float *dest = NULL;
bool alloced = false;
// Set up some constants for the blurring
const float iblur = fade / ((1.0f + blur) * (1.0f + blur));
const float edge = 1.0f - (rad - blur * 0.5f);
const float addf = add ? 1.0f : 0.0f;
// set up tile dimensions and the stride of the input region.
w = tiles->myX2 - tiles->myX1 + 1;
h = tiles->myY2 - tiles->myY1 + 1;
stride = w + rad * 2;
// Boost the input region. Unlike input tiles, you can modify data in
// input regions, as long you have not explicitly requested a shared region.
if(boost != 0.0f)
{
for(i=0; i<PLANE_MAX_VECTOR_SIZE; i++)
{
src = (float *) input->getImageData(i);
if(src)
for(y=0; y<(h+rad*2) * stride; y++)
src++ += boost;
}
}
// When assigning values on the first pass, it is not necessary to fetch the
// image data from the tile. We won't be using it and it'll be overwritten.
// So, the dest array must be allocated, as getTileInFP() won't be called
// to allocate it for us.
if(!add)
{
dest = new float[w*h];
alloced = true;
}

The first part of this method sets up some constants for the blur, and makes a pass through the input region data to boost the input values.

// Iterate over each component in the tilelist.
FOR_EACH_UNCOOKED_TILE(tiles, itr, ti)
{
// Grab the image data from the region. It is FP32, as requested in
// cookMyTile().
src = ((float *) input->getImageData(ti)) + rad;
if(add)
{
// Grab each tile in FP format. 'dest' may be allocated; if so, we
// need to free it, but it can be reused by getTileInFP for the next
// component.
if(getTileInFP(tiles, dest, ti))
alloced = true;
}
else
{
// It is being assigned, ignoring the previous tile data, so just
// zero out the array.
memset(dest, 0, sizeof(float)*w*h);
}
// Process each pixel in the tile.
for(idx=0, y=0; y<h; y++)
{
for(x=0; x<w; x++, idx++)
{
sum = 0.0f;
// for each pixel, blur the surroundings
scan = src+x;
for(i=-rad; i<=rad; i++)
{
vedge = (i == -rad || i == rad) ? edge : 1.0f;
hsum = scan[-rad] * edge;
if(rad)
hsum += scan[rad] * edge;
for(j=-rad+1; j<rad; j++)
hsum += scan[j];
sum += hsum * vedge;
scan += stride;
}
// Assign explicitly or add to the previous result, depending
// on the pass.
dest[idx] = dest[idx] * addf + sum * iblur;
}
src += stride;
}
// Write the FP result to the tile.
writeFPtoTile(tiles, dest, ti);
}
// If dest was allocated by getTileInFP() or by this method, free it.
// (If getTileInFP() returned false, it is likely that the tile was
// already a FP32 tile, and freeing dest would crash as this is a direct
// pointer to the tile data.)
if(alloced)
delete [] dest;
}

The second part iterates over each tile, and blurs each pixel using the boosted input. This creates a HDR-bloom effect in bright spots.

The tile data is extracted in FP from the tile using COP2_Node::getTileInFP() on the second pass, where the second input is accumulated with the first. On the first pass, the extra conversion is avoided and the dest array is initialized with zero. For both passes, COP2_Node::writeFPtoTile() is used to commit the data back to the tile.

Note
This blur uses a convolve which is very slow for larger blur sizes. This was done to keep the example simple. Separable filters, resolution reduction or filtering by integration, are all much faster methods of applying a blur.
void
{
table->addOperator(new OP_Operator("hdk_multiwipe",
"HDK Multi Input Wipe",
COP2_MultiInputWipe::myConstructor,
&COP2_MultiInputWipe::myTemplatePair,
2,
2,
&COP2_MultiInputWipe::myVariablePair,
0, // not generator
COP2_MultiInputWipe::myInputLabels));
}

Finally, the custom COP is registered with Houdini. It is a 2 input filter, both inputs required.

Note
This example could be extended to any number of inputs, using a multiparm for the faders, array versions of the stashed data in cop2_MultiInputWipeData and looping over each input in the cookMyTile() method. The number of faders can be managed automatically by overriding OP_Node::inputConnectChanged() (but be sure to call the parent class version as well).