Oink
Scripting and plugins

Extending Oink (for TDs)

Studio pipeline guide: custom nodes, Python panels, menus, callbacks, init.py / menu.py, PySide tools, gizmos, OFX and MCP.

raw .md

Oink runs a studio's Nuke pipeline code as it is: init.py, menu.py, callbacks, Python panels, gizmos and PySide tools. This page is for pipeline TDs setting it up. Every feature on it is checked against a set of real studio scripts before each release.

Startup scripts and the plugin path

At startup the backend runs, in this order:

StepWhat
1Every plugin folder's init.py: ~/.opencomp (or $OPENCOMP_USER_DIR), then each folder of NUKE_PATH, then each of OPENCOMP_PLUGIN_PATH
2nuke.pluginAddPath(dir) inside an init.py adds dir: its own init.py runs at once, its menu.py later
3Every menu.py, in the same order (the app always has a UI, like Nuke's GUI mode)
4Every oc_nodes/ folder on the plugin path: custom nodes
  • NUKE_PATH works exactly as in Nuke, so point it at your existing studio folder. OPENCOMP_PLUGIN_PATH is for folders only this app should read.
  • __file__ is set while init.py / menu.py run, so os.path.dirname(__file__) works.
  • An error in one file is printed (Script Editor, backend log) and never stops the others.

A typical init.py:

import os, nuke
ROOT = os.path.dirname(__file__)
nuke.pluginAddPath(os.path.join(ROOT, "gizmos"))
nuke.pluginAddPath(os.path.join(ROOT, "python"))
nuke.addFormat("2048 858 1.0 STUDIO_Scope_2K")
nuke.knobDefault("Root.format", "STUDIO_Scope_2K")
nuke.knobDefault("Blur.size", "3")
MenuWhere it shows
nuke.menu('Nuke')the menu bar: items under an existing menu (findItem('Edit')) join it, new menus go after Help
nuke.menu('Nodes')the node toolbar on the left (a new menu gets a button) and the Tab menu
nuke.menu('Node Graph')the Tab menu
nuke.menu('Viewer')the Viewer menu
nuke.menu('Pane')the pane menu (the small square at the left of every tab strip), under a separator: registered panels and Qt tools
m = nuke.menu("Nuke").addMenu("Studio")
m.addCommand("Shot Setup...", shot_setup.run, "alt+shift+s")      # a callable or a string
tb = nuke.menu("Nodes").addMenu("Studio", icon="studio.png")
tb.addCommand("Gizmos/StudioVignette", "nuke.createNode('StudioVignette')")

Shortcuts use Nuke's forms (^#b = Ctrl+Alt+B) or words (ctrl+alt+b) and work anywhere in the app.

Callbacks

FunctionFires
addOnUserCreate(fn, nodeClass=...)a node created by the user (Tab menu, toolbar, nuke.createNode)
addOnCreate(fn, nodeClass=...)every new node, also when a script opens or a .nk is pasted
addOnDestroya node deleted
addKnobChanged(fn, nodeClass=...)a knob edited in the Properties panel (nuke.thisNode(), nuke.thisKnob() are set); a node's own knobChanged knob runs too
addBeforeRender / addAfterRender / addBeforeFrameRender / addAfterFrameRenderaround every render of a Write (the Render dialog, nuke.execute)
addOnScriptLoad / addOnScriptSave / addOnScriptCloseFile > Open, Save, New / Close
addFilenameFilter(fn)every file path a node reads or writes (Read, Write, ReadGeo, Precomp...): path mapping between platforms

The two most common studio callbacks, as they run here unchanged:

def create_write_dir():                      # beforeRender: make the output folder
    d = os.path.dirname(nuke.filename(nuke.thisNode()))
    d = nuke.callbacks.filenameFilter(d)
    if not os.path.isdir(d):
        os.makedirs(d)
nuke.addBeforeRender(create_write_dir)

def path_map(f):                             # a Windows artist's P:/ opens on Linux
    return f.replace("P:/", "/mnt/proj/") if f.startswith("P:/") else f
nuke.addFilenameFilter(path_map)

Callbacks run in the backend. Their print output and errors go to the Script Editor; nodes they create or change show up in the Node Graph at once and join the undo step of the edit that fired them.

Python panels

nukescripts.PythonPanel and nuke.Panel are drawn with the same knob widgets as the Properties panel (fields, sliders, dropdowns, checkboxes, colour knobs, file browser). The panel's knobChanged runs in the backend on every edit and the panel redraws from the answer, so setEnabled, setVisible, setValues, setValue and adding / removing knobs all show at once.

CallShows
panel.showModalDialog()a modal dialog with OK / Cancel; returns True / False. Knobs named OK / Cancel replace the default buttons; finishModalDialog(result) closes it from knobChanged
panel.show()a floating panel (it can be docked like any pane)
panel.addToPane()a tab beside Properties
nuke.Panel(title) + addSingleLineInput, addEnumerationPulldown, addBooleanCheckBox, addFilenameSearch, addClipnameSearch, addMultilineTextInput, addPasswordInput, addRGBColorChip, addButton... .show()the old-style modal form; show() returns 1 / 0 or the index of the pressed addButton
nuke.message, ask, getInput, getFilename, getClipname, choice, getColorsmall modal dialogs; the script waits for the answer
nuke.ProgressTask(title)a progress dialog; setMessage, setProgress, and isCancelled() turns True when the user presses Cancel

Every knob type the Python API can create has a widget: String, EvalString, Multiline, Password, Int, Double, Array, XY, XYZ, UV, WH, Scale, BBox, Box3, Range, Keyer, Color, AColor, ColorChip, Enumeration, Radio, CascadingEnumeration, Boolean, File, Format, Channel, ChannelMask, Font, PyScript, Script, Pulldown, Text (a text line, or a divider when it has no label and no text), Help, Tab, BeginTabGroup / EndTabGroup, and the -STARTLINE layout flag. Link, Obsolete, Transform2d, Histogram and PyCustom knobs are not shown.

Register a panel for the pane menu the Nuke way:

nukescripts.registerPanel("com.studio.ShotSetup", ShotSetupPanel)
nuke.menu("Pane").addCommand("Shot Setup", lambda: ShotSetupPanel().addToPane())

Headless (farm, tests) there is no one to answer: dialogs return what Nuke returns in -t mode (ask is False, getInput is None, panels are cancelled).

PySide / Qt tools

The app's interface is not Qt, so a Qt widget can't be drawn inside it. Studio PySide tools run instead in a Qt helper: a separate Python process with PySide6 that the backend starts when a Qt tool is first opened. Their windows float next to the app (they can't dock into its panes).

Set the helper up once per machine: make a Python environment with PySide6 in the default place, or point the app at one you already have.

python3 -m venv ~/.cache/opencomp/qt-venv
~/.cache/opencomp/qt-venv/bin/pip install PySide6

OPENCOMP_QT_PYTHON (a Python that has PySide6 or PySide2) or OPENCOMP_QT_VENV (a venv folder) pick another one. PySide6 is LGPL; it is installed separately and loaded, unmodified, by that separate process.

What works unchanged:

  • import nuke inside the tool: every call goes to the backend's nuke (nuke.selectedNode(), node['size'].setValue(7), nuke.createNode, nuke.nodes.Write(), with nuke.Undo():, nuke.message...). Nodes and knobs come back as live handles. Changes are undoable and the Node Graph, Properties and Viewer follow.
  • nuke.addKnobChanged(self.refresh) and the other callbacks: the backend calls back into the tool on its Qt thread (after the event, not waiting for the tool).
  • nukescripts.panels.registerWidgetAsPanel('studio.tools.MyPanel', 'My Panel', 'com.studio.MyPanel'): the tool gets an item in the pane menu; create=True opens it at once.
  • A menu command that builds and shows a Qt dialog (mytool.show_dialog() doing MyDialog().show()) opens it in the helper.
  • menu.py can import Qt tool modules: in the backend, PySide2 / PySide6 / Qt.py imports load stand-in modules so the class definitions work, and the widget is only ever built in the helper.
  • nuke.executeInMainThread inside a tool runs on the tool's Qt thread.

Limits: a tool's windows can't dock into the app; a tool can't subclass the backend's nuke classes (for example class P(nukescripts.PythonPanel) inside a Qt tool: keep that in menu.py); PyCustom_Knob widgets inside node panels are not shown; callbacks registered from a tool can't return a value to the backend (so a filenameFilter belongs in init.py).

Custom nodes

A studio node is a Python class and a GLSL fragment shader in an oc_nodes/ folder of any plugin directory. No change to the app is needed. Example:

# oc_nodes/studio_tint.py
import bpy
from opencomp_core import plugins
from opencomp_core.gpu_pipeline.executor import evaluate_shader
from opencomp_core.nodes.base import OpenCompNode
from opencomp_core.nodes.common import CommonKnobs, add_common_sockets, process


class StudioTintNode(CommonKnobs, OpenCompNode):
    """Multiply the colour by a tint."""
    bl_idname = "OC_N_studio_tint"                  # always OC_N_<name>
    bl_label = "StudioTint"

    tint: bpy.props.FloatVectorProperty(name="tint", size=3, default=(1.0, 0.8, 0.6))
    amount: bpy.props.FloatProperty(name="amount", default=1.0, soft_min=0.0, soft_max=1.0)

    def init(self, context):
        super().init(context)
        add_common_sockets(self)                    # image + mask in, image out

    def evaluate_image(self, texture_pool):
        u = {"u_tint": list(self.tint), "u_amount": float(self.amount)}
        return process(self, texture_pool,
                       lambda tex: evaluate_shader("studio_tint.frag", tex, u, texture_pool))


def register():
    plugins.register_node(StudioTintNode, label="StudioTint", category="Studio")
// oc_nodes/studio_tint.frag
uniform sampler2D u_image;
uniform vec3      u_tint;
uniform float     u_amount;
in  vec2 v_uv;
out vec4 out_color;

void main() {
    vec4 src = texelFetch(u_image, ivec2(gl_FragCoord.xy), 0);
    out_color = vec4(mix(src.rgb, src.rgb * u_tint, u_amount), src.a);
}

plugins.register_node(cls, label, category, nk_class=label):

  • registers the class and puts it in the node menu (Tab menu and left toolbar) under category;
  • maps the Nuke class name, so nuke.createNode('StudioTint'), nuke.nodes.StudioTint(tint=[1, .5, .25]) and .nk import / export work; knob names are the property names;
  • the .frag next to the module is found by name.

Rules (the same as for the built-in nodes): properties use a colon (tint: bpy.props...), never =; the pipeline is scene-linear float, never clamp; get textures only through evaluate_shader / the texture pool; return None on failure; uniforms must match their GLSL types exactly (a vec3 takes 3 floats). CommonKnobs + process() give the node Nuke's channels / mask / mix knobs. The Properties panel is laid out from the knob values.

Gizmos and Groups

.gizmo files are found in every plugin folder (and its sub-folders), ~/.nuke, ~/.opencomp/gizmos and NUKE_PATH, and appear in the Tab menu. A gizmo's own knobChanged / onCreate runs for each instance, so gizmo panels that show / hide knobs work. User knobs (addUserKnob, node.addKnob(nuke.Double_Knob(...))) appear in the Properties panel. More in .nk import and export.

OFX and BlinkScript

OpenFX image plugins become nodes in the Tab menu and on the OFX toolbar button; they run in their own process. BlinkScript kernels are translated to GLSL. See BlinkScript and OFX.

MCP

The backend serves the Model Context Protocol on your machine, so agents and pipeline services can read the graph, render frames and (with the artist's approval) edit the comp. See MCP and the assistant.

ShotGrid Toolkit

tk-nuke is not installed or tested. What it would need is listed in docs/notes/qt-bridge.md (a tk engine that runs in the Qt helper, with its scene-operation, publish and Write-node hooks going through the bridged nuke).

On this page