Scripting Guide
Contents
- 1 Introduction
- 2 User Interface Scripting
- 3 Hooks
- 4 Silhouette Coordinate System
- 5 Scripting Reference
- 6 Scripting Examples
- 6.1 Toggle Shape Opacity Keybind
- 6.2 Toggle Paint Opacity Keybind
- 6.3 Toggle Layer Visibility
- 6.4 Rename Layer based on first child snippet
- 6.5 Count up all of the shapes and print the result
- 6.6 Find the previous key to the current player frame
- 6.7 Export 4 Point Trackers as a Nuke corner-pin script
- 6.8 Toggle shapes on/off
- 6.9 Use a shape exporter from Python
Introduction
Silhouette has an embedded Python interpreter that can be used to control key bindings and invoke Actions. When Silhouette starts up, it executes the startup.py script, which in turn runs the keybinds.py script. There is a search path when looking for these scripts, so users can modify their scripts without modifying the default scripts.
The Silhouette scripting functionality is implemented in the fx module.
Script Search Path
Silhouette has multiple search paths where it looks for scripts. First, it checks in $(SFX_SCRIPT_PATH), then in $(SFX_USER_PATH)/scripts. These can easily be overridden on a per-user basis by setting the environment variables. Finally Silhouette looks in $(SFX_RESOURCE_PATH)/scripts, where SFX_RESOURCE_PATH is the path to the Silhouette resources directory on Windows and Linux, and the Resources bundle folder on Macintosh OSX.
Platform | Path |
---|---|
Linux | /opt/BorisFX/Silhouette <version>/resources |
OS X | /Applications/BorisFX/Silhouette <version>/Silhouette.app/Contents/Resources |
Windows | C:\Program Files\BorisFX\Silhouette <version>\resources |
To add custom scripts/actions, define SFX_SCRIPT_PATH to point to a directory with a Python module called sfxsite or sfxuser. Alternatively, set the SFX_SCRIPT_IMPORTS to one or more paths (delimited by : or ;) that contain an __init__.py file and individual files you want imported.
For instance, if you want to predefine some preferences, you could create a file called "default_prefs.py" and place it in a directory called "sfx_site", and set SFX_SCRIPT_IMPORTS to point there. Note you *must* also place a copy of the __init__.py file found in Silhouette's scripts/actions directory.
Additional paths can also be added in the Scripting Preferences page.
Scripting Console
The Silhouette Console can be used to enter interactive scripts. This is useful for running quick code snippets or testing scripting commands. The fx module is imported automatically in the interactive console.
For example, load a Project and then open the Console tab. Press Enter until you see a >> prompt, and type the following command:
print(activeProject().label)
The current project name will be printed to the console.
Script Editor
More complex scripts can be developed and tested in the Script Editor. See the Silhouette User Guide for more information.
User Interface Scripting
Key binds
Most of the UI functions in Silhouette can be controlled by custom keybinds, which are normally defined in keybinds.py. Keys can be configured to perform simple, global operations such as activating a control, or complex operations such as toggling states or cycling through modes each time a key is pressed. Binds can also examine the current node and tool to perform different operations depending on the current state.
The easiest way to get familiar with key binding is to examine the default keybinds file. More information on the bind functions can be found in the Scripting Reference.
Actions
Silhouette Actions are commands, written as scripts, that show up in the Actions menu. Actions can operate on the Silhouette object model with proper undo/redo behavior, execute rendering loops, import/export data using custom formats, and other powerful operations.
An Action is implemented as a Python class that is subclassed from fx.Action. It must implement two functions, available() and execute().
available() is called to determine if the Action can be executed with the current Silhouette state. If an Action requires one or more selected objects, for example, it should check the selection to make sure there are enough selected objects, and return True if everything is order or False otherwise. An assertion can also be used to return a message that is displayed in the status bar when the action is hovered over, in the event the action is not available.
execute() is called to actually perform the command. The action should use an undo block if it manipulates the object model, or it can clone the project and work from the copy if it needs to manipulate some state for rendering.
Actions can create their own sub-menus by prefixing their label with the sub-menu name followed by the '|' character.
The easiest way to get familiar with actions is to examine the bundled actions, found in the scripts/actions directory. Many actions also make use of some helper functions and classes found in the scripts/tools directory.
More information on the various Action functions can be found in the Scripting Reference.
Hooks
Hooks can be used to perform various operations when certain events happen. Hooks can be very powerful tools for integrating Silhouette with other tools. As of 5.2, the rv integration is implemented almost entirely with hooks and globals.
The available hooks are:
Hook name | Description | Added |
---|---|---|
startupComplete | called after startup initialization finishes | |
quit | called before shutting down | 5.1 |
project_selected | called when then active project changes | |
node_selected | called when the active node changes | |
post_load | called after a Project is loaded | |
pre_save | called just before a Project is saved | |
post_save | called just after a Project is saved | |
pre_render | called just before rendering begins | |
post_render | called just after rendering finishes | |
post_save_frame(frame, path, type) | called just after a frame is saved during rendering | |
frameChanged | called when the frame changes or playback stops | 5.2 |
selectionChanged | called when the object selection changes | 5.2 |
object_created |
called after a new object is created in the UI. The object is passed as the argument. | 6.0 |
attach_pipe |
called after a new pipe connects two nodes | 6.0 |
remove_pipe |
called just before a pipe is removed | 6.0 |
output_node |
called after an Output node finishes writing all of its parts. A meta-data dictionary of useful information is passed. | 6.0 |
object_deleted |
called the first time the user deletes an object in the UI. The object is passed as the argument. | 7.0 |
object_added |
called when an object is added to the object model. The object is passed as the argument. | 7.0 |
object_removed |
called when an object is removed from the object model. The object is passed as the argument. | 7.0 |
Hooks are assigned by editing the fx.hooks dictionary, usually in startup.py.
As of 5.2, a hook value can be a callable (ie. function) or List of callables. To assist in adding multiple hook functions to a specific hook, the add() function from the hook.py utility module should be used:
import hook
hook.add("post_render", my_post_render_function)
Note the important difference between object_created and object_added, as well as object_deleted and object_removed. object_created and object_deleted are called when the user creates or deletes an object for the first time, and they are only triggered once. object_added and object_removed are part of the undo system and will be called every time an object is added to or removed from the object model.
Drop Hooks
Register one or more drop hooks with the trees object. A drop hook is triggered when a specific mime type is dropped into the Trees view. For example, to respond to files from the desktop being dropped, use mime type "text/uri-list".
def drop_hook(type, data):
print(type, data)
trees.registerDropHook("text/plain", drop_hook)
Hook Example: node_selected
# set the view mode to 'Foreground' each time a 'Roto' node is selected
def nodeSelected():
node = activeNode()
if node != None and node.type == "RotoNode":
fx.viewer.setViewMode(1)
fx.hooks["node_selected"] = nodeSelected
Hook Example: post_load
# parse the project name for a version field, and set an environment
# variable with the version number. Later, the variable can be used
# in the output format specification
def post_load():
p = fx.activeProject()
if p:
name = p.path
name = os.path.basename(name)
name = os.path.splitext(name)[0]
version = name.split('_')[-1]
if version.startswith('v'):
os.environ["PROJECT_VERSION"] = version
fx.hooks.add("post_load", post_load)
Silhouette Coordinate System
Silhouette uses a size-independent coordinate system for Shapes, Trackers, and other objects that are positioned within a Session. In theory, this allows the size of plates to change without affecting the positions of objects. The world coordinate system is from -(image_width/image_height*pixel_aspect)/2 -> (image_width/image_height*pixel_aspect)/2 on the X axis and -0.5 -> 0.5 on the Y axis, where 0.0 is in the center of the session and -(image_width/image_height*pixel_aspect)/2, -0.5 is the top-left corner of the session and (image_width/image_height*pixel_aspect)/2, 0.5 is the bottom right of the session. Note that even if a plate is larger or smaller than the session or has a DOD that is radically different from the session's ROI, object coordinates are still based on the session world coordinate system.
A shape control point coordinate, or tracker position for example, can be mapped from world (image size independent) coordinates to pixel coordinates with the following equation:
aspect = session_width / session_height * session_pixel_aspect
new_p.x = (p.x / aspect + 0.5) * session_width
new_p.y = (p.y + 0.5) * session_height
As a convenience, the node.worldToImageTransform and node.imageToWorldTransform matrices can be used directly:
new_p = node.worldToImageTransform * p
p = node.imageToWorldTransform * new_p
Scripting Reference
Use the Silhouette Module Reference for full reference information for scripting with Silhouette objects.
Scripting Examples
Toggle Shape Opacity Keybind
Here is an example of how to bind a key to toggle the opacity of selected shapes on/off:
def toggleOpacity():
s = fx.selection()
fx.beginUndo("Toggle Opacity")
frame = fx.player.frame
for shape in s:
opacity = shape.property("opacity")
if opacity:
value = opacity.getValue(frame)
value = 100.0 - value
if opacity.constant:
opacity.constant = False
opacity.setValue(value, frame)
fx.endUndo()
fx.bind("Alt+o", toggleOpacity)
Toggle Paint Opacity Keybind
Here is an example of how to bind a key to cycle the paint opacity between 0 and 100%:
def togglePaintOpacity():
if fx.activeNode().type == "PaintNode":
fx.paint.opacity = 1.0 - fx.paint.opacity
fx.bind("Alt+z", togglePaintOpacity)
Toggle Layer Visibility
This snippet binds Shift+Alt+v to a function that toggles the selected Layers on/off.
def toggleLayerVisibility():
beginUndo("Toggle Layer Visibility")
s = selection()
for o in s:
if isinstance(o, Layer):
o.visible = not o.visible
endUndo()
bind("Shift+Alt+v", toggleLayerVisibility)
Rename Layer based on first child snippet
This snippet is designed to be pasted into the Script Editor. It can easily be turned into an Action.
from tools.objectIterator import ObjectIterator
class LayerIterator(ObjectIterator):
def visit(self, object):
children = object.children
if children:
object.label = object.children[0].label
ObjectIterator.visit(self, object)
node = activeNode()
children = node.children
li = LayerIterator()
beginUndo("Relabel Layers")
li.iterate(children)
endUndo()
Count up all of the shapes and print the result
This snippet will iterate all of the objects in the current node and add up all the Shapes.
from fx import *
from tools.objectIterator import ObjectIterator
class ShapeCounter(ObjectIterator):
def __init__(self):
self.count = 0
def visit(self, object):
if isinstance(object, Shape):
self.count = self.count + 1
ObjectIterator.visit(self, object)
node = activeNode()
c = ShapeCounter()
c.iterate(node.children)
print c.count
Find the previous key to the current player frame
from fx import *
def findPrevKeyTime(prop, time):
keys = prop.keys
kt = 0.0
for k in keys:
if k >= time:
break
kt = k
return kt
# example here
shape = selection()[0]
path = shape.property("path")
print findPrevKeyTime(path, player.frame)
Note that as of 5.2, new property key-frame functions are included: getPrevKeyTime(), getNextKeyTime(), getKeyIndex(), isKey().
Export 4 Point Trackers as a Nuke corner-pin script
Parts of this script, such as actually determining that there are 4 selected Point Trackers, are left to the reader. It also exports the session work range but can easily be changed to export the full session range or some other range of key-frames.
Note each tracker point is transformed through two matrices - the first is the object matrix which encapsulates all of the transforms for the object, such as if it was in a Layer that was also transformed. Secondly it is transformed by the node's world-to-image transform which projects the point from world space into image pixel coordinates. Note that we flipped this transform earlier to account for Nuke's coordinate system being bottom-up.
from fx import *
def exportCornerPin(node, range, trackers, path):
first = range[0]
last = range[1]
xform = node.worldToImageTransform
# flip upside down per Nuke system
xform.scale(Point3D(1.0, -1.0, 1.0))
file = open(path, "w")
file.write("set cut_paste_input [stack 0]\n")
file.write("version 5.2100\n")
file.write("push $cut_paste_input\n")
file.write("Tracker {\n")
counter = 1
for t in trackers:
points = []
frame = first
while frame <= last:
p = t.getPosition(frame)
matrix = t.getTransform(frame)
p = matrix * p
p = xform * p
points.append(p)
frame = frame + 1.0
file.write(" enable%d true\n" % (counter))
file.write(" track%d {{" % (counter))
file.write(" curve x1 ")
for p in points:
file.write("%f " % (p.x))
file.write("} {curve x1 ")
for p in points:
file.write("%f " % (p.y))
file.write("}}\n")
counter = counter + 1
file.write(" name CornerPin\n")
file.write(" label \"Source: SilhouetteFX\"\n")
file.write("}\n")
path = "output.nk"
trackers = selection()
# make sure trackers contains 4 trackers
if len(trackers) == 4:
node = activeNode()
session = activeSession()
exportCornerPin(node, session.workRange, trackers, path)
Toggle shapes on/off
This snippet can be added to the keybinds.py file and binds the 'g' key to toggle shape outlines on/off. This can be used while editing to temporarily hide the outlines.
shape_color = prefs["shape.color"]
def toggleShapes():
from fx import Color
global shape_color
color = prefs["shape.color"]
if color.a == 0.0:
color.a = shape_color.a
else:
color.a = 0.0
prefs["shape.color"] = color
fx.bind("g", toggleShapes)
Use a shape exporter from Python
# fx.io_modules contains the list of inputer/exporter modules
import fx
exporter = io_modules["Nuke 6.2+ Shapes"]
if exporter.can_export:
exporter.export(output_path)