Houdini Development Toolkit - Version 9.0

Side Effects Software Inc. 2007

Compositing Operators

Cooking COP Image Data


Defining the Cook Method

Depending on what class in COP2 you derived from, you need to override one of the following methods:
virtual OP_ERROR cookMyTile(COP2_Context &context, TIL_TileList *tiles)
virtual OP_ERROR generateTile(COP2_Context &context, TIL_TileList *tiles)
virtual OP_ERROR doCookMyTile(COP2_Context &context, TIL_TileList *tiles)
You don't need to override the cook method. See section on Pixel Operations.

Getting Data to Cook

There are two main pieces of data you need to retrieve at the beginning of your cook method - the image data arrays in the tiles, and the parameter information stashed in the context data. The context data array is retrieved from the COP2_Context you are passed:
COP2_MyContextData *cdata = dynamic_cast<COP2_MyContextData *>(context.data());
You are guarenteed to get a valid pointer from context.data(), even if you didn't create your own context data class (it will be a COP2_ContextData, which isn't very useful). The dynamic_cast isn't strictly necessary, as you'll always get the same class you returned from newContextData() back, but it's good programming practice.

Next, you need to get the data out of the tiles - specifically the TIL_TileList you are passed. There are a couple of ways to do this, with macros from TIL_Defines.h. First, you can iterate over each tile in the list:
TIL_Tile *tile;
FOR_EACH_UNCOOKED_TILE(tiles, tile)
{
    void *data = tile->getImageData();
    int current_index = tiles->myTileIndex;
    // do work on tiles[current_index].
}
This will iterate over each tile in the list that is not NULL. It is possible to have 3 tiles in a tile list, with only 2 of them uncooked, and the third could be cached (thus cooked). In this case, it will skip the cooked tile, so you will only get 2 iterations. NULL tiles are always skipped.

Next, you can get all the tiles at once, which is useful for algorithms that work more efficiently on all components.
TIL_Tile *tile1, *tile2, *tile3, *tile4;
void *data1=0, *data2=0, *data3=0, *data4=0;

TILE_VECTOR4(tiles, tile1, tile2, tile3, tile4);

if(tile1) data1 = tile1->getImageData();
if(tile2) data2 = tile2->getImageData();
if(tile3) data3 = tile3->getImageData();
if(tile4) data4 = tile4->getImageData();
Similar to FOR_EACH_UNCOOKED_TILE(), if one of the tiles has been cached, it will show up as NULL. You should never write to cached tiles.

If you also need to access tiles from the input, you can use the macros:
FOR_EACH_UNCOOKED_PAIR(tiles, input_tiles, tile, input_tile)

or

TILE_INPUT4(input_tiles, input_tile1, input_tile2, input_tile3, input_tile4);
The first iterates through the output tilelist, keeping input_tile in synch with the corresponding output tile. The second extracts the tiles into TIL_Tile pointers. In both cases, the cooked state of the tile is ignored, since they are inputs and already cooked. This is an especially important distinction for TILE_VECTOR4() and TILE_INPUT4() - the first should always be used on the output tile list, and the second always on tiles received from inputTile().

Next, you need to determine the area that the data represents. This can be found in the TIL_TileList:

Interpreting and Writing Image Data

The next thing you need to do is determine the image format you are reading from and writing to. Above, we assigned the data to a void pointer. This is because the data can be in a variety of formats - currently 8bit int, 16bit int, 32bit int, 32bit float and 16bit float. The integer formats can also have black and white points other than their min/max values. You can get this data from the TIL_Plane member of the COP2_Context you are passed:
TIL_DataFormat format = context.myPlane->getFormat();
bool useblackwhite = context.myPlane->usesBlackWhitePoints();
unsigned black, white;
context.myPlane->getBlackWhitePoints(black,white);
Since writing an algorithm efficiently for five data types can substantially increase the development time of an operation, there are several ways to abstract the data format and write the algorithm without worrying about it:
This is probably the easiest way, as it does all of the low-level conversions for you. The 'alloced' return value is set depending on whether the data array needed to be allocated. If you pass in a pointer, it will use that for the data and return false (it must be sizeof(float)*tiles->mySize). If the plane is already in floating point, it will just reference the tile data and return false. Either way, if alloced is false, do not delete the data array. Use writeFPtoTile() to write the data back to the tile (if the data was already FP and referenced by getTileInFP(), this does nothing).
float *fpdata = 0;
bool alloced = false;

alloced = getTileInFP(tiles, &fpdata, component_index);

// work with data in fpdata

writeFPtoTile(tiles, fpdata, component_index);
if(alloced)
{
delete [] fpdata;
fpdata = 0;
}
Please see the docs for implementing an RU_Algorithm for more information on using templated algorithms.
This template class is used to convert a single element to FP:
TIL_Pixel<TILE_INT8, 1> pixel;

pixel.set(data[i]);
fpvalue = (float)pixel;
// change fpvalue
pixel = fpvalue;
outdata[i] = pixel.getValue();
Also see the docs for TIL_Pixel.
Converts an array of data to floating point (amoung its many uses):
TIL_FillParms fparms;
float fpdata = new float[tiles->mySize];

fparms.mySource = data;
fparms.mySX2 = tiles->mySize-1;
fparms.mySType = tiles->myPlane.getFormat();
fparms.mySFast = !tiles->myPlane->usesBlackWhitePoints();
tiles->myPlane->getBlackWhitePoints(fparms.mySBlack,fparms.mySWhite);

fparms.myDest = fpdata;
fparms.myDType = TILE_FLOAT32;
fparms.myDX2 = tiles->mySize-1;

TIL_Fill::fill(fparms);
You can do the opposite (switch the S/D prefixes) to convert back to the original format. Also see the docs for TIL_Fill.
This method is useful when you (1) are using inputRegion() anyways because you can't use inputTile() (see Input Functions below), and (2) are using the data as a secondary input (like a mask). You can copy the TIL_Plane from the input plane, and then set its data type to TILE_FLOAT32.

TIL_Plane fpplane(context.myPlane);
fpplane.setFormat(TILE_FLOAT32);

region = inputRegion(0, context, &fplane, context.myArrayIndex, context.myTime, tiles->myX1, tiles->myY1,
tiles->myX2, tiles->myY2);
if(region)
{
fpdata1 = (float *) region->getImageData(0);
fpdata2 = (float *) region->getImageData(1);
fpdata3 = (float *) region->getImageData(2);
fpdata4 = (float *) region->getImageData(3);
}

Input Functions

There are several COP2_Node methods for getting data from the input.
This method returns the sequence information of the input specified. If the input does not exist, it will return NULL. This allows you to access the frame range, planes, and resolution of the input. See also TIL_Sequence.
There are several different signatures of this method. The main signature (the one with the most parameters) allows you to specify all the parameters. The other signatures are abbreviated versions; the omitted parameters are filled in either by the corresponding values in COP2_Context or TIL_TileList.

The inputTile() functions should only be used when you are sure the tiles in the input line up with the output tiles. The tiles will not line up if you change the resolution or the bounds of the canvas. In this case, use inputRegion instead (below).

You can also use the family of methods isTileAlignedWithInput() to determine if the output tiles are aligned with the input or not; however, this is often used as an optimization technique, as inputRegion() is slightly slower than inputTile(). So if there are some cases where your node will have tiles aligned with the input, you can write two codepaths, one for inputTile() and one for inputRegion(), and determine which one to use with isTileAlignedWithInput() (this is only recommended for advanced users, though, as the performance gain is small).

When finished with a TIL_TileList returned by inputTile(), you must call releaseTiles() on the it, otherwise the tiles will remain locked, using up memory, until the cook finishes.
Like inputTile(), there are several different signatures for inputRegion(). A TIL_Region is always copied from the input into temporary data arrays. It is much more flexible than inputTile(), as it can grab any arbitrary area of the input, and convert the data format to any format desired. If you change the canvas size of your image from the input, you should use this method for grabbing the input.

This is a good input method for algorithms that require a larger input area than the output area they produce, neighbour algorithms like blur or expand. You can easily grab a few more edge pixels that the output:
region = inputRegion(0, context, tiles->myX1-5, tiles->myY1-5, tiles->myX2+5, tiles->myY2+5);
If this region includes pixels from outside the canvas area, you can also set the TIL_RegionExtend parameter to TIL_BLACK (for black pixels) or TIL_HOLD (to clamp the edge pixels).

A TIL_Region is more like a TIL_TileList than a TIL_Tile, as it contains all the components' data in one class. You must use releaseRegion() on the region pointer once you are finished with it.
This is a more complicated (and slower) version of inputRegion(), which allows you to perform an arbitrary 2D transform on the input. The transform is specified in COP2_TransformParms. The only values you are required to fill out are the ones in the constructor.

This class will transform the input image and then fill the area you specify with the results. You must call releaseRegion() on the TIL_Region returned by this class.
This is a convenient interface to inputTile(), which copies the input tiles specified to the tiles parameter.
Please note than any of these methods can return NULL, if the input isn't connected, if the operation was interrupted by the user, or if the frame or plane doesn't exist in that input. You should always test the return value before using it.

Selectively Passing Through Planes and Tiles

Finally, instead of cooking the image data on planes, you can pass the input data through the node without copying or operating on it. This is useful when swapping components or frames, as you aren't changing the actual image data, just re-routing it. The methods you will need to override to do this are:
    virtual int		passThrough(COP2_Context &context,
const TIL_Plane *plane, int comp_index,
int array_index, float t,
int xstart, int ystart);

virtual void passThroughTiles(COP2_Context &context,
const TIL_Plane *plane,
int array_index,
float t,
int xstart, int ystart,
TIL_TileList *&tile,
int block = 1,
bool *mask = 0,
bool *blocked = 0);
If you are just passing the data through unchanged in the same component of the same plane, you can instead do this in the cookSequenceInfo() method by setting the scoping on the plane to false (plane.setScoped(false), plane.setPlaneMask(false, component_index)). The default versions of these functions do this for you.

Otherwise, you can selectively determine which planes to pass through. You can even cook some tiles in an image and pass others through (if you know the operation is only affecting a certain area). Basically, the way these pairs of methods work is that the first (passThrough) returns whether the tile should be passed through or not (non-zero, pass through; 0 don't). The second function (passThroughTiles) selects the data to pass through. In this respect, it is like a cook, except that you are setting the TIL_TileList 'tile' parameter to the returned inputTile() list.

Since both of these methods require much the same information to determine which planes to pass, it is a good idea to stash this information in the Context Data class for your node, rather than recompute it.

NOTE: You can use the COP2_Node method passInputTile() instead of inputTile() as a convienience method. It will automatically unscope the input tile list.

If you still want plane scoping to work, you should call the COP2_Node versions if your conditions for passing the plane or tile through are not met.

Cooking the Entire Canvas

There are some algorithms that are difficult to implement working with tiles. There is a method in COP2_Node which you can call from your cook method to cook the entire image plane as one chunk. For certain algorithms, this can be a much simpler approach. However, it has the disadvantage of requiring the entire image canvas to cook. In instances where the canvas is much larger than the frame area, this can result in slow performance. This still only cooks one plane at a time.

The function to call is:
    OP_ERROR		 cookFullImage(COP2_Context	&context,
TIL_TileList *tiles,
COP2_FullImageCB callback,
UT_Lock &fullimagelock,
bool use_float);
You must pass both the context and the tiles parameter that was passed into the cook method. The next parameter is the callback function that will be called to process the data once the full image input has been gathered. The fullimagelock parameter is a lock you must pass to prevent multiple threads from stomping on each other's work. It can either be static (meaning not even two threads working on different planes can be active in that function) or stored in the context data (as long as it isn't created per-thread). Finally, you can pass 'true' for use_float if you want the data to come back in floating point format, regardless of its original data format.

An example of how to use this is found in COP2_FullImageFilter.C in the COP HDK samples.

Controlling Threaded Cooking

Gives a hint of the maximum number of threads that can be cooking a given:
Override this to limit how many threads can work in a given situation. If you don't want to limit the threads for one of the sitatuations, set it to TIL_MAX_THREADS. Normally, the situation is set to 1 or TIL_MAX_THREADS.

For example, I have a COP which spends most of its time reading from a file, and since I/O is generally single threaded, I set the parms to plane=1, node=1, op=1 to try to force the scheduler to cook other nodes.

If I use cookFullImage() to cook a plane entirely (which blocks other threads trying to cook that plane), I would set the parms to plane=1, node & op = TIL_MAX_THREADS.

This is a performance hint only. If the scheduler cannot do other work elsewhere, it will ignore the hint.
Forces this node to only cook in the main thread (thread index 0). This is a good way to make non-threadsafe COPs work safely, since two nodes of the same type will never be able to cook simultaneously if this is set.
Some COPs work better single threaded, since they are so fast and the threading overhead slows them down. Others work better threaded, because the amount of work is high. You can return a threading preference from your node:
Before cooking, a COP is first analyzed by gathering the thread preferences of all the inputs and itself. A COP2_THREAD_MULTI preference is always taken over a COP2_THREAD_SINGLE, and a COP2_THREAD_SINGLE is always taken over COP2_THREAD_NO_PREF. So, if you have a chain with two COPs with 'SINGLE' preference, and three with 'NO_PREF', the chain will be cooked single-threaded. The default for most COPs is MULTI.


Table of Contents
Operators | Surface Operations | Particle Operations | Composite Operators | Channel Operators
Material & Texture | Objects | Command and Expression | Render Output |
Mantra Shaders | Utility Classes | Geometry Library | Image Library | Clip Library
Customizing UI | Questions & Answers

Copyright © 2007 Side Effects Software Inc.
477 Richmond Street West, Toronto, Ontario, Canada M5V 3E7