Gaffer’s Box node is a staple building block sharing chunks of node graph (via the Reference system), or quickly building tools via expressions and promoted plugs.
The Gaffer docs have a page covering Box nodes and how to use them. In this post we take a quick look at how to re-create a simple GroundPlane tool made in the UI, using only Gaffer’s python API.
warning
Introduction
For those reading who have used the Plane node. Its ‘z-up’ orientation is less than helpful if you just want a bit of ground for some reason or another. It is also tiny.
Here we’re going to make a simple box that encapsulates the network shown to the left so that you can add a plane with the right orientation to your scene. Exposing a few useful plugs to make it easy to tweak.
The script
All of the code below can be run directly in the Python Editor. Let start first by importing a few pre-requisites we’ll need later:
import Gaffer
import GafferScene
import imath
Making the Box
The box node, as with all Gaffer nodes, is made by literally constructing an instance of the Box
class and adding it to the graph. In this script, we’ll just put it at the root. In a real tool, you’d probably wan’t to use GafferUI.EditMenu.scope()
to get the root visible in the user’s Graph Editor.
box = Gaffer.Box( "GroundPlane" )
root.addChild( box )
tip
parent["NodeName"]
syntax. The latter works in simple cases, but if there is already a node with that name, the new one will be renamed, so the sub-script syntax would give the existing node, not your new one!The internal network
We need a Plane, to set some non-default values so it’s more useful, freeze it’s transform and use a Parent node to insert it into the incoming scene. Make these nodes, connect them up and add them to the Box:
plane = GafferScene.Plane( "Plane" )
plane["transform"]["rotate"].setValue( imath.V3f( -90, 0, 0 ) )
plane["dimensions"].setValue( imath.V2f( 100, 100 ) )
plane["name"].setValue( "ground" )
freeze = GafferScene.FreezeTransform( "FreezeTransform" )
# Note, don't confuse 'parent' here with the built-in variable
# available in Python expressions. This is just a local holding
# our 'Parent' node (I should have used a less confusing example!)
parent = GafferScene.Parent( "Parent" )
parent["parent"].setValue("/")
freeze["in"].setInput( plane["out"] )
parent["child"].setInput( freeze["out"] )
box.addChild( plane )
box.addChild( freeze )
box.addChild( parent )
Promoting plugs
We now have our network inside the box, but the box has no input or output. We can use the Gaffer.BoxIO
node’s static promote
method to promote the in/out plugs on the Parent
node, as a user might do using the right-click menus in the UI:
Gaffer.BoxIO.promote( parent["in"] )
Gaffer.BoxIO.promote( parent["out"] )
You can use exactly the same function to promote plugs that appear in the Node Editor instead of in the Graph Editor (they’re all just plugs after all). BoxIO.promote
preserves the plugs existing metadata, which is what controls where they’re presented, so it knows whether they should be on the top or bottom or sides of the box.
Gaffer.BoxIO.promote( plane["name"] )
Gaffer.BoxIO.promote( parent["parent"] )
Gaffer.BoxIO.promote( plane["divisions"] )
Gaffer.BoxIO.promote( plane["dimensions"] )
The promote()
method takes care of creating, connecting and configuring BoxIn
/ BoxOut
nodes and plugs on the Box itself. You don’t need to manually manage these.
You should now have a functional Box just like the one made in the UI.
Disabling the node (a.k.a finding inputs and outputs).
In order to allow a user to disable/bypass the Box, we need to provide a pass-through connection (explained here) that defines what to output when the Box is disabled. To do this, we’ll need to get the BoxIn
and BoxOut
nodes so we can connect the boxOutNode["passThrough"]
plug to the boxInNode["out"]
plug.
info
"out"
on the BoxIn
node? Don’t you mean "in"
?To understand this, we’ll have to look at a few details of how Box nodes work. A box node has in/out plugs. When you look at the box node from the outside you are looking at plugs on the box node itself. But you also want to see them when you’re looking at the graph inside the box so you can hook things up.
As its Gaffer, and we try to keep the fundamentals simple and consistent. Rather than invent something new and esoteric, the
BoxIO
code you used earlier creates child nodes that are either BoxIn
or BoxOut
to represent the box’s input/output plugs when viewing the graph inside the box in the Graph Editor and connects these from/to the box’s actual inputs and outputs so data can flow from outside, to inside and back again, ie:As they’re
__
prefixed (ie: private plugs), you don’t see these ‘bridging’ plugs and connections in the UI (it’d just be confusing too).The name of these internal nodes also determines the name of the plug on the outside of the box – which works well as these plug names need to be unique.
So, you need the
out
plug of the BoxIn
node as thats the plug that’s visible when looking at the inside of the box, that carries the data from the in
plug on the outside of the box. Back, to connecting up the passthrough. There are a couple of ways to find the plugs to do this.
Getting the i/o nodes for an existing box
In this simple case, the Box node in
plug is connected to its child BoxIn
node, and it’s out
plug is connected from it’s child BoxOut
node. A such, you can use the following:
boxInNode = box["in"].outputs()[0].node()
boxOutNode = box["out"].getInput().node()
boxOutNode["passThrough"].setInput( boxInNode["out"] )
A safer route
The above works, but relies on knowing the correct names for the input/output plugs on the box. If you’ve promoted several inputs and outputs, they have to have unique names so they might not be what you think.
When you promote a plug using Gaffer.BoxIO.promote()
it returns the corresponding plug on the outside of the box. So we could amend our promotion code to the following:
boxInPlug = Gaffer.BoxIO.promote( parent["in"] )
boxOutPlug = Gaffer.BoxIO.promote( parent["out"] )
# Add a passthrough
boxOutNode = boxOutPlug.getInput().node()
boxInNode = boxInPlug.outputs()[0].node()
boxOutNode["passThrough"].setInput( boxInNode["out"] )
You could also go backwards, from the parent
node:
boxOutNode = parent["out"].outputs()[0].node()
boxInNode = parent["in"].getInput().node()
Box Icons
By default, Box nodes are drawn with a ‘box’ icon. When you change this in the UI Editor, it simply edits the node’s metadata. You can do the same yourself if you want to remove (or change) the icon:
# remove
Gaffer.Metadata.registerValue( box, 'icon', None )
# change
Gaffer.Metadata.registerValue( box, 'icon', 'grid.png' )
NOTE: Images specified by a relative path need to be on $GAFFER_IMAGE_PATHS.
Useful links
- Box node introduction
- Working with the Node Graph scripting tutorial
- The
BoxIO
module - Finding the ‘current’ graph root with
GafferUI.EditMenu.scope()
The final script
import Gaffer
import GafferScene
import imath
# The box
box = Gaffer.Box( "GroundPlane" )
Gaffer.Metadata.registerValue( box, 'icon', None )
root.addChild( box )
# Internal network
plane = GafferScene.Plane( "Plane" )
plane["transform"]["rotate"].setValue( imath.V3f( -90, 0, 0 ) )
plane["dimensions"].setValue( imath.V2f( 100, 100 ) )
plane["name"].setValue( "ground" )
freeze = GafferScene.FreezeTransform( "FreezeTransform" )
parent = GafferScene.Parent( "Parent" )
parent["parent"].setValue("/")
freeze["in"].setInput( plane["out"] )
parent["child"].setInput( freeze["out"] )
box.addChild( plane )
box.addChild( freeze )
box.addChild( parent )
# Promote i/o
boxInPlug = Gaffer.BoxIO.promote( parent["in"] )
boxOutPlug = Gaffer.BoxIO.promote( parent["out"] )
# Promote useful Node Editor plugs
Gaffer.BoxIO.promote( plane["name"] )
Gaffer.BoxIO.promote( parent["parent"] )
Gaffer.BoxIO.promote( plane["divisions"] )
Gaffer.BoxIO.promote( plane["dimensions"] )
# Add a passthrough
boxOutNode = boxOutPlug.getInput().node()
boxInNode = boxInPlug.outputs()[0].node()
boxOutNode["passThrough"].setInput( boxInNode["out"] )