This page by The Foundry is an amazing place to start learning C++ in combination with the Nuke NDK,

because there are actual example C++ files for existing Nuke nodes (like Constant, SimpleBlur, Deepcrop) available for download.

The Nuke NDK Developer Guide is also a great place to read about conventions and terminology.

Let’s look at the code in my example project ShiftRGB. This plugin has three input integer boxes, one for each channel R, G, and B.

It horizontally translates the channels according to that value, and that’s all.

First, we have to import the header files and namespace that will allow C++ to use the coding functions Nuke provides.

#include "DDImage/Row.h" #include "DDImage/Knobs.h" #include "DDImage/NukeWrapper.h" using namespace DD::Image; 0 1 2 3 4 #include "DDImage/Row.h" #include "DDImage/Knobs.h" #include "DDImage/NukeWrapper.h" using namespace DD : : Image ;

Then, let’s name the plugin and give it a Help-function for Nuke to understand.

static const char* const CLASS = "ShiftRGB"; // Name of the class (should be the same as the name of the final .dll file) static const char* const HELP = "Example script - Horizontally shift RGB (individually)"; // The tooltip you see when hovering above the Help-button in Nuke 0 1 static const char * const CLASS = "ShiftRGB" ; // Name of the class (should be the same as the name of the final .dll file) static const char * const HELP = "Example script - Horizontally shift RGB (individually)" ; // The tooltip you see when hovering above the Help-button in Nuke

Be sure to name the final compiled file the same as the class (in our case “ShiftRGB”), as Nuke will be looking for that filename.

In the main class we define the variables that can be influenced by user knobs in Nuke.

In this case, we’ll be adding a knob that has 3 integer values (the offset for each channel).

Nuke also wants the plugin to have a _validate, _request, and an _engine. Validate and Request are called once,

Engine is continuously called (multi-threaded).

We also create a function for the knobs to be loaded in the UI.

class ShiftRGB : public Iop { int value[3]; // Create user variable for per-channel translation public: ShiftRGB(Node* node) : Iop(node) { // Set all default values here value[0] = value[1] = value[2] = 0; // For instance, all items in the int value[3] should be 0 by default } void _validate(bool); // This will define the output image, like the size and channels it will have void _request(int x, int y, int r, int t, ChannelMask channels, int count); // This requests information from the input void engine(int y, int x, int r, ChannelMask, Row & row); // Where the calculations take place const char* Class() const { return d.name; } const char* node_help() const { return HELP; } virtual void knobs(Knob_Callback); // This is where knobs can be assigned static const Iop::Description d; // Make Nuke understand this node (at the bottom of the script more information on this) }; 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class ShiftRGB : public Iop { int value [ 3 ] ; // Create user variable for per-channel translation public : ShiftRGB ( Node * node ) : Iop ( node ) { // Set all default values here value [ 0 ] = value [ 1 ] = value [ 2 ] = 0 ; // For instance, all items in the int value[3] should be 0 by default } void _validate ( bool ) ; // This will define the output image, like the size and channels it will have void _request ( int x , int y , int r , int t , ChannelMask channels , int count ) ; // This requests information from the input void engine ( int y , int x , int r , ChannelMask , Row & row); // Where the calculations take place const char * Class ( ) const { return d . name ; } const char * node_help ( ) const { return HELP ; } virtual void knobs ( Knob_Callback ) ; // This is where knobs can be assigned static const Iop : : Description d ; // Make Nuke understand this node (at the bottom of the script more information on this) } ;

In the Validate and Request, let’s fetch all image data from the first input (input0).

void ShiftRGB::_validate(bool for_real) { copy_info(); // Output image will have the same properties as the input } void ShiftRGB::_request(int x, int y, int r, int t, ChannelMask channels, int count) { input0().request(x, y, r, t, channels, count); // Request the image data from input 0 } 0 1 2 3 4 5 6 void ShiftRGB : : _validate ( bool for_real ) { copy_info ( ) ; // Output image will have the same properties as the input } void ShiftRGB : : _request ( int x , int y , int r , int t , ChannelMask channels , int count ) { input0 ( ) . request ( x , y , r , t , channels , count ) ; // Request the image data from input 0 }

Now comes the hardest part, which is the Engine. This will be called for each row (a horizontal line pixels), for each channel.

Keep this as lightweight as possible, as this can easily crash your Nuke.

In the Engine, we’re going to iterate through each channel (z), and check the knob value for that channel (value[z]).

Then, for each pixel in each row in each channel, check if that pixel index + the knob value is still a valid pixel index.

If it is, show the input on that new pixel index at the current pixel. Otherwise, output black (a value of zero).

This is what that would look like in C++:

void ShiftRGB::engine(int y, int x, int r, ChannelMask channels, Row& row) { row.get(input0(), y, x, r, channels); // Fetch the data row foreach(z, channels) { // Do for each channel (0, 1, 2 = R, G, B) int rowlength = info().r(); // Get width of screen const int PixOffset = value[colourIndex(z)]; // Get current channel offset value for values 0, 1, 2. If higher than 2, pick value[0] const float* INP = row[z]; // Copy the pointer float* OUTP = row.writable(z); // Allocate the output pointer for (int X = x; X < r; X++) { // For each horizontal pixel within the row int NewPixel = int(X + PixOffset); // NewPixel = X + PixOffset if (!(NewPixel > 0 && NewPixel < rowlength)) { // Check the pixel index for illegal values NewPixel = -1; // When NewPixel does not exist within array INP, assign error value } float NewColor = (NewPixel == -1) ? 0 : INP[NewPixel]; // NewColor = INP value at pixel NewPixel, except if the value is -1 OUTP[X] = NewColor; // Set OUTP at X to NewColour } } } 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void ShiftRGB : : engine ( int y , int x , int r , ChannelMask channels , Row & row) { row.get(input0(), y, x, r, channels); // Fetch the data row foreach ( z , channels ) { // Do for each channel (0, 1, 2 = R, G, B) int rowlength = info ( ) . r ( ) ; // Get width of screen const int PixOffset = value [ colourIndex ( z ) ] ; // Get current channel offset value for values 0, 1, 2. If higher than 2, pick value[0] const float * INP = row [ z ] ; // Copy the pointer float * OUTP = row . writable ( z ) ; // Allocate the output pointer for ( int X = x ; X < r ; X ++ ) { // For each horizontal pixel within the row int NewPixel = int ( X + PixOffset ) ; // NewPixel = X + PixOffset if ( ! ( NewPixel > 0 && NewPixel < rowlength)) { // Check the pixel index for illegal values NewPixel = -1; // When NewPixel does not exist within array INP, assign error value } float NewColor = ( NewPixel == - 1 ) ? 0 : INP [ NewPixel ] ; // NewColor = INP value at pixel NewPixel, except if the value is -1 OUTP [ X ] = NewColor ; // Set OUTP at X to NewColour } } }

Now we only have to create the user knob for the node, in the Knobs void.

Nuke also needs some information about this node, like what its UI-name is (when searching nodes).

This should be the same as the name of the final compiled file!