The PixelIop class provides a specialized version of Iop, adding boilerplate code for convenience of use at the expense of limiting potential use cases. PixelIop inherits the majority of its functionality from the base image processing class Iop, so if you haven’t already, please read 2D Architecture before coming back to this section.
PixelIop is intended for image processing operators that are not spatial in any way - that is, each pixel in the output depends upon the pixel at the same coordinates in the input, and no other pixels. Examples of Ops that might be implemented as PixelIops could include Gain or Desaturate.
PixelIops are an excellent way to get started with using the NDK in NUKE, and have a very low memory footprint. If the algorithm you are looking to implement can be reduced to only rely on the input pixel or row, and thus be implemented as a PixelIop, then it should.
PixelIop:
PixelIop varies from Iop by having two new pure virtual calls that an Op built on it must implement. These are:
in_channels is called by the PixelIop’s definition of _request, for each input. It passes the input number as ‘input’ and a reference to a ChannelSet as ‘mask’. This ChannelSet is initialized with the channels that have been requested - in_channels should fill in any other channels that are needed (or remove those that are unnecessary) on the given input.
For example, a simple Desaturate would implement in_channels like so:
virtual void in_channels(int input, ChannelSet& mask) const { if (mask.contains(Mask_Red) || mask.contains(Mask_Green) || mask.contains(Mask_Blue)) mask += Mask_RGB; }
That is, if any of the Red, Green or Blue channels have been requested, it needs to request all of them from its input.
The pixel_engine function is called with the row from the input already fetched with the correct channels from in_channels. Generally, the output row and the input row are actually the same Row, but this should not be relied upon. A simple implementation of pixel_engine that merely multiples all the pixels by 0.5 is as follows:
virtual void pixel_engine(const Row &in, int y, int l, int r, ChannelMask channels, Row& out)
{
foreach(z, channels) {
const float* inptr = in[z];
float* outptr = out.writable(z);
for (int x = l; x < r; x++) {
outptr[x] = inptr[x] * 0.5;
}
}
}
A PixelIop-derived Op can optionally choose to override the default _validate provided by Iop.
The default, Iop-provided _validate merges IopInfo from all inputs into one, and turns on all the channels set in out_channels. By default, out_channels is set to Mask_All, but it can be set as you desire by overriding _validate, calling set_out_channels, and then calling the base class’ validate. For example:
void _validate(bool for_real) {
set_out_channels(Mask_RGB);
PixelIop::_validate(for_real);
}
Another common situation is that the current interface settings mean your Op does not actually do anything to the underlying image data. In this situation, you should instead set the output channels to Mask_None in your _validate(), which allows NUKE to optimize the Op tree.
void _validate(bool for_real) {
if(_doingAnyWork) {
set_out_channels(Mask_RGB);
PixelIop::_validate(for_real);
} else {
set_out_channels(Mask_None);
}
}
More _validate circumstances are described in the 2D Architecture section.
As with all Ops, PixelIops must additionally implement Description, and may optionally implement knobs() to provide user interface elements.
Congratulations for wading through the theory thus far! Now for the start of the fun stuff - writing your first operator. Get yourself setup with a compiler as per Appendix A: Setting up Projects & Compilers and compile up the Basic example. We’ve replicated the Basic code below for convenience.
static const char* const HELP = "Basic: Does nothing but copy the input from input0 to the output";
#include <DDImage/NukeWrapper.h>
#include <DDImage/PixelIop.h>
#include <DDImage/Row.h>
#include <DDImage/Knobs.h>
using namespace DD::Image;
class Basic : public PixelIop {
public:
void in_channels(int input, ChannelSet& mask) const;
Basic(Node *node) : PixelIop(node) {
}
void pixel_engine(const Row& in, int y, int x, int r, ChannelMask, Row& out);
static const Iop::Description d;
const char* Class() const {return d.name;}
const char* node_help() const {return HELP;}
void _validate(bool);
};
void Basic::_validate(bool for_real) {
copy_info();
set_out_channels(Mask_All);
}
void Basic::in_channels(int input, ChannelSet& mask) const {
//mask is unchanged
}
void Basic::pixel_engine(const Row& in, int y, int x, int r, ChannelMask channels, Row& out){
foreach (z, channels) {
const float* inptr = in[z]+x;
const float* END = inptr+(r-x);
float* outptr = out.writable(z)+x;
while (inptr < END) *outptr++ = *inptr++;
}
}
static Iop* build(Node *node) {return new NukeWrapper(new Basic(node));}
const Iop::Description Basic::d("Basic", "Basic", build);
First, some notes about this plug-in from top to bottom:
The actual interesting things in this snippet are the _validate and pixel_engine functions:
Have a good play around with this code and start making some changes. Take what you’ve learned and apply it to understanding the Add.cpp example shipped with the NDK. This is the actual source code for the Add node compiled into NUKE, and is a great starting point for exploring PixelIops. If you run into areas you’re not sure about, jump on to the next section which covers some further fundamentals, with reference to the more complex Grade example.
Now, lets take a look at our first ‘real’ node: namely the Grade operator. The source code from which this node is actually built is shipped with the NDK, so again, compile yourself up a copy referencing Appendix A: Setting up Projects & Compilers. The source code is duplicated below for convenience.
// Grade.C
// Copyright (c) 2009 The Foundry Visionmongers Ltd. All Rights Reserved.
const char* const HELP =
"<p>Applies a linear ramp followed by a gamma function to each color channel.</p>"
"<p> A = multiply * (gain-lift)/(whitepoint-blackpoint)<br>"
" B = offset + lift - A*blackpoint<br>"
" output = pow(A*input + B, 1/gamma)</p>"
"The <i>reverse</i> option is also provided so that you can copy-paste this node to "
"invert the grade. This will do the opposite gamma correction followed by the "
"opposite linear ramp.";
#include "DDImage/PixelIop.h"
#include "DDImage/Row.h"
#include "DDImage/DDMath.h"
#include "DDImage/NukeWrapper.h"
#include <string.h>
using namespace DD::Image;
static const char* const CLASS = "Grade";
class GradeIop : public PixelIop
{
float blackpoint[4];
float whitepoint[4];
float black[4];
float white[4];
float add[4];
float multiply[4];
float gamma[4];
bool reverse;
bool black_clamp;
bool white_clamp;
public:
GradeIop(Node* node) : PixelIop(node)
{
for (int n = 0; n < 4; n++) {
black[n] = blackpoint[n] = add[n] = 0.0f;
white[n] = whitepoint[n] = multiply[n] = 1.0f;
gamma[n] = 1.0f;
}
reverse = false;
black_clamp = true;
white_clamp = false;
}
// indicate that channels only depend on themselves:
void in_channels(int, ChannelSet& channels) const { }
void pixel_engine(const Row &in, int y, int x, int r, ChannelMask, Row &);
virtual void knobs(Knob_Callback);
const char* Class() const { return CLASS; }
const char* node_help() const { return HELP; }
static const Iop::Description d;
void _validate(bool for_real);
};
void GradeIop::_validate(bool for_real)
{
bool change_any = black_clamp | white_clamp;
bool change_zero = false;
for (int z = 0; z < 4; z++) {
float A = whitepoint[z] - blackpoint[z];
A = A ? (white[z] - black[z]) / A : 10000.0f;
A *= multiply[z];
float B = add[z] + black[z] - blackpoint[z] * A;
if (A != 1 || B || gamma[z] != 1.0f) {
change_any = true;
if (B)
change_zero = true;
}
}
set_out_channels(change_any ? Mask_All : Mask_None);
PixelIop::_validate(for_real);
if (change_zero)
info_.black_outside(false);
}
void GradeIop::pixel_engine(const Row& in, int y, int x, int r,
ChannelMask channels, Row& out)
{
foreach (n, channels) {
unsigned z = colourIndex(n);
if (z > 3) {
out.copy(in, n, x, r);
continue;
}
float A = whitepoint[z] - blackpoint[z];
A = A ? (white[z] - black[z]) / A : 10000.0f;
A *= multiply[z];
float B = add[z] + black[z] - blackpoint[z] * A;
if (!B && in.is_zero(n)) {
out.erase(n);
continue;
}
float G = gamma[z];
// patch for linux alphas because the pow function behaves badly
// for very large or very small exponent values.
#ifdef __alpha
if (G < 0.008f)
G = 0.0f;
if (G > 125.0f)
G = 125.0f;
#endif
const float* inptr = in[n] + x;
float* OUTBUF = out.writable(n) + x;
float* END = OUTBUF + (r - x);
if (!reverse) {
// do the linear interpolation:
if (A != 1 || B) {
for (float* outptr = OUTBUF; outptr < END;)
*outptr++ = *inptr++ *A + B;
inptr = OUTBUF;
}
// clamp
if (white_clamp || black_clamp) {
for (float* outptr = OUTBUF; outptr < END;) {
float a = *inptr++;
if (a < 0.0f && black_clamp)
a = 0.0f;
else if (a > 1.0f && white_clamp)
a = 1.0f;
*outptr++ = a;
}
inptr = OUTBUF;
}
// do the gamma:
if (G <= 0) {
for (float* outptr = OUTBUF; outptr < END;) {
float V = *inptr++;
if (V < 1.0f)
V = 0.0f;
else if (V > 1.0f)
V = INFINITY;
*outptr++ = V;
}
}
else if (G != 1.0f) {
G = 1.0f / G;
for (float* outptr = OUTBUF; outptr < END;) {
float V = *inptr++;
if (V <= 0.0f)
; //V = 0.0f;
#ifdef __alpha
else if (V <= 1e-6f && G > 1.0f)
V = 0.0f;
#endif
else if (V < 1)
V = powf(V, G);
else
V = 1.0f + (V - 1.0f) * G;
*outptr++ = V;
}
}
else if (inptr != OUTBUF) {
memcpy(OUTBUF, inptr, (END - OUTBUF) * sizeof(*OUTBUF));
}
}
else {
// Reverse gamma:
if (G <= 0) {
for (float* outptr = OUTBUF; outptr < END;)
*outptr++ = *inptr++ > 0.0f ? 1.0f : 0.0f;
inptr = OUTBUF;
}
else if (G != 1.0f) {
for (float* outptr = OUTBUF; outptr < END;) {
float V = *inptr++;
if (V <= 0.0f)
; //V = 0.0f;
#ifdef __alpha
else if (V <= 1e-6f && G > 1.0f)
V = 0.0f;
#endif
else if (V < 1.0f)
V = powf(V, G);
else
V = 1.0f + (V - 1.0f) * G;
*outptr++ = V;
}
inptr = OUTBUF;
}
// Reverse the linear part:
if (A != 1.0f || B) {
if (A)
A = 1 / A;
else
A = 1.0f;
B = -B * A;
for (float* outptr = OUTBUF; outptr < END;)
*outptr++ = *inptr++ *A + B;
inptr = OUTBUF;
}
// clamp
if (white_clamp || black_clamp) {
for (float* outptr = OUTBUF; outptr < END;) {
float a = *inptr++;
if (a < 0.0f && black_clamp)
a = 0.0f;
else if (a > 1.0f && white_clamp)
a = 1.0f;
*outptr++ = a;
}
inptr = OUTBUF;
}
else if (inptr != OUTBUF) {
memcpy(OUTBUF, inptr, (END - OUTBUF) * sizeof(*OUTBUF));
}
}
}
}
#include "DDImage/Knobs.h"
void GradeIop::knobs(Knob_Callback f)
{
AColor_knob(f, blackpoint, IRange(-1, 1), "blackpoint");
Tooltip(f, "This color is turned into black");
AColor_knob(f, whitepoint, IRange(0, 4), "whitepoint");
Tooltip(f, "This color is turned into white");
AColor_knob(f, black, IRange(-1, 1), "black", "lift");
Tooltip(f, "Black is turned into this color");
AColor_knob(f, white, IRange(0, 4), "white", "gain");
Tooltip(f, "White is turned into this color");
AColor_knob(f, multiply, IRange(0, 4), "multiply");
Tooltip(f, "Constant to multiply result by");
AColor_knob(f, add, IRange(-1, 1), "add", "offset");
Tooltip(f, "Constant to add to result (raises both black & white, unlike lift)");
AColor_knob(f, gamma, IRange(.2, 5), "gamma");
Tooltip(f, "Gamma correction applied to final result");
Newline(f, " ");
Bool_knob(f, &reverse, "reverse");
Tooltip(f, "Invert the math to undo the correction");
Bool_knob(f, &black_clamp, "black_clamp", "black clamp");
Tooltip(f, "Output that is less than zero is changed to zero");
Bool_knob(f, &white_clamp, "white_clamp", "white clamp");
Tooltip(f, "Output that is greater than 1 is changed to 1");
}
static Iop* build(Node* node)
{
return (new NukeWrapper(new GradeIop(node)))->channelsRGBoptionalAlpha();
}
const Iop::Description GradeIop::d(CLASS, "Color/Correct/Grade", build);
Once again, notes from top to bottom. Standard HELP definition, set of includes and a namespace declaration, followed by a static declaration of the CLASS name, used in a variety of places throughout the remainder of the Op. This can be useful when developing, as it allows the class name to be easily altered during the development cycle - see the Versioning discussion for areas where this may come in handy.
We then declare our subclass of PixelIop, with member variables used to store values in use by the interface knobs. Often, depending on relevant coding standards, you’ll see such member variables prefixed with an underscore. We initialize these to their default values in the class constructor, and set up in_channels to define no extra dependency for an output channel than the corresponding input channel.
Next up, we have our _validate implementation. Rather more interesting than the basic example, it figures out if the current interface settings are going to make any difference to the image data on any of the four potential channels, if not calling set_out_channels with a Mask_None, otherwise with Mask_All. This has an identical effect to using disable() in that NUKE optimizes this operator out of the tree. It then calls the default PixelIop implementation, which merges all the input’s IopInfo structures into the output stream, in this case just copying through the single input’s structure. Subsequently, the op sets its IopInfo.black_outside flag dependent on whether the Op itself is altering the 0 level in any of the image channels affected. This has the effect of switching off repetition of the bounding box edge pixel out to the format size, in the circumstance the bounding box is smaller than the format and the tree has not had a black pixel padding inserted into the stream. This means that if you alter a comped element’s black point, it won’t edge repeat over a larger background element. See the IopInfo detail in the _validate section for more information on this mechanism.
The majority of the body of the operator is taken up with the pixel_engine, which is responsible for the color correction operation being applied to the pixels. We won’t take you through line by line; instead, we’ll introduce the fundamental functions utilized within. Breaking down the actual color correction operation itself is an excellent first exercise for learning to use a debugger, so if you’re a little rusty there, do take this as a good opportunity to do so. From this point forward, we will assume that you are able to hook up a debugger and inspect variables, insert breakpoints, and so on.
As in the Basic example, foreach is used to iterate across all the passed-in channels, however, in this circumstance we then test for the current channel’s index (an associated integer identifier). If the index is above 3 (that is, not Chan_Red, Chan_Green, or Chan_Blue), then we copy the data through untouched using the row.copy method and move on to the next channel in the set. row.copy is preferred in this circumstance over setting up a pointer on the source row and a writeable pointer on the target row, as in some circumstances it can simply switch the pointer over, avoiding the overhead of the data copy.
If the incoming channel is in the color channel range, then the engine sets up to do the calculation necessary to build the output row. It uses a shortcut for the circumstance where the input row is all zero and the current settings don’t involve changing the 0 value. This helps speed up processing on the common use case of 3D elements with poor bounding box settings and large regions of black. To do this, it uses row.is_zero(Channel) to, as you would imagine, identify if the row is all zero for the selected channel, and row.erase(Channel) to zero the selected channel.
As before, there’s the definition of the source and target pointers and the decision tree associated with the various Grade node settings. This is where the image processing actually happens - if you’re interested in the mechanics, feel free to hook up that debugger. An interesting point to note is the use of memcpy**s from input to output buffer. Note that if you call **row.copy instead, having already written other parts of your row, you may end up blitzing already calculated data.
After the engine functionality, we have the knobs(Knob_Callback) call. As mentioned previously, this is where your user interface is defined. In this circumstance, we have a number of AColor_knob entries, which appear as a slider that can be split four ways plus defined by a color swatch alongside the standard NUKE animation controls and a couple of Bool_knob entries that appear as checkboxes. As with the Basic example, you can also see in our build(Node*) method we use the NukeWrapper class to add channel, mix, and mask knobs. We also call the NukeWrapper.channelsRGBOptionalAlpha() method to preset our channel knob to be RGB by default.
Each knob call specifies:
There are a vast array of options for knob types, interface layout, and so forth, as covered in the Knob Types and Knob Flags sections. Note that for the variable passed to the Bool_knob entries we use the address of a bool variable, whilst for the AColor_knob entries we use an array pointer. The array must have the correct number of entries in the array of the correct type.
As a final point of interest, note how the Description method here is passed a hierarchy for the plug-in name. This is an old technique for laying out menus in NUKE. It is less flexible than the newer menu.py python approach, but can be used to insert Ops into pre-existing menu structures. It’s not recommended for third-party development.
Now, let’s take what we’ve covered so far and apply it. For this first exercise, take the Add.cpp source code in the NDK, get it building, and then: