In this fourth part of our introduction to the Nuke Development Kit (NDK), we’ll dive deeper into advanced knob functionality. We’ll explore how to create custom knobs, implement custom OpenGL handles in the viewer, and work with the powerful table knob. These techniques will allow you to create more sophisticated and user-friendly plugins for Nuke.
Creating Custom Qt Knobs
While Nuke provides a wide range of built-in knobs, sometimes you need something more specific for your plugin. Custom knobs can greatly enhance the user interface and functionality of your Nuke plugins.
Before we dive into C++ implementation, it’s worth noting that custom knobs can also be created using Python. For those interested in the Python approach, you can refer to this article: Custom Qt Knobs for Nuke Nodes – Making Stars Gizmo (Part 1/2). This article demonstrates how to create custom knobs with Qt using Python.
However, creating custom knobs in C++ with the NDK offers several advantages:
- Self-Contained: As we saw in the Python example, shipping a node containing a custom knob is not straightforward. You have to either stash the code in another knob (possibly resulting in large .nk files) or ship the node with a side .py file, complicating installation. In C++, the code is directly compiled into your node.
- Value storage: Unlike in Python, where we often need to use workarounds to store knob values, C++ allows us to store the value directly within the knob. This, again, results in cleaner .nk files.
- Updates: Another advantage is the knob updating properly as we scrub through the timeline. As we saw in the python example, we had to write a custom callback to attempt to keep the knob’s widget visually up to date.
It also has a few disadvantages:
- Complexity: Using custom knobs complicates the implementation of your plugin, and require you to implement things that would normally come “for free” when using standard knobs (for example, tooltips don’t show unless you reimplement them)
- Dodgy python API compatibility: Custom knobs don’t play nice with the python API, unless you also write a full python implementation.
There are 2 parts to making a custom knob, as well as a pre-requisite:
- Install the Qt Binaries
- Make the Qt Widget
- Make the Knob
Install the Qt Binaries and prepare for compiling
Before you can compile a plugin containing Qt code, you need to install the Qt Binaries.
While it’s mentioned in the official documentation, the link doesn’t point you directly at the files to download, so here is the link as of late 2024: Nuke Developers Resources
Note that they don’t show links for many versions in the past, so if you need libraries for older versions of Nuke, you may have to find alternative sources, or contact Foundry hoping they still have a link.
Download and uncompress the files for your OS and Nuke version.
Compiling Qt files is a whole mission unto itself.
Luckily Foundry has an example in their CMakeLists.txt that we can use as a base to setup our own if compiling with CMake.
The relevant lines are below:
# Set the Qt5_DIR path to the correct location. The Docs say QTDIR but that's outdated set(Qt5_DIR "C:/Users/erwan/Documents/NDK/Qt/nuke14.1/lib/cmake/Qt5") # Find and load the Qt5 Core, Gui, and Widgets libraries for this project. # These modules will allow us to use Qt features and components within our plugin. find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) # Enable AUTOMOC, which automatically handles the Qt "moc" (Meta-Object Compiler) step. # AUTOMOC detects any classes using the `Q_OBJECT` macro and generates the necessary files. # This makes it easier to work with Qt classes without manually running moc on each. set(CMAKE_AUTOMOC ON) # Define a custom Nuke plugin target named "MyPlugin" using the specified source file `MyPlugin.cpp`. # `add_nuke_plugin` is a custom function that encapsulates the necessary setup for building a Nuke plugin. add_nuke_plugin(MyPlugin MyPlugin.cpp) # Add `MyPlugin.moc.h` as a source file for the MyPlugin target, ensuring that CMake recognizes it as a dependency. # This header will be compiled by MOC (Meta-Object Compiler) via AUTOMOC. target_sources(MyPlugin PRIVATE MyPlugin.moc.h) # Link the required Qt libraries to MyPlugin, specifically the Core, Gui, and Widgets modules. # `PRIVATE` means these libraries are linked only to MyPlugin and won’t be visible to other targets that depend on it. # Linking these libraries gives the plugin access to Qt's core functionality, GUI framework, and widget elements. target_link_libraries(MyPlugin PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)
If you’re trying to compile another way, however, it can get a bit gnarly, as Qt requires an extra compiler called MOC to pre-process some files. This could necessitate a whole separate article but good news, ChatGPT is giving decent answers if you ask it “I use <compilation software of your choice> to compile C++ and Foundry Nuke (NDK) plugins. Please give me instructions on how to setup MOC so that I can include Qt widgets in my plugins.”. Personally I decided to go with CMake as it was a lot easier to setup.
Preparing to compile with CMake and MOC
Also keep in mind MOC operates on headers only, so we’ll need to split our plugin into a .h and .cpp where up until now we’ve kept everything grouped into a single .cpp file.
Regardless of the way you will compile, you also likely will need to add the includes to your IDE. As I use visual studio, I had to go to Project > {My Project} Properties, then under C/C++ > General > Additional Include Directories I added the path to the include folder that came with the Qt library, so that Visual Studio wouldn’t highlight everything in red.
Making the QtWidget
I will not post an entire example directly in this blog post as the amount of code needed is a bit much, instead we will follow along a couple of examples provided by Foundry.
The first example we will look at is called `AddCustomQt` (Reminder: example code lives with your Nuke install, in the folder `Documentation/NDKExamples/examples`).
You will notice the code is broken out into 2 parts: AddCustomQt.moc.h and AddCustomQt.cpp.
The header file is pretty minimalist, and only consists of the definition for the MyWidget class, which is a subclass of QDial, with a few added functions to allow for communication with Nuke. This Header MUST* be separated from the cpp code for MOC, as it uses the Q_OBJECT macro (*there might be other ways but following the example was the easiest).
In the cpp file, the Qt Widget class is fully implemented. In this example, the widget is essentially a default Qt widget, therefore very little code is present with regards to the drawing if the widget.
You could go fully custom and have a paintEvent as well as custom mouse events for handling user-interaction.
Find a simple example of this on this gist, where I make a simple toggle widget: https://gist.github.com/herronelou/445668948fdef9d2c6e86f2c80dfd68a
Making the Knob
Making the widget is not sufficient, we also need a subclass of DD::Image::Knob, here called MyKnob.
MyKnob lists MyWidget as friend class, which allows it to access a few private functions it couldn’t otherwise access.
In MyKnob, the necessary code to update the widget from knob values, and vice versa is implemented, as well as to_script and from_script, which are necessary to properly store the knob value in the nk file.
You will notice that your Op (in the example the class AddCustomQt) creates the knob in its knobs function, in turn, the knob creates the widget, in its make_widget function.
That is roughly the extent of what is needed at a minimum to make a basic custom knob.
Advanced: Ensuring Python API compatibility
If you access the knob from Nuke with Python, you will notice that the returned class is only the parent nuke.Knob class. It will be nearly impossible to interact with the knob using python (although to_script and from_script seem to be working).
There is yet more work needed to expose your custom Knob class to Python. Here, the AddCustomQt example doesn’t help, but we can look at another example from Foundry: CryptomatteLayerWidget and CryptomatteLayerKnob.
Just like introducing Qt in the plugin required the Qt library to out CMakeLists, introducing Python requires doing so for Python. Luckily we don’t need to install it separately, as Nuke ships with the necessary files.
We can add the library to our CMake file:
find_package(Python3 COMPONENTS Interpreter Development)
For the rest, I would recommend going through the example to see everything that was implemented by Foundry for the cryptomatte knobs, but I also wrote a more minimalist example based on the toggle knob I shared above.
See the full code on Gist: https://gist.github.com/herronelou/88c656fd449f14ccfe9f7658686dd8fd
This isn’t really a plugin that does anything useful, it’s purely a demo for the sake of of showing how to implement a custom Qt Widget with a Python API, here are some of the key steps for making sure the Python implementation works:
- Create a Python Type Object (PyTypeObject): This is like defining a class in Python, and is required for accessing the object in the Nuke Python API
- Define your python methods (PyObject and PyMethodDef): This is like the “def” you would have when writing python methods in your python class. Notice in the example above I had to forward declare my methods. This is because I ran into a bit of a chicken or egg situation, where my knob needs to know the python type, but the python methods need to know about the knob, and C++ doesn’t allow you to use things that haven’t been declared yet. This is where having a proper header file would have come useful, but as I was trying to keep things to a single file for the example, the forward declarations are needed, and the implementation is much further down in the file.
- If your python methods are going to access private attributes of the knob, make sure you add them as friends to your knob class.
- Important! In your knob initialization, call setPythonType, and override pluginPythonKnob
Bonus: Implement the tooltip
As I mentioned somewhere above, custom Qt widgets don’t support tooltips by default like other nuke knobs.
It’s pretty easy to reimplement one that will be visually indistinguishable from the default one: https://gist.github.com/herronelou/88c656fd449f14ccfe9f7658686dd8fd#file-myplugin-cpp-L216
Going further
This is a good start, and sufficient for a lot of cases, but there are a few limitations, for example this knob doesn’t support animation, and it’s a subclass of the base Knob class instead of one of the other pre-defined classes.
The Encryptomatte example shows how to make an enumeration knob.
I haven’t made an animated custom knob yet so I don’t have an example, I’ll add one if I need to make one later or someone contributes an example.
Displaying OpenGL handles in the viewer
Having a knob in the properties bin is cool, but having handles in the viewer is cooler. What would the Transform or CornerPin nodes be without their viewer handles?
OpenGL handles are supported both in 2D and 3D views, and go from simple (a dot for XY_knobs) to complex (Tracking overlays on the tracker).
The official documentation is again pretty scarce for custom handles, but the examples include a few examples:
- Draw2D.cpp and Draw3D.cpp: Drawing handles at the Op level
- Handle.cpp: Was intended to be another example showing how to implement handles/mouse interaction at Op level, but because that’s not supported it adds a Dummy custom knob, which is what I’m more interested in here, as we’re making custom knobs.
- SimpleAxis.cpp and PythonGeo.cpp: 3D examples that draw handles. They’re not made specifically to explain handles, but they use them so I thought it might be worth listing them. PythonGeo also contains a custom Knob with a python interface.
Let’s keep iterating over the toggle knob I built in the first section, and make some handles for it. Let’s draw a smiley face in the viewer when it’s enabled, and a frowny face when its disabled, because why not, and I can’t think of a simple yet useful example. We’ll also make it so that when we click on it, it toggles the knob.
Drawing the handles
We start by declaring 2 functions in our Knob class:
bool build_handle(ViewerContext* ctx) override; void draw_handle(ViewerContext* ctx) override;
These are overrides from the default Knob class.
Build Handles will return True or False depending on whether we want to display the handles or not, while Draw Handles will be in charge of the actual drawing.
The implementation of the first one is simple:
bool MyKnob::build_handle(ViewerContext* ctx) { // We only want to draw handles in 2D mode return (ctx->transform_mode() == VIEWER_2D); }
The second on is a bit more complex, but also a bit more fun.
First we need to make sure we include the gl headers
#include "DDImage/gl.h"
Nuke draws the OpenGL handles layer by layer. In 3D, it has multiple passes, for drawing Opaque objects, transparent objects, hidden objects, lines, then overlays.
In 2D, it only does the shadow, then the lines.
Usually, the shadow will just be the same drawing at the lines but black and offset by one pixel, however you can make it draw other stuff if you really want to.
void MyKnob::draw_handle(ViewerContext* ctx) { if (!ctx->draw_lines()) return; if (ctx->event() == DRAW_LINES || ctx->event() == DRAW_SHADOW) { // Get the color we need to draw. node_color() will return black if the current event is DRAW_SHADOW, else the color of the node as set by the user. glColor(ctx->node_color()); float centerX = ctx->viewerWindowFormatContext().format.width() * 0.5f; float centerY = ctx->viewerWindowFormatContext().format.height() * 0.5f; // Draw a circle at the center of the viewer, with a radius equal to 20% of the viewer height. float radius = centerY * 0.4f; // CenterY is already half the height of the viewer, so 40% of that is 20% of the viewer height. // As far as I know, there's no premade ways to draw circles in glew, so we're going to draw them as segments. const int numSegments = 100; // Number of segments for circles const float PI = 3.14159265359f; // Draw head (large circle) glBegin(GL_LINE_LOOP); for (int i = 0; i < numSegments; i++) { float theta = 2.0f * PI * float(i) / float(numSegments); float x = centerX + radius * cosf(theta); float y = centerY + radius * sinf(theta); glVertex2f(x, y); } glEnd(); // Draw left eye (filled) float eyeRadius = radius * 0.15f; float eyeOffsetX = radius * 0.35f; float eyeOffsetY = radius * 0.25f; glBegin(GL_POLYGON); // Changed to GL_POLYGON for filled circle for (int i = 0; i < numSegments; i++) { float theta = 2.0f * PI * float(i) / float(numSegments); float x = (centerX - eyeOffsetX) + eyeRadius * cosf(theta); float y = (centerY + eyeOffsetY) + eyeRadius * sinf(theta) * 1.5f; // Scale the eye vertically a bit glVertex2f(x, y); } glEnd(); // Draw right eye (filled) glBegin(GL_POLYGON); // Changed to GL_POLYGON for filled circle for (int i = 0; i < numSegments; i++) { float theta = 2.0f * PI * float(i) / float(numSegments); float x = (centerX + eyeOffsetX) + eyeRadius * cosf(theta); float y = (centerY + eyeOffsetY) + eyeRadius * sinf(theta) *1.5f; // Scale the eye vertically a bit glVertex2f(x, y); } glEnd(); // Draw smile (curved line) glBegin(GL_LINE_STRIP); for (int i = 0; i <= numSegments / 2; i++) { float theta = PI * float(i) / float(numSegments / 2); float x = centerX + radius * 0.6f * cosf(theta); float y; if (_data) { // If the knob is toggled, draw a smile y = centerY - radius * 0.6f * sinf(theta); } else { // If the knob is not toggled, draw a frown y = centerY + radius * 0.6f * sinf(theta) - radius * 0.6f; } glVertex2f(x, y); } glEnd(); } }
And we now have a smiley face in our viewer:
Mouse interaction
We now have OpenGL handles in our viewer, but no way to interact with them with our mouse.
Adding support for that is pretty simple, the ViewerContext’s event contains pretty much all the information we need about the mouse, we just need to enable a callback.
First we add a declaration for the callback. This must be static for it to work:
static bool handle_cb(ViewerContext* ctx, Knob* knob, int index);
Then, we implement this function, and make a minor modification to draw_handle:
// Callback function for the handle // We don't need the index, but the function signature requires them. bool MyKnob::handle_cb(ViewerContext* ctx, Knob* knob, int index) { // Cast the knob to our custom knob. This is required because the handle_cb is a static // function and doesn't have access to the member variables of the class. // If the knob is not our custom knob, we return false to indicate that we don't have an interest in this event. MyKnob* myKnob = dynamic_cast<MyKnob*>(knob); if (myKnob) { switch (ctx->event()) { // I don't need a switch here because I only care about PUSH, but in many cases you will need to handle multiple events case PUSH: { // Toggle the data myKnob->setValue(!myKnob->_data); break; } } // return true to indicate that we have an interest in this event return true; } return false; } void MyKnob::draw_handle(ViewerContext* ctx) { if (!ctx->draw_lines()) return; if (ctx->event() == DRAW_LINES || ctx->event() == DRAW_SHADOW || ctx->event() == PUSH) // Note the added PUSH event, which we need to detect mouse clicks { begin_handle(ctx, handle_cb, 0, 0, 0, 0); // The 0's are an index and x, y and z values, which we don't need for this example. // UNCHANGED CODE HERE //.................... end_handle(ctx); } }
All we do in draw_handles is calling begin_handle before we draw our openGL handles, and end_handle after. Anything drawn between begin and end will be considered “opaque” to clicks.
We are now able to click anywhere on the face and it will toggle the knob (as long as we click a solid area).
This is all we’ll be covering in this part about handles, but part 5 will contain a much more complex example of handles.
Using the Table_knob
Nuke contains a knob called the Table_Knob, which is really convenient when needing to display an arbitrary number of data points. Its class is not exposed in the public API so we can’t subclass it (unless we can, and I just don’t know how to), but we can use it in our plugins.
You’re probably familiar with it as part of the Tracker node:
I have used this knob in a node I made a while ago called the LayerRenamer, which allows you to rename Layers using full names or Regexes.
I won’t go super in details over the implementation of the Table Knob, but if you look at the source code of the LayerRenamer, in the _validate function you can see most of the utilisation.
The Table_Knob will be a big part of Part 5, but you might be wondering why I’m introducing it here, when it’s not a custom knob.
There are two main reasons:
- Foundry didn’t seem to think it was necessary to have a python implementation for Table Knobs, you may be aware if you ever tried to interact with the Tracker node in python. While we apparently can’t subclass the table knob itself, we can add a dummy knob to our op and implement a Python object on it, greatly improving usability.
- The data in the table knob doesn’t have handles in the viewer. It does when you use a Tracker node, because Foundry implemented it, but not when you use your own Table knob. Using what we learned in this article and the example from handles.cpp to make a dummy knob, we can add handles for the table!
This made me decide to mention this knob alongside custom knobs, as a lot of the same concepts apply if we want to add python or handles to it.
Feel free to think of this as homework until the next article, try to use it in one of your plugins to get familiar with it.
This concludes part 4 of the NDK series.
In part 5, I’ll share a much larger example of plugin that I made, using combined knowledge from parts 1 to 4, but also uses an external library to simplify a lot of the complex math (by not having to write it myself).
Leave A Comment