Creating custom knobs

Creating a custom knob allows you to pull off one or more of the following:

  • Store arbitrary data into the NUKE scripts.
  • Present open gl interface handles in the viewer, with which the user can optionally interact.
  • Post NUKE 6.3v1, draw Qt based user interface widgets to the node’s user interface panel.
  • Provide a Python interface into the C++ plug-in

This section is broken up into essentially these main topic areas, prefixed by a short breakdown of the knob architecture itself.

Knob architecture

class DD::Image::Knob

It is possible to create custom knobs within plugins by inheriting from this class. This can be necessary if the Op/Iop needs to keep around state that is not representable with one of the built-in knobs.

To include a custom Knob in your Op, you would for example do the following in your knobs function:

CustomKnob1(DemoKnob, f, &_data, "data");

This would invoke your knob constructor as follows:

new DemoKnob(&f, &_data, "data");

If you do not wish to specify a data pointer, you can use CustomKnob0. If you wish to specify a label as well as a name, you can use CustomKnob2.

bool DD::Image::Knob::not_default() const

Knobs are written out as part of the saved script (or when copied to the clipboard) only if the not_default() function on them returns true. Otherwise, they are assumed to still be at their default value, and not be worth writing out.

A knob’s default value can per-knob rather than per-knob-class. It is entirely up to the knob how this is handled, except that it should remain invariant for the lifetime of that knob.

It is possible simply to always return true from this.

void DD::Image::Knob::to_script(std::ostream& o, const OutputContext* oc, bool quote) const
const char* DD::Image::Knob::get_text(const OutputContext* oc) const

to_script should serialise the data represented by the knob. The default implementation calls get_text to get the string to write out, and then automatically handles any quoting necessary.

Both functions take a pointer to an OutputContext. If this is NULL, it is intended for saving or copying, and therefore should serialise the entire internal state of the knob.

If it is not NULL, then it is being invoked from the value function, or similar, and should give a serialisation of that knob’s state at the given frame and view. For example, a simple knob representation an animation might choose to produce key-value pairs in its full serialisation format. For example, a line between the points (0, 0) could be represented as “0 0 100 1” (0, 0) to (100, 1). If the functions were to be called with an OutputContext, it should then evaluate that curve at the appropriate frame and time. For example, it might take the time value as x on the curve. Therefore, if OutputContext had a frame of 50, it should produce an output of “0.5”.

The “quote” parameter on to_script indicates whether or not the output needs quoting for TCL safety. When writing to a file, the output from to_script is directly placed after the knob name. For example, in this:

GainExample {
  gain 0.5
  name GainExample1

the text “0.5” was the output from to_script for that knob. In a more complex example, where the gain knob has been split, this will look like:

GainExample {
  gain {0.1 0.2 0.3 0.4}
  name GainExample1

The extra quoting is needed as newlines are treated no differently to other whitespace, and otherwise it would assume 0.2 was the name of the next knob. Therefore, the “quote” parameter is passed as true, to indicate that the output needs potentially quoting if there are any characters in it which would disrupt TCL parsing. The DD::Image::Knob::cstring helper class exists to deal with this, a simple implementation might be:

void to_script(std::ostream& o, const OutputContext* oc, bool quote) const
  if (quote)
    o << cstring(_data);
    o << _data;

Note that this extra layer of quoting must not be put if the “quote” parameter is false, and note that it will be stripped out by the time the value is passed to from_script.

bool DD::Image::Knob::from_script(const char* None)

from_script is passed the serialised value. It should set the knob’s internal state to match. This is a value that has already stripped of any quoting that was added in to_script because of quote. For example, if the script file is

GainExample {
  gain {0.1 0.2 0.3 0.4}
  name GainExample1

then from_script will be passed the string “0.1 0.2 0.3 0.4”. You can use the helper class DD::Image::Knob::Script_List to help parse TCL-formatted lists. For example, constructing a Script_List with the string “0.1 0.2 0.3 0.4” as a constructor will construct an object with size() = 4, whose elements are “0.1” “0.2” “0.3” and “0.4”. Script_List can deal with elements themselves being TCL-quoted structures, so “{0 1} {2 3}” would parse to a two-element list, with elements “0 1” and “2 3”. Script_List can then be used in turn on these elements, etc.

from_script should return true if the value changed. Also, if the value changed, it should have called new_undo before the internal state changed, and changed after it does. This is so that NUKE is aware that the script may need evaluating, and also so that the undo system can be maintained.

void DD::Image::Knob::changed()

Any change made to the internal state of the knob should be surrounded by a call to new_undo before (at the point of the new_undo a to_script should result in the original value), and changed after. If the knob is not to be included in the Undo stack then it can be marked with the flag Knob::NO_UNDO, and calls to new_undo can be omitted, but calls to changed should still be made.

void DD::Image::Knob::reset_to_default()

By default this calls from_script(""), which is expected to reset the knob to its default value. If this would not happen, you should override this.

void DD::Image::Knob::store(StoreType None, void* dst, Hash& hash, const OutputContext& context)

As described in [...], NUKE updates your Op with the new values of knobs by calling knobs and arranging for the closure to place the new values in those pointed to. It does this by calling the store function on the knobs. You therefore need to implement the store function.

StoreType is used internally in NUKE for type information. In a custom knob it can reasonably be ignored, as it will be ‘Custom’. dst here is the pointer that was passed to the CustomKnob1 or CustomKnob2 function as its third argument. For example, if you included your knob in knobs like so:

CustomKnob1(DemoKnob, f, &_data, "data");

then it would result in store() being called with the dst pointing at &_data.

It is expected that store will evaluate the knob at the frame and time given in context, and copy that cooked data into the destination pointer. If the knob doesn’t have any animation, this might be a simple memcpy. store is also an appropriate place to do any proxy-scaling, as the OutputContext contains the proxy scaling information.

It is important that custom knobs do this, and that in particular that engine functions do not directly access the same data store, as the internal knob value could be changed _during_ a call to engine, which might result in the cache being poisoned.

In addition to this, the store function should also add the cooked data it stored to the hash it has been passed in. This will ensure proper calculation of the Op’s hash.

void DD::Image::Knob::append(Hash& hash, const OutputContext* context)

If context is specified, this should add the same thing to the hash that a call to store would with the same context.

If context is NULL, it should hash the entire representation of the curve, for all times and views. This is used for the ‘curve hash’.

Storing Arbitrary Data

The previous section touched briefly on writing arbitrary data using the to_ and from_ script methods provided on the Knob class, so go back and read it if you haven’t already. In this section we’ll break down a more complete example from the NDK - namely Serialize.cpp, the interesting parts of which are shown in the snippets below. In the first part, we set up the custom SerializeKnob as a sub-class of Knob, allowing an instance of CallbackHandler supplied to be the constructor. The idea is that we make our plug-in a CallbackHandler, then when NUKE asks our SerializeKnob if it wants to save any data (i.e. by calling its to_script() function), the SerializeKnob will then ask our plug-in to covert any important data into a std::string (i.e. serialize the data) and pass it back to the SerializeKnob to be saved in the script. In the reverse process, when the script contains some saved information about the SerializeKnob, NUKE asks the SerializeKnob to process the data (stored as a string) in the script by calling its from_script() function. This passes the stored data string to our plug-in, and asks it to deserialize the data into something useful.

class SerializeKnob : public Knob {
  CallbackHandler * _host;
  const char *Class() const { return "SerializeKnob"; }

  SerializeKnob(Knob_Closure *kc, CallbackHandler * host, const char* n): Knob(kc, n),_host(host)

  bool not_default() const {
    return true;

  virtual bool from_script(const char* v) {

    std::string loadString(v);

    printf("Function from_script() called, with param: %s.\n", loadString.c_str());

    if (_host && loadString!="") {
      bool success = false;

      try {
        success = _host->load(loadString);
          error("Failed to load from script");
      }catch (const char * msg) {
        printf("Failed to load from script: %s\n", msg);
      }catch (...) {
        error("Failed to load from script");

      return success;

    return true;

  virtual void to_script(std::ostream& theStream, const OutputContext*, bool quote) const {

    printf("Function to_script() called, with \"quote\" param: %d.\n", quote);

    std::string saveString;

    if (_host) {
      try {
        _host->save( saveString );
      }catch (const char * msg) {
        printf("Failed to save to script: %s\n", msg);
      }catch (...) {
        error("Failed to save to script");

    if(quote) {

    printf("\tSaved data: %s.\n", saveString.c_str());

    theStream << saveString;

The following code snippet shows how our plug-in “Serialize” inherits and implements the load and save functions of the CallbackHandler. In this contrived example, we’re simply loading and saving the std::string data found in the important_data_to_keep variable.

class Serialize :
public Iop, public CallbackHandler
  /* lines omitted for brevity */
  SerializeKnob * _serializeKnob;
  std::string important_data_to_keep;
  /* lines omitted for brevity */

  virtual void knobs( Knob_Callback f ) {
    _serializeKnob = CustomKnob1(SerializeKnob, f, this, "serializeKnob");
  /* lines omitted for brevity */

  bool load(std::string loadString){

      /* going to pretend that we've de-serialized loadString
       and are copying the meaningful results back into
       "important_data_to_keep" */

    important_data_to_keep = loadString;

    printf("Trying to load \"%s\" from script.\n", loadString.c_str());

    return true;

  void save(std::string &saveString){
      /* if we were using boost to save the data */
    //std::ostringstream archive_stream;
    //boost::archive::text_oarchive archive(archive_stream);
    //archive << test;
    //std::string outbound_data_ = archive_stream.str();

    /* saving some stuff to the string */
    //saveString += outbound_data_;//"Hello World!";

      /* instead we're going to pretend "important_data_to_keep"
       already contains serialized data */
    saveString += important_data_to_keep;
      /* i.e. saveString += "Hello World!"; */

    printf("Trying to save \"%s\" to script.\n", saveString.c_str());


As we’re only manipulating string data, the equivalent could be accomplished by a simple Text_knob, however the idea is to show how it could be extended to more complicated scenarios. In practice, you’d store your data in an appropriate structure or class, and then use a robust serialization scheme to handle the object-to-string conversion, such as the one found in the Boost library ( The code for saving to a Boost archive is commented out in the above snippet.

Custom knob widgets using Qt

As of NUKE 6.3 and greater you can add your own GUI to a custom knob using NUKE’s inbuilt Qt toolkit.

Installing the Qt build environment

In order use Qt in your custom knob you will need to get the Qt source code and Qt tool binaries from the Foundry web site.

You can download them from the NUKE Developer site.

Unpack the files into a directory and then at the command-line set the QTDIR environment variable to point to that directory, so that the example Makefile will be able to find the toolkit.

Compiling the Example

Included in the example directory is a custom node with a custom knob that implements a ‘add’ or ‘gain’ operation.

To compile to this at the command line ensuring the QTDIR is set and pointing to your Qt toolkit, plus your NDKDIR set and pointing to the directory up from the DDImage include directory, then:

make -f Makefile.qt

Once the plugin is compiled ensure it is in your plugin path, fire up NUKE and create the new node ‘AddCustomQt’. You should see a simple node with a dial instead of a slider to control the gain level.

The source files are in AddCustomQt.cpp and AddCustomQt.h

Example breakdown

The basic steps required to add a custom GUI to your knob are:

  1. Create a new widget class the inherits from QWidget for your UI
  2. Register and handle knob callbacks from NUKE.
  3. Implement the knob method make_widget() and return your new Qt widget from this.

Let break down the example step by step to show how it works.

The first thing we need is the widget itself. In this case we have a widget the inherits from QDial.

class MyKnob;

class MyWidget : public QDial {

  MyWidget( MyKnob* knob );

  void update();
  void destroy();
  static int WidgetCallback( void* closure, Knob::CallbackReason reason );

 public Q_SLOTS:
   void valueChanged ( int value );

  MyKnob* _knob;

Notice here we are passing a back pointer to the widgets knob, and defining a callback called WidgetCallback.

The constructor registers the widget with the knob to handle callbacks.

MyWidget::MyWidget(MyKnob* knob) : _knob(knob)
  setNotchesVisible( true );
  setWrapping( false );
  _knob->addCallback( WidgetCallback, this );
   connect( this, SIGNAL(valueChanged(int)), this, SLOT(valueChanged(int)) );

The destructor removes the callback. Its important to check for the knob still being valid here as there are some circumstances when it could have been destroyed before the widget.

  if ( _knob )
    _knob->removeCallback( WidgetCallback, this );

The widget then handles three callbacks from NUKE.

  • Knob::kIsVisible - return true if the widget is visible, false otherwise. Used to decide if handles are shown in the viewer
  • Knob::kUpdateWidgets - the widget should fetch the current value from the knob and update the UI.
  • Knob::kDestroying - the widget’s knob is being destroyed and therefore any knob back pointer is now invalid and should no longer be used.

If widget needs to push a new value into knob it should set the knob value and then called ‘changed()’.

For instance the Qt slot valueChanged is called with the dial changes value.

void MyWidget::valueChanged(int value)
  _knob->setValue( value );

And in the knob the setValue function looks like this:

void MyKnob::setValue( int value ) {
  new_undo ( "setValue" );
  _data = value;

Finally to actually create the UI the knob implements the make_widget function.

WidgetPointer MyKnob::make_widget() {

  MyWidget* widget = new MyWidget( this );
  return widget;


Keep in mind that a knob can have multiple instances of widgets ( eg make_widget can be called more than once ). For example when link knobs are used more than one widget can be created. Make sure you don’t rely on a one to one mapping between widgets and knobs.