Overcoming (some) Tool Development Obstacles - Tutorial - Guillermo Algora - Visual Effects Compositor

Guillermo Algora
Go to content
Let's take a look at some roadblocks that we could encounter in the process of making new tools and some solutions to overcome them. The following points are a compendium of obstacles that I myself have encountered along the way.

By no means are all the solutions mine, but some have been provided by users in the Nuke forum and other means (see here); however, I thought it would be practical to combine these solutions into a single article that can bring light to others in darker times.

This guide will not reference solutions regarding the menu.py or init.py as the purpose of it is to showcase solutions outside of these two.
1. Helpful Documentation:

First, we will take a look at the available documentation as this should be the starting point.
1.1. Nuke Python Developer's Guide:

The guide aims to familiarize you with using the Python API in Nuke and using Nuke as a Python module by giving you practical examples of using the API.

1.2. Nuke Python API:

An API Reference is found at the bottom of the 'Nuke Python Developer's Guide':

While the 'Index' (link) and 'Module Index' (link) are available right at the bottom of the Nuke Python Developer's Guide homepage.
However, I find these less comprehensible and I prefer the easiness and tree-like visualization of the "standard" but older Nuke Python API:

1.3. TCL Scripting:

The command center for the official TCL scripting information regarding Nuke:

1.4. Nuke Developers Kit:

Here we can find a good amount of documentation in regards to the development of plug-ins for Nuke. Fairly complete, and includes:
NDK Reference, NDK Developer's Guide, Examples (source files), Header files, OpenFX as well as a Guide to Writing Blink Kernels and the Blink API Reference.

Unfortunately, two of the links from above are currently unavailable (the 'Header files' and the 'Blink API Reference'):

The Blink API Reference can be found here: https://learn.foundry.com/nuke/developers/8.0/Blink/index.html
While the header files must be accessed via the Nuke's top menu -> Help -> Documentation -> C++ Plug-in Development -> Header files.

It is in the local Documentation of the Nuke client itself where we can find additional elements of the Nuke Developers Kit, for example, a 'Cat File Creation Reference' and a 'Hiero Python Developers Guide' among others.

Also as usual, the Nuke forums (link) and other users' websites are a fantastic source of information.
2. Online documentation version:

The online documentation comes with a version and it corresponds to the Nuke version. Which is the latest version available is not always clear, for example, a Google search for 'Nuke API' will return as the first result the Nuke Python API for version 7.0:

This is denoted by the number 70. However, according to my records the latest of such documentation (presented in that form) is version 12:


This version number can be entered both in an integer (120) or decimal (12.0) form.

It is important to keep this in mind while searching for documentation, as outdated API will not reflect changes from newer versions of Nuke. Some of the documentation accepts a 'latest' statement instead of a version number, such is the case of the 'Nuke Python Developer's Guide' and the 'Nuke Developers Kit'. e.g:

3. Flags:

Knob flags provide a mean to customise aspects of a previously created knob. Flags can be added or removed from a knob using the setFlags and clearFlags calls. In some circumstances, after such an update the knob interface may require manual refreshing through manual use of the appropriate updateWidgets(), redraw(), or changed() call. The most commonly used Flags are generic across all knobs and the remainder are specific to subsets of the full roster.

The official list of Nuke Flags is not as easily accessible as it should be; however, the easiest way to find it is through the Nuke client itself. In the top menu, go to Help -> Documentation -> C++ Plug-in Development -> Header files -> DDImage -> Knob.h, and search for the FlagMask enumeration. Here is a list of all the Nuke flags as of version 15.0.2 (link).

There is also resources available on Nukepedia (link) as well as in the NDK Developers Guide v13.2 (link). The Foundry has removed the list of flags from the Guide as of v14.
4. Float knob without slider:

Nuke makes it easy to create an Integer knob without a slider, but what about a simple Float knob?  
There are two solutions that I know of so far:
4.1. The nuke.Range_Knob:

This knob comes with a slider the first time that you create it, but once you have copied the node or closed the script the slider will disappear. Happy days!
4.2. nuke.knob.clearFlag(0x00000002):

Another possible way is to create a Double_Knob and delete the slider by clearing the flag: 0x00000002 (or 'nuke.SLIDER'). One drawback of this procedure is that Nuke fails to save the flag clearance, meaning that every time we duplicate the node or close the script, it will reappear. The solution to this is to add a function in the 'onCreate' knob which will clear the flag every time the node is pasted or the script opened (i.e. the node is created):

function = "nuke.thisNode()['knob_name'].clearFlag(0x00000002)" # Substitute knob_name

I have presented this approach because, while it might be less efficient than the previous method, it proves useful not just for the SLIDER flag but, in fact, for any other flag.
5. Transferring a multi-line Python function as a docstring:

This one is quite simple, but worth noting. The three consecutive quotes (''') allow a multi-line string in Python and with this we can pass a multi-line function to a pertinent knob in Nuke, for example to the callback knobs.

function = '''  
node = nuke.thisNode() for knob_name in node.knobs():  
if knob.name() == 'size':  
6. Setting a default value to a knob:

The python code for setting a default value is not difficult:

node.knob.setDefaultValue([ ])
* [ ] A list is required because Nuke expects a 'sequence of floating-point values'; however, this does not mean that you cannot input a single number.

Saving the default value after the node has been copy-pasted or the script closed is a trickier matter.
6.1. Saving the node as a gizmo:

Saving the node (Group) as a gizmo effectively bakes-in default values, however this comes with a downside: gizmos must be placed in a suitable folder and loaded, unlike normal groups, which can be copied and pasted, making them easier to transfer.
6.2. Saving it in an onCreate() function:

If we want to maintain our tool as a Group and keep the default value settings, we can add a function to the 'onCreate' knob in the node so that every time the node is copy-pasted or the script closed-opened the default value settings are restored. This will not take a toll on performance since this callback is only called once on those occasions.

function = "nuke.thisNode()['knob_name'].setDefaultValue([value(s)])" # Substitute knob_name and value(s)

You can find documentation about callbacks in here:
7. Expressions in certain knobs can only be set with Python:

Right-click -> Add expression... is great, but there are some knobs that are suitable for Expressions which cannot be set that way. To name a few: a drop-down menu (e.g. a 'filter' menu knob), a check-box type of knob (e.g. 'disable'), the node's position ('xpos', 'ypos'), etc. In this matter, Python comes to our aid:

node.knob.setExpression("expression") # Substitute expression
8. Accessing the Link_Knob not the referent:

Ohh, how wonderful this knob is. By Foundry words: "Soft links to another node, identified by name passed on construction. Presents panel and viewer widgets from soft linked knob, and is is generally used in NUKE to present knobs internal to a gizmo on its main param panel."

Link knob is what happens when you 'Pick' a knob through the UI in the 'Manage User Knobs' panel; and the way that you can create one with Python looks like this:

knob = nuke.Link_Knob('knob_name') # Substitute knob_name
knob.setLink('referent_node_name.referent_knob_name') # Substitute referent_node_name and referent_knob_name
However, when it comes to accessing the referent or the Link knob, the process has its nuances. Trying to access the knob by its name using the node.knob() method or the dictionary method (like node['knob_name']) as you would normally do returns the referent's knob, not the Link_Knob.
8.1. node.allKnobs() method:

Get a list of all knobs in this node, including nameless knobs. Using the node.allKnobs() method returns both the referent knob and the Link_Knob objects.
We can further filter this result by searching for the nuke.Link_Knob class, using Python's isinstance() function:
isinstance(object, type), which returns True if the specified object is of the specified type, otherwise False. For example:

for knob in nuke.selectedNode().allKnobs():
  if isinstance(knob, nuke.Link_Knob)
       if knob.name() == 'knob_name': # Substitute knob_name
8.2. node.knobs() method:

Get a dictionary of (name, knob) pairs for all knobs in this node. node.knobs() returns both the name and the object.
'size': <Link_Knob object at 0x0000021E7B928090>

Therefore, and as we have seen above, we can also filter both by knob type and name.

knobs = nuke.selectedNode().knobs() # A dictionary containing the node knobs {name : knob_object}
for name, knob in knobs.items():
  if isinstance(knob, nuke.Link_Knob):
      print(name, knob)    

Result: size <Link_Knob object at 0x000001CD213F5A50>
9. Environment variables:

There's a function in the Nuke Python module that allows us to obtain an environment report (in the form of a dictionary):

The result looks like this:
# Result:
{'64bit': True, 'ExecutablePath': '/usr/local/Nuke12.2v3/Nuke12.2', 'ExternalPython': False, 'LINUX': True, 'MACOS': False, 'NukeLibraryPath': '/usr/local/Nuke12.2v3/libnuke-12.2.3.so', 'NukeVersionDate': 'Sep 17 2020', 'NukeVersionMajor': 12, 'NukeVersionMinor': 2, 'NukeVersionPhase': None, 'NukeVersionPhaseNumber': 609517, 'NukeVersionRelease': 3, 'NukeVersionString': '12.2v3', 'PluginExtension': 'so', 'PluginsVerbose': False, 'WIN32': False, 'assist': False, 'gui': True, 'hiero': False, 'hieroNuke': False, 'hieroStudio': False, 'indie': False, 'interactive': True, 'nc': False, 'nukex': False, 'numCPUs': 16, 'ple': False, 'studio': False, 'threads': 16}

We can access any of the keys in the dictionary and therefore set filters to our scripts based on environment variables:
nuke.env['environment_variable'] # Substitute environment variable

For example, if we are writing a tool that may not work in Nuke Non-Commercial, we can set the following filter to stop execution if the non-commercial environment reports 'True':
if nuke.env['nc']:
10. Node's Info via the 'i' key:

Quite simple but useful. If you select a node in the node graph and press the 'i' key, a pop-up panel will appear with information such as the node's name, class, memory usage, and its visible knobs and values.
11. nuke.createNode() vs nuke.nodes.Class():

There are two ways to create nodes via Python and each has its own advantages.
11.1. nuke.createNode():

createNode() behaves as if the node was created through the menus, which means it:
  • Changes the node selection to the new node.
  • Tries to auto connect the new node.
  • Auto-positions the new node.
  • Hooks up the current Viewer buffer.
  • Opens the control panel by default.

nuke.createNode(node, args, inpanel)
node – Node class (e.g. Blur).
args – Optional string containing a TCL list of name value pairs (like size 50 quality 19)
inpanel – Optional boolean to open the control bin (default is True; only applies when the GUI is running).
Returns: Node.

nuke.createNode('Blur', 'name MyBlur size 5 mix 0.3', inpanel = False)

Upon creation, it features automatic numbering upwards of the node's name (if duplicated), changing knob values and an 'inpanel' parameter to prevent the node's properties panel from opening (inpanel = False or inpanel = True).
11.2. nuke.nodes.Class():

The nuke.nodes module can be used instead of the nuke.createNode() method, and it is Foundry's suggested method for scripting. The difference is that nuke.nodes does a little less hand-holding. It simply creates a node without doing or changing anything. Use this to make sure the Node Graph does not change for the user after the script has run.

It also allows changing knob values on creation in the form of knob name = value pairs separated by commas (e.g. size = 5, mix = 0.3).
Unlike the previous method, it automatically does not open the node's properties panel and does not force the node's name numbering upwards when duplicated if a name is specified on creation. This is an advantage for us, as it allows us to temporarily have the same name on multiple nodes, but obviously the object ID is different for each one (e.g. Node object at 0x0000027743BD6DF0). The node name will automatically rename (numbering upwards) when the script is re-opened or the node copy-pasted.

nuke.nodes.Blur(size = 5, mix = 0.3)

For additional information on this matter, check:
12. nuke.Root():

The Project Settings of the Compositing environment are contained within the Root node, but the node itself is never visible in the Node Graph (the Root node is what appears in the control panel when you press the 's' key while hovering the mouse over the DAG). It is the ideal place to set and retrieve certain information such as the project's file path, format, fps, frame range, color settings, etc.

In addition, and this should be done with caution, we can use this node to set the following callbacks: 'onScriptLoad', 'onScriptSave' and 'onScriptClose'. You can find more information about these callbacks here.

Furthermore, we can also use this node to create custom user knobs to store information inside it or even to execute certain code, etc. However, and as stated above, this should be done with the utmost caution, as any mishandling could produce dire consequences, especially since this is an invisible node and also because people will not expect much to happen inside it. To access the node use: nuke.Root()

node = nuke.Root()
13. Force re-evaluation using the 'CurveTool':

When it comes to updating the UI or Viewer information amid code execution, in theory, these will not update until all python is processed. This is not a problem if what we intend is to retrieve updated information from knobs (since this is not the UI) but it is a problem if what we intend is to retrieve, for example, sampled values from the Viewer (link).

To force a re-evaluation of the frame, the most accurate way I have found so far is to use the 'CurveTool' node. To do this, we will create a 'CurveTool' node using the nuke.nodes.Class() method (thus preventing the new node from hooking up to any selected node and as well from opening the properties panel), then execute it by using nuke.execute() (link) and finally deleting the node:

curve_tool = nuke.nodes.CurveTool()
nuke.execute(curve_tool, start_frame, end_frame)
14. Recursive functions:

A recursive function is a function that calls itself recursively. Recursive functions can be particularly useful when dealing with hierarchical structures or nested data, for example, iterating through the node graph or a sequence generation. Any recursive function must have a base condition that stops the recursion or else the function will call itself infinitely. Here is an example of a recursive function that counts the total number of parents a node has all the way back to the last parent in the node tree:

def count_inputs_recursive(node): # Recursive function
  number_inputs = 0 # Number of inputs count.

  for i in range(0, node.inputs()): # For index in node's number of inputs.
      node_input = node.input(i) # Node's input.
      if node_input: # Base condition to stop iteration: node's input must exists.
          # Launch recursive function, using node as the node's input, while adding + 1 to the inputs count.
          number_inputs += count_inputs_recursive(node_input) +1

  return number_inputs # Return the total number of inputs.

number_inputs = count_inputs_recursive(node = nuke.selectedNode()) # Inititate recursive function, on selected node.
Back to content