All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Creating a custom generator COP


A Generator COP is one of the easiest COPs to create because you don't need to worry about fetching image data from inputs. This makes creating the COP a bit less complex than other nodes, since all that needs to be done is to cache any parameters and constant data for the cook, and then cook each tile.

COP Generator walkthrough

This example of a COP generator creates scalable random white noise.

The generator uses the Image and Sequence tabs to control the basic sequence parameters, such as resolution and frame range. It is also maskable, with controls for this in the Mask tab. These features are provided by the parent class COP2_Generator and its parent, COP2_MaskOp.

#include <UT/UT_Math.h>
#include <UT/UT_SysClone.h>
#include <PRM/PRM_Parm.h>
#include <TIL/TIL_Plane.h>
#include <TIL/TIL_Tile.h>

TIL_Plane and TIL_Tile are needed for most COPs. UT_SysClone and UT_Math are used for this specific example, and the rest is used for general OP cooking (evaluating parms, installing the custom OP).

COP_GENERATOR_SWITCHER(2, "HDK Sample Generator");
static PRM_Name names[] =
PRM_Name("seed", "Seed"),
PRM_Name("ampl", "Amplitude"),
COP2_SampleGenerator::myTemplateList[] =
OP_TemplatePair COP2_SampleGenerator::myTemplatePair(
OP_VariablePair COP2_SampleGenerator::myVariablePair(0, &COP2_Node::myVariablePair );

This next section sets up the parameters and local variables for the node. There are two parameters and no local variables. The parameters reside in a switcher. COP_GENERATOR_SWITCHER is a macro which generates tabs for the Image and Sequence pages.

The OP_TemplatePair joins our short template list to the default Generator's template list, which contains all the normal generator parameters for the Image and Sequence tabs.

COP2_SampleGenerator::myConstructor( OP_Network *net,
const char *name,
return new COP2_SampleGenerator(net, name, op);
COP2_SampleGenerator::COP2_SampleGenerator(OP_Network *parent,
const char *name,
OP_Operator *entry)
: COP2_Generator(parent, name, entry)

These methods are responsible for creating and destroying our COP. Normally, not much needs to be done in either the constructor or destructor. Generators usually derive from COP2_Generator, which provides the Image and Sequence tabs for manipulating all the sequence information. If this information is generated in a different way (file, device, etc), you may want to avoid these tabs and derive directly from COP2_MaskOp instead, which provides the Mask tab and masking capabilities. If this isn't desired, you can simply derive directly from COP2_Node.

COP2_SampleGenerator::cookSequenceInfo(OP_ERROR &error)

This method determines all the sequence information based on the default parameters in the Image and Sequence tabs. If you didn't derive from COP2_Generator, your method may look more like:

COP2_SampleGenerator::cookSequenceInfo(OP_ERROR & /*error*/)
mySequence.setSingleImage(0); // not a still (single) image
mySequence.setStart(1); // 1-300 @30fps
mySequence.setRes(500, 400); // 500x400, 1:1 aspect
// add 2 planes, color (C) and alpha (A).
TIL_Plane *plane;
// FP color plane, vector size 3 with components named r,g,b.
plane = mySequence.addPlane( getColorPlaneName(), TILE_FLOAT32,
// 8 bit alpha plane, scalar.
plane = mySequence.addPlane( getAlphaPlaneName(), TILE_INT8 );
return &mySequence;

This specifies the minimum parameters needed for a sequence - the frame rate, resolution and aspect ratio, and plane composition. This is a sample of how you would do this manually, though generally these wouldn't be constant values, but derived from a file or parameter.

The next method, newContextData(), creates a custom object derived from COP2_ContextData, which was declared in the header as:

class cop2_SampleGeneratorData : public COP2_ContextData
cop2_SampleGeneratorData() : myAmp(0.0f,0.0f,0.0f), mySeed(0) { }
virtual ~cop2_SampleGeneratorData() { ; }
UT_Vector4 myAmp;
int mySeed;

In this instance, it just caches the two parameters so that they are not evaluated in the generateTiles() method, which is called multiple times in different threads. Parameter evaluation is not yet threadsafe. You can also cache precomputed data which would be the same for all tiles (filter kernels, geometry, etc).

COP2_SampleGenerator::newContextData(const TIL_Plane * /*planename*/,
int /*arrayindex*/,
float t, int /*xsize*/, int /*ysize*/,
int /*thread*/, int /*max_num_threads*/)
// create one of my context data objects.
cop2_SampleGeneratorData *data = new cop2_SampleGeneratorData;
// stashing some parm values for the cook in my context data.
AMP(data->myAmp, t);
data->mySeed = SEED(t);
return data;

Next up is generateTile(). The COP engine passes you a list of tiles to cook.

The first part of the generateTile() method grabs the context data object that newContextData() allocated earlier in the cook. This will be used to reference all the parameters evaluated earlier.

COP2_SampleGenerator::generateTile(COP2_Context &context, TIL_TileList *tiles)
// retrieve my context data created for this plane/res/time/thread
// by my newContextData method.
cop2_SampleGeneratorData *data =
static_cast<cop2_SampleGeneratorData *>( context.data() );

Next, the macro FOR_EACH_UNCOOKED_TILE() is used to loop over the tiles in the list. This macro specifically avoids tiles may already be cooked in the tilelist (which is possible as caching is done on a per-tile basis).

This examples allocates a small block of floats and writes the output to that, and then calls the convenience method COP2_Node::writeFPtoTile() to convert and copy the data to the tile, doing any data conversions required.

The data generated is simply some random numbers based on a seed, and scaled by a vector4 amplitude.

int i, ti;
TIL_Tile * itr;
unsigned seed;
const float *amp = data->myAmp.data();
// alloc some temp space for our values. If we know that we're always using
// FP, we could just write the values into the tiles directly using
// dest = (float *) itr->getImageData();
float *dest = new float[tiles->mySize];
FOR_EACH_UNCOOKED_TILE(tiles, itr, ti)
// initial seed value based on lower left corner position & tile index.
seed = ((unsigned) data->mySeed * 4 + ti) *
(context.myXres * context.myYres) +
tiles->myX1 + tiles->myY1 * context.myXres;
// tiles->mySize is the # of pixels in the tile.
for(i=0; i<tiles->mySize; i++)
dest[i] = UTrandom(seed) * amp[ti];
// write the values back to the tile using a convenience method.
// not necessary if we used dest = (float *) itr->getImageData() above.
writeFPtoTile(tiles, dest, ti);
delete dest;
return error();

That's all the code needed to generate image data on a cook. Finally, the COP just needs to register itself in the COP table.

// This operator flags itself as a generator taking zero or one inputs.
// The optional input is the mask.
table->addOperator(new OP_Operator("hdk_samplegen", // node name
"HDK Sample Generator", // pretty name
0, // min inputs
2, // max inputs

This COP has 0-2 inputs, an optional input for masking and an optional input for inline generation. COP2_Generator gives you the option to generate a new plane alongside existing planes in the input, or generate output for a plane and combine it with the input's plane in a simple way (add, multiply, max, etc).

The Mask input is used by COP2_Generator's parent class, COP2_MaskOp. It restricts output to the area defined by the mask.

If you do not derive from COP2_Generator, the input range should be 0-1 (if deriving from COP2_MaskOp) or 0-0 (COP2_Node). If you need to add a required input for your COP, then it may be more appropriate to make the COP a filter.