Extending Oink (for TDs)
Studio pipeline guide: custom nodes, Python panels, menus, callbacks, init.py / menu.py, PySide tools, gizmos, OFX and MCP.
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:
| Step | What |
|---|---|
| 1 | Every plugin folder's init.py: ~/.opencomp (or $OPENCOMP_USER_DIR), then each folder of NUKE_PATH, then each of OPENCOMP_PLUGIN_PATH |
| 2 | nuke.pluginAddPath(dir) inside an init.py adds dir: its own init.py runs at once, its menu.py later |
| 3 | Every menu.py, in the same order (the app always has a UI, like Nuke's GUI mode) |
| 4 | Every oc_nodes/ folder on the plugin path: custom nodes |
NUKE_PATHworks exactly as in Nuke, so point it at your existing studio folder.OPENCOMP_PLUGIN_PATHis for folders only this app should read.__file__is set whileinit.py/menu.pyrun, soos.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")Menus
| Menu | Where 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
| Function | Fires |
|---|---|
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 |
addOnDestroy | a 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 / addAfterFrameRender | around every render of a Write (the Render dialog, nuke.execute) |
addOnScriptLoad / addOnScriptSave / addOnScriptClose | File > 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.
| Call | Shows |
|---|---|
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, getColor | small 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 PySide6OPENCOMP_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 nukeinside the tool: every call goes to the backend'snuke(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=Trueopens it at once.- A menu command that builds and shows a Qt dialog (
mytool.show_dialog()doingMyDialog().show()) opens it in the helper. menu.pycan 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.executeInMainThreadinside 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
.fragnext 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).