In this second part of our intro to the NDK, we will look at the pieces we need to begin writing our first plugins for Nuke and will write three complete simple plugins.

The architecture of an Op

As we have covered quickly in Part 1, when writing plugins for Nuke, we are writing Ops rather than Nodes. Nuke will handle wrapping our Ops in Nodes, and we have a decent amount of control over how it will do this, but we never get direct control over the node object itself.

The NDK exposes a few different subclasses of Ops that you can start from (non-exhaustive list):

- Op: Base Op, you most likely never want to start from here unless you're already an expert.
    - Iop: Base Image Op, can start here but requires more boilerplate code to get going.
        - NoIop: This operator returns the input unchanged. You can use this as a
                 base class for an Iop that does nothing to the image but changes
                 some of the info, such as the format, attributes, or set of
                 channels. 
        - PixelIop: This is a base class for a very common type of operation, where each
                    output pixel depends only on the input pixel at the same location,
                    the x,y position, and on the Iop's controls.
            - DrawIop: This is a base class for operations that draw a black and white
        - FileIop: This class is a base class for Read and Write.
                   image (such as a shape, text, PostScript, etc).
        - PlanarIop: Base class for an Iop which needs to output more than a single line of 
                     pixels at once, for example blurs or convolves.
        
    - GeoOp: Base Geometry Op.
    - DeepOp: Base Op for Deep (not technically a subclass of Op).
    - ParticleOp: Base Particle Op.

Today, we will focus on the Iop class, more particularly the NoIop subclass.

The NoIop subclass can be used by default directly like a NoOp in Nuke, which means we don’t require anything special to setup a working example. By default, it does not modify pixels, but it still allows us to modify the node’s info, such as its format, bbox, metadata and channels. As the beginners that we are, this should be enough to get us started.

Below is the near minimum amount of code needed to create a new NoOp class that can be used in Nuke:

// MyNoOp.cpp
// Minimum boilerplate code required to create a Nuke Op/Plugin

// A string to define your class name.
static const char* const RCLASS = "MyNoOp";

// Includes, in this case we need the NoIop class to inherit from it.
#include "DDImage/NoIop.h"

// Namespace. In this case we don't need it, but it's usually convenient to have.
using namespace DD::Image;

// Our class: MyNoOp, inheriting from NoIop.
class MyNoOp : public NoIop
{
public:
    // Constructor
    MyNoOp(Node* node) : NoIop(node) {}
    // Class member function, must return the class name.
    const char* Class() const override { return RCLASS; }
    // Help string, optional, but good to get into the habit of defining it.
    const char* node_help() const override { return "Do nothing."; }
    // Define the class description object. At this point it's not initialized yet.
    static Iop::Description d;
};

// Create a build function. It is responsible for initializing our Op with a Node object, that Nuke will create.
static Iop* build(Node* node) { return new MyNoOp(node); }
// Initialize our class description. 
// The first argument MUST be the same object as the return value of our Op's Class() function.
// The second argument is a leftover from Nuke4, back before nuke menus were made with TCL, then Python. It was representing where the menu should be created. Nuke still expects a value but doesn't use it.
// The third argument is a pointer to our build function.
Iop::Description MyNoOp::d(RCLASS, "Other/MyNoOp", build);

A new NoOp clone, just like the NoOp node, it does nothing.

Not so scary is it?

While we have only created an Op, and not a Node, by using the Iop::Description we were able to tell Nuke that this Op should be able to be created as a Node. Nuke too care of doing everything else, and when we invoke the node in Nuke we can see that is has all the standard “node knobs”, an input, an output, a shape in the DAG, etc…
From here-on, I might refer to our Op as our Node, even though we’re really creating an Op, because I tend to think of the plugins I make as nodes.

The Knobs function

In order to make our Node do anything useful, we USUALLY want to define some knobs so that the user can interact with it.

The is done through the knobs member function. Almost every single example of NDK plugin you can find online will have this defined, so there are plenty of examples to go by, but let’s look at the basics.

// MyNoOp.cpp
static const char* const RCLASS = "MyNoOp";

#include "DDImage/NoIop.h"
// Add Knobs to our includes
#include "DDImage/Knobs.h"


using namespace DD::Image;

class MyNoOp : public NoIop
{
    // We add a variable (member) of type ChannelSet (accessible in the DD::Image namespace)
    ChannelSet channels;
public:
    MyNoOp(Node* node) : NoIop(node) 
    {
        // In the constructor, we can give default values to our variables
        // Mask_All is a pre-defined value for ChannelSets, there are many others (Mask_None, Mask_RGB, ...)
        // Ctrl+click on it with your IDE would normally take you to where it's defined so you can see all possible values.
        channels = Mask_All;
    }
    // We add a knobs function override. The order doesn't matter, but in the examples from 
    // Foundry they place this under the constructor, so I do th same.
    // This function returns nothing (void), and takes a Knob_Callback as an argument.
    void knobs(Knob_Callback) override;
    const char* Class() const override { return RCLASS; }
    const char* node_help() const override { return "Do nothing, but has a knob."; }
    static Iop::Description d;
};


// Implementation of the knobs function
void MyNoOp::knobs(Knob_Callback f)
{
    // When initializing a knob, the first argument is always the knob callback (here f)
    // The other arguments might vary slightly but usually: 
    // - A reference to a variable which will store the value (here the ChannelSet channels we created line 14)
    // - A name for the knob (here "Channels")
    // - A Label for the knob (skipped here)
    // This specific knob takes an int for the input as the third argument.
    Input_ChannelMask_knob(f, &channels, 0, "channels");
    // It's a good habit to add a tooltip to your knobs, to help guide your users.
    // Notice that we're not telling Nuke on which knob to add the tooltip, unlike in Python, it gets added to the latest knob you made.
    Tooltip(f, "This knob doesn't do anything right now...");
}

static Iop* build(Node* node) { return new MyNoOp(node); }
Iop::Description MyNoOp::d(RCLASS, "Other/MyNoOp", build);

Our Node now has a knob!

If you want an example of adding almost every possible kind of knob, check the KnobParade example from Foundry.

The Validate function

After having created your Node for you, along with its knobs,  Nuke will not do anything else with your Node, until you attempt to view it, or a node below it.

When the time comes to “render” your Op, the first interaction Nuke will have with your OP is calling the validate function.

To paraphrase the documentation, on Iops this consists of setting the info, such as the BBox, the format (both full size and proxy), the channels, the frame-range, whether the node has black_outside, the render direction (top to bottom or bottom to top), and a set of modified channels, which may be used for optimization to skip rendering channels that the Op is not modifying.

Here, for the example, I will modify the list of channels passing through our NoOp, essentially turning it into a “Keep” node:

// MyNoOp.cpp
static const char* const RCLASS = "MyNoOp";

#include "DDImage/NoIop.h"
#include "DDImage/Knobs.h"


using namespace DD::Image;

class MyNoOp : public NoIop
{
    ChannelSet channels;
public:
    // Define _validate override
    void _validate(bool) override;
    MyNoOp(Node* node) : NoIop(node) 
    {
        channels = Mask_All;
    }
    void knobs(Knob_Callback) override;
    const char* Class() const override { return RCLASS; }
    const char* node_help() const override { return "Keeps only the selected channels."; }
    static Iop::Description d;
};

// Implement _validate
void MyNoOp::_validate(bool for_real)
{
    // Copy all the original info from the input. This puts it into the _info variable.
    copy_info();
    // Set the new channels to be the intersection of the original channels and the channels selected via knob.
    info_.channels() &= (channels);
    // I tell Nuke that I have not modified any of the channels
    set_out_channels(Mask_None);
}


void MyNoOp::knobs(Knob_Callback f)
{
    Input_ChannelMask_knob(f, &channels, 0, "channels");
    Tooltip(f, "Controls which channels will be kept.");
}

static Iop* build(Node* node) { return new MyNoOp(node); }
Iop::Description MyNoOp::d(RCLASS, "Other/MyNoOp", build);

This is not a NoOp anymore, it keeps the selected channels only.
Notice the small red dot on the Node, indicating that the channel was not modified, thanks to the call to set_out_channels.

You can check the documentation about IopInfo to see what you can do with the _info variable.

The ChannelSet object

In the example above, I used an object called a ChannelSet. If you are familiar with Python sets, or sets in other programming languages, you may be able to guess what it does based on its name.

A set is a list of objects with no repeated entries. This is a set of Channels. It lets you easily defined a list of Channels, for whichever purpose you wish, here to decide which channels to keep based on a stock Input_ChannelMask_knob.

You can perform boolean operations on ChannelSets, for example the intersection of RG and GB would be G, and the union of RG and GB would be RGB.

The NDK has some pre-defined ChannelSets, prefixed with Mask_, for example Mask_RGB is a ChannelSet that contains RGB by default. Mask_Red would be a ChannelSet containing only Red,  and Mak_None is an empty set.

A few plugins we can make with our current knowledge

Examples from Foundry

The following examples from Foundry mostly use knobs and _validate, and should be good examples to study at this point:

  • KnobParade: A whole bunch of knobs doing nothing
  • DynamicKnobs: How to add knobs dynamically. Introduces knob_changed which we haven’t covered yet, but nothing complicated if you’re familiar with the python knob_changed callback.
  • Assert: Throws an error if a TCL expression returns false
  • Remove: Remove or keep channels, very similar to what we built above but more usable.

Wildcard channel remove

This is one of the first nodes I made as a learning exercise.

Nuke’s remove node has always been limited to removing or keeping channels from 4 layers only, and sometimes we have more to deal with.

This node allows you to define either a simple wildcard expression or a regex to define which channels to keep or remove.

I always found it to be a great example of a node that’s very easy to make in C++ but would be a headache with Python or gizmos.

Here, it is removing rgba.red and rgba.green, which are both matching *re*

Code on Github.

NoBlackOutside

Another simplistic Node that I made. Nuke has a “BlackOutside” node, which adds 1 pixel of black around your image, but it doesn’t have the opposite, to remove it.

Personally, I find that depending on what I am modifying in my script, it works better with or without black outside, for example, a Blur with black outside tends to introduce an undesirable dark edge on a full format image.

I have made a Gizmo version in the past that would crop my image by 1 pixel all around to remove black outside, however it would remove one pixel whether or not the image had black outside. The BlackOutside node in Nuke doesn’t add a second pixel of black if you use it on an image that already has black outside, and I thought that my node shouldn’t remove an extra pixel if it did not have black outside either.

Code on Github.

BBox Merge

Yet another tool which I thought could be useful once in a while.

Nuke has a Merge node, and it has a CopyBBox node, but it does not have a MergeBBox node. I admit this might be a bit niche because I rarely encountered cases when I needed this, but it did happen, particularly when making gizmos and more frequently recently while making tools interacting with external processes, where I wanted to prepare my images to have a certain format without merging them yet, or because the MergeExpression node is lacking these controls.

The CopyBBox Node in action. Impressive!

Code on Github.

Homework

If you would like to keep pushing by yourself, I recommend reading the official documentation, particularly the 2D architecture section.

As a challenge, you could try to see if you figure out how to inject some metadata in the the image, which is doable from a NoIop (hint, MetaData handling is defined at the Op level).

Next Time:

Part 3: Intro to engine and pixel-engine.

In part 3 we will start looking at pixel engines, and do simple math on our images such as simple grades.

Part 4: Custom knobs with custom GL handles and the table knob

In part 4, I’m hoping to introduce some more advanced functions around knobs, like making our own knobs, our own handles in the viewer and taking a look at more complex knobs such as the Table Knob.

Part 5: A complete plugin example: Point-based gradient generator

Using concepts we have learned in parts 1 to 4, up the level a bit and make a full gradient generator, in the style of Nikolai Wüstemann’s Voronoi Gradient or Kevin Fisch’s KF N-Point Gradient

At the speed I’m writing these articles, it might take years before I get to Part 5, but I decided to announce them publicly to see if the added pressure would make me find more time for it.