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:
- COP2_Node, COP2_MultiBase, COP2_TimingBase, COP2_PixelBase:
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:
- myX1, myY1, myX2, myY2 - The area of the tile, with 0,0
being the lower left corner of the canvas (which is not the lower left
corner of the frame area, the visible image to the user).
- mySize - The number of pixels in the data, for algorithms
that don't care about the tile's position. This is equal to
(myX2-myX1-1) * (myY2-myY1+1)
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:
- Use the COP2_Node methods getTileInFP() and writeFPtoTile()
methods to manipulate the data in 32bit floating point (FP).
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;
}
- Implement the algorithm as an RU_Algorithm, with C++
templates.
This is a useful method when certain data types can be highly optimized
using template instantiation.
Please see the docs for implementing an RU_Algorithm for more
information on using templated algorithms.
- Use the TIL_Pixel<> template class to convert the
data
to/from FP
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.
- Use the TIL_Fill class to perform a conversion on the
entire data
array.
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.
- Use the inputRegion() method to fetch a FP version of
the data.
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.
- const TIL_Sequence *inputInfo(int input_index)
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.
- TIL_TileList *inputTile(int input_index, COP2_Context
&context, ...)
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.
- TIL_Region *inputRegion(int input_index, COP2_Context
&context, ...)
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.
- TIL_Region *transformRegion(int input_index, COP2_Context
&context, ...)
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.
- bool copyInput(int input_index, COP2_Context &context,
TIL_TileList *tiles, ...)
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:
- plane
- node instance
- operator type
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:
- COP2_THREAD_NO_PREF - no preference
- COP2_THREAD_SINGLE - prefers to be cooked single
threaded
- COP2_THREAD_MULTI - prefers to be cooked multi
threaded
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