Creating Custom Knobs

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

  • Store arbitrary data in NUKE scripts
  • Present OpenGL interface handles in the Viewer, enabling user interaction
  • Post NUKE 6.3v1, draw Qt based user interface widgets to the node’s Properties panel
  • Provide a Python interface into the C++ plug-in

This section is essentially broken up into 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 plug-ins by inheriting from this class. This is sometimes necessary if the Op/Iop needs to keep a state that is not representable with one of the built-in knobs.

As an example, to include a custom Knob in your Op you would do the following in your knobs function:

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

This invokes 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 be 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 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 serialises 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 serialize 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 for an animation might choose to produce key-value pairs in its full serialisation format: a line between the points (0, 0) could be represented as “0 0 100 1” (0, 0) to (100, 1). If the functions were 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 placed directly after the knob name. For example:

GainExample {
  gain 0.5
  name GainExample1
}

The text “0.5” was the output from to_script for that knob. A more complex example where the gain knob has been split, might 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 - 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);
  else
    o << _data;
}

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

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

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

GainExample {
  gain {0.1 0.2 0.3 0.4}
  name GainExample1
}

then from_script is 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 constructs 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 is 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 does not need to be included in the Undo stack, 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 it.

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

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 is ‘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");

The above results in store() being called with the dst pointing at &_data.

It is expected that store evaluates the knob at the frame and time given in context, and copies 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 in particular, that engine functions do not directly access the same data store. If they do, 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 ensures 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 - 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 as 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 (by calling its to_script() function), the SerializeKnob then asks our plug-in to covert any important data into a std::string (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 {
private:
  CallbackHandler * _host;
  const char *Class() const { return "SerializeKnob"; }

public:
  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);
        if(!success)
          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) {
      saveString.insert(saveString.begin(),'\"');
      saveString+="\"";
    }

    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 are simply loading and saving the std::string data found in the important_data_to_keep variable.

class Serialize :
public Iop, public CallbackHandler
{
protected:
  /* 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 are only manipulating string data, the equivalent could be accomplished using a simple Text_knob, however the idea is to show how it could be extended to more complicated scenarios. In practice, you would 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 - http://www.boost.org/). The code for saving to a Boost archive is commented out in the snippet above.

Custom Knob Widgets Using Qt

From NUKE 6.3 onwards, 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 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, from 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 ‘add’ or ‘gain’ operations.

To compile this from the command line, ensure that:

  • The QTDIR is set and pointing to your Qt toolkit
  • Your NDKDIR is set and pointing to the directory up from the DDImage include directory

Then:

make -f Makefile.qt

Once the plug-in is compiled, ensure it is in your plug-in 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 that 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.

Lets 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 that inherits from QDial.

class MyKnob;

class MyWidget : public QDial {
  Q_OBJECT

public:
  MyWidget( MyKnob* knob );
  ~MyWidget();

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

 public Q_SLOTS:
   void valueChanged ( int value );

private:
  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. It’s important to check that the knob is still valid at this point, as there are some circumstances when it could have been destroyed before the widget.

MyWidget::~MyWidget()
{
  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 a widget needs to push a new value into a knob it should set the knob value and then call ‘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;
  changed();
}

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

WidgetPointer MyKnob::make_widget(const DD::Image::WidgetContext& context) {

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

Note

Keep in mind that a knob can have multiple instances of widgets. For example, a make_widget can be called more than once or 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.