Nuke 8.0 introduces support for Planar Iops, in addition to the usual Row-based Iops. These are primarily intended for Iops that find producing their output in single rows to be unnatural, and would prefer instead to output data containing multiple rows at a time (either the entire image, or smaller stripes.)
Planar Iops have two levels of abstraction: there is a core PlanarI interface, which provides a mechanism that PlanarIop then uses. We expect that most users will want to implement PlanarIop.
PlanarIop provides a function
void renderStripe(ImagePlane& imagePlane)
This is the main rendering function. The implementation of this should output a rendered stripe. The image plane has been set up with the required bounding box and channels already; all the implementation needs to do is to fill in the data for this region and channels.
By default renderStripe() will be called for the entire bounding box of the iop, as set by _validate. This is ideal for plugins that take the entire input image as a single input and produce a single output image. Additionally, renderStripe() will be called for each layer individually.
PlanarIops also should not implement _request; instead they should implement getRequests(), which is a similar, but non-recursive, function.
You may want a different pattern of access.
If you want to be called for stripes rather than the full image, you can implement the function
bool useStripes() const
to return true.
You can then implement the function
int stripeHeight() const
to describe the height of the stripes that you wish to be called for. Stripes will start at the origin, be of stripeHeight(); and then clipped to the bounding box size. Stripes are always full-width for purposes for interoperability with the Row-based parts of Nuke.
By default renderStripe will called for the requested channels in each layer individually. It is possible to override this behaviour with the function
PlaneID is a presently a typedef to ChannelSet (in subsequent versions it might end up being a simpler class that has a default-constructor from ChannelSet). The default implementation implements the following mapping
Chan_Red Mask_RGBA Chan_Green Mask_RGBA Chan_Blue Mask_RGBA Chan_Alpha Mask_RGBA Chan_U Mask_MoVec Chan_V Mask_MoVec Chan_Backward_U Mask_MoVec Chan_Backward_V Mask_MoVec
If you wanted the forward and backward vectors to be calculated seperately, you could override the function to provide the following mapping
There is a further virtual function you can implement
The return value of this indicates whether the Iop prefers to produce packed or unpacked output planes. If it returns ePackedPreferenceNone it has no strong preference and renderStripe() can be called with either depending upon which the calling code finds more convenient; if it returns ePackedPreferencePacked ePackedPreferenceUnpacked then renderStripe() will only be called to fill a packed or unpacked stripe.
Some plugins may wish to directly implement the slightly higher-level class PlanarI. This might be because you also need to inherit from some other type of Iop. The functions
virtual PackedPreference packedPreference() const = 0;
and
virtual PlaneID getPlaneFromChannel(Channel chan);
are to be implemented as with PlanarIop. Rather than implementing stripeHeight(), PlanarI allows you to have a stripes of various different heights. Implement
virtual size_t getStripeCount() const = 0; virtual Box getStripeBox(int idx) const = 0; virtual size_t rowToStripeIndex(int y) const = 0;
The PlanarI does not have an actual render call; you must override the function
virtual void doFetchPlane(ImagePlane& imagePlane);
Unlike renderStripe() which is constrained so that it will only ever be called with the imageplane in the preferred format, doFetchPlane() should be able to cope with any combination of bounding boxes; the stripe/planes/packed preferences are hints rather than mandatory.
The ImagePlane class is at the heart of the Planar code. ImagePlanes can be of various formats, with different bounding box, channels and packness. The bbox can be obtained with the accessor function bounds(); the channels with channels() and the packness with packed().
In all modes there is no padding. For example, in packed mode, with a 2x2 image, with RGBA channels, the floats would be
red 0,0 green 0,0 blue 0,0 alpha 0,0 red 0,1 green 0,1 blue 0,1 alpha 0,1 red 1,0 green 1,0 blue 1,0 alpha 1,0 red 1,1 green 1,1 blue 1,1 alpha 1,1
whereas in unpacked mode the data is represented by
red 0,0 red 0,1 red 1,0 red 1,1 green 0,0 green 0,1 green 1,0 green 1,1 blue 0,0 blue 0,1 blue 1,0 blue 1,1 alpha 0,0 alpha 0,1 alpha 1,0 alpha 1,1
ImagePlane provides several accessor functions for its data.
ImagePlane::readable() points at the start of the allocation. For the examples above, this will point at the red at (0, 0).
rowStride(), colStride() and chanStride() can then be used to multiply y, x, and channel values, to add to these. For example, in the first,
colStride() 4 rowStride() 8 (2 * 4) chanStride() 1
and in the second
colStride() 1 rowStride() 2 chanStride() 4
data() points at the bottom-left corner of the image, which is not necessarily at the origin (0, 0).
There is no support for images with origins at the top-left.
You can access individual pixels with
const float& ImagePlane::at(int x, int y, int z)
z is the channel number /within/ the ChannelSet for the ImagePlane, i.e. for a Mask_RGBA image plane then red = 0, green = 1, blue = 2, alpha = 3. If it was just red, green and alpha, then, then this would make red = 0, green = 1, alpha = 2. The function ImagePlane::chanNo() can be used to look this up but this iterates over the ChannelSet and you should avoid calling it within a tight loop.
There is also a
const float& ImagePlane::at(int x, int y, Channel z)
which does the lookup of the Channel for you.
These functions give references to the actual image data. If you want to abstract some of that away, there are functions that return strided pointers.
const ImageTilePtr ImagePlane::readableAt(int y, int z);
This returns a pointer to (0, y, z). The pointer is strided: so it knows what chanStride() is, so that ++ increments it to the next x value.
Note that returns a pointer to (0, y, z) even if the x bounds of the origin start rightwards of the origin. This means that the pointer cannot be safely deferenced without incrementing by (at a minimum) the x bounds. This is there to make multiplying in the offset slightly easier, and is consistent with the way that Row::operator[] works.
ImagePlanes use shallow copying when possible.
If you are writing to an ImagePlane that might be shared with what ought to be read-only copies then you should call
void ImagePlane::makeUnique()
If you are writing to a new ImagePlane that you made, you should call
void ImagePlane::makeWritable()
One pattern that is valid, for example, is
void renderStripe(ImagePlane& outPlane) { input0().fetchPlane(outPlane); outPlane.makeUnique() Box box = outPlane.bounds(); foreach(z, outPlane.channels()) { for (Box::iterator it = box.begin(); it != box.end(); it++) { outPlane.writableAt(outPlane.chanNo(z), it.x, it.y) *= 2; } } }
If there is another copy that that shares backend data then it will copy the data and then make the modifications in-place. If there is no extra copy that is needed, because it has decided that the input does not need caching (perhaps it is really cheap to calculate), then it will not need to even do a copy, thus reducing peak memory use.
If you want to build an output from scratch, you can do something like:
outPlane.makeWritable() foreach(z, outPlane.channels()) { for (Box::iterator it = box.begin(); it != box.end(); it++) { outPlane.writableAt(outPlane.chanNo(z), it.x, it.y) = 0.5; } }