How To Write a Package SuperTool

A ‘Package SuperTool’ is a specialized type of SuperTool that allows for the easy creation and editing of ‘packages’, where a package is simply a scene graph location with a predefined set of attributes. Packages can be defined to represent lights, cameras, materials or geometry — anything with a complex enough attribute set to benefit from the streamlined workflows that Package SuperTools offer.

The most prominent example of a Package SuperTool is the GafferThree node. A GafferThree node enables users to create lights, rigs and template materials, assign shaders, mute or solo lights, group lights under rigs and apply transforms. It defines a LightPackage, a RigPackage and a TemplateMaterialPackage, which enable users to create lights, rigs and template materials. It also supports editing of data from the incoming scene, via ‘adoption’ of locations. For example, lights which were defined in an upstream node can be ‘adopted’ using a LightEditPackage.

../../../../_images/GafferThree.png

A screenshot showing the Scene Graph tab and the Parameters tab, with a GafferThree node flagged for viewing and editing.

This page is a guide to writing your own Package SuperTool using the PackageSuperToolAPI. It focuses on two example SuperTools that ship with Katana: PonyFarm, which provides a simple but functional SuperTool for creating and manipulating geometry (ponies and cows), and ExamplePackageSuperTool, which demonstrates a minimal implementation.

See also

The PonyFarm Example

Katana comes with a simple Package SuperTool that demonstrates how you can use the PackageSuperToolAPI to construct your own SuperTools that manage packages — the PonyFarm. This can be found in $KATANA_ROOT/plugins/Resources/Examples/SuperTools/PonyFarm. This example shows how, with minimal code, you can use the PackageSuperToolAPI to create Gaffer-like SuperTools that manage and manipulate arbitrary packages.

The PonyFarm SuperTool lets users create both ponies and cows, and group them into herds. The animals’ colors can be changed and randomized, and their transforms and ages can be set individually. Whilst it’s simplistic, it demonstrates some of the core functionality of the PackageSuperToolAPI without requiring too much code.

User Interface

After creating a PonyFarm node, editing it will display a tree view in the Parameters tab. This tree view displays scene graph locations beneath the node’s root location. Each of these scene graph locations represents either a local package, or a location from the incoming scene. Locations from the incoming scene are only shown if the ‘Show Incoming Scene’ option is enabled on the PonyFarm node (available in its cog menu). Locations from the incoming scene are displayed in light gray italic text to differentiate them from local packages.

In the screenshot, the location named ‘upstream’ has come from the incoming scene, whilst the other locations are packages which were created by this PonyFarm node instance.

../../../../_images/PonyFarm1.png

A screenshot showing the Scene Graph tab, Viewer tab and Parameters tab with a PonyFarm node flagged for viewing and editing.

PonyFarm API

Since the PonyFarm is implemented using PackageSuperToolAPI base classes, there are a number of operations we can perform on the packages through the Python API. For example:

# Create a new PonyFarm node
ponyFarmNode = NodegraphAPI.CreateNode("PonyFarm", NodegraphAPI.GetRootNode())

# Get the root package for the node
rootPackage = ponyFarmNode.getRootPackage()

# Add a cow and a herd
rootPackage.createChildPackage("CowPackage")
rootPackage.createChildPackage("HerdPackage")

# Fetch an existing package by name
cowPackage = ponyFarmNode.getPackageForPath('/root/world/geo/farm/cow')

# Rename the package
cowPackage.setName("anotherCow")

# Reparent the cow package under the herd
herdPackage = ponyFarmNode.getPackageForPath('/root/world/geo/farm/herd')
herdPackage.adoptPackage(cowPackage)

# Create a new pony package
newPonyPackage = rootPackage.createChildPackage("PonyPackage")

# Print out the scene graph location of the newly created package
print(newPonyPackage.getLocationPath())  # /root/world/geo/farm/pony

The PonyPackage class, defined in PonyPackage.py extends the above API to provide its own specialized functions:

# Use the pony package's API to set and print the age of the pony
newPonyPackage.setAge(65)
print newPonyPackage.getAge() # 65

Creating Your Own SuperTool Using the PackageSuperToolAPI

In this section we’ll explain the individual components that are required when creating a Package SuperTool. We’ll create a minimal example SuperTool using these components. The complete code for this example Package SuperTool can be found in $KATANA_ROOT/plugins/Resources/Examples/SuperTools/ExamplePackageSuperTool.

Components specific to Package SuperTools include the following:

  • Package classes represent types of package that the SuperTool can create and manage. GafferThree has three of these package types: LightPackage, RigPackage and TemplateMaterialPackage. PackageSuperToolAPI provides a number of base class implementations for packages, discussed below.

  • UI Delegate classes bridge the gap between the packages and their UI representation, for use in the Editor. The Editor defines which tabs should be displayed below the tree view displaying all the packages, and a registered UI Delegate is used to turn the currently selected package into a ValuePolicy that can be used to drive the tab UI. UI Delegates also define the keyboard shortcuts which are registered for adding packages.

Node Class

In a Package SuperTool, the node class must derive from PackageSuperToolAPI.BaseNode.BaseNode. This base class provides the internal node graph required for package management, so the Package SuperTool node class definition itself is typically small. The only real requirement is to implement the following abstract methods:

Other than that, all other node behaviour is taken care of by BaseNode, although you can override other methods if you wish to specialize particular behaviour.

Here is a minimal implementation of an example Node class:

from PackageSuperToolAPI import BaseNode

class ExampleNode(BaseNode.BaseNode):
    @classmethod
    def getSuperToolName(cls):
        return "ExamplePackageSuperTool"

    @classmethod
    def getDefaultRootLocation(cls):
        return '/root/world/geo/example'

    @classmethod
    def getItemListAttributeName(cls):
        return "exampleSuperTools"

Editor Class

The job of the Editor is to provide an interface through which users can interact with the SuperTool’s package hierarchy, in the form of a QtWidgets.QWidget. In a Package SuperTool, the editor class must derive from PackageSuperToolAPI.BaseNode.BaseEditor. This BaseEditor class provides a large amount of functionality, but similar to BaseNode, it expects subclasses to implement a small number of abstract methods. The only two that are explicitly required to be re-implemented are:

There are many other methods that can be overridden to customize the Editor’s behaviour — see the documentation for PackageSuperToolAPI.BaseEditor.BaseEditor for full details, and Scene Graph View for information on customizing the Editor’s SceneGraphView widget.

Here is a minimal implementation of an example Editor class:

from PackageSuperToolAPI import BaseEditor
from UI4.Widgets.SceneGraphView import ColumnDataType


class ExampleEditor(BaseEditor.BaseEditor):

    @classmethod
    def getSuperToolName(cls):
        return "ExamplePackageSuperTool"

    @classmethod
    def getTabNames(cls):
        return ["ExampleTab1", "ExampleTab2"]

    def setupSceneGraphViewColumns(self):
        exampleColumn = self.getSceneGraphView().addColumn('Example')
        exampleColumn.setAttributeName('testAttr')
        exampleColumn.setDataType(ColumnDataType.Number)
        exampleColumn.setEditable(True)

The Packages

The Package types that you define as part of your SuperTool provide the majority of the functionality of the SuperTool. They are responsible for constructing the nodes within your SuperTool, and setting up their parameters, in order to create a particular scene graph location.

To define your custom Package types, there is a choice of different base classes from which you can subclass. The key base classes are:

  • PackageSuperToolAPI.Packages.Package. The ‘lowest’ base class that all other packages derive from. All package types are expected to directly or indirectly subclass this. In the PonyFarm example, the PonyPackage directly subclasses Package.

  • PackageSuperToolAPI.Packages.GroupPackage. Subclasses Package, adding functionality for creating child packages. In the PonyFarm example, HerdPackage is a subclass of GroupPackage.

  • PackageSuperToolAPI.Packages.EditPackage. Subclasses Package, adding functionality for editing data from the incoming scene. In the PonyFarm example, editing of scene graph data created by a PonyPackage in an upstream PonyFarm node is facilitated by a PonyEditPackage.

There is only one function that should be defined when creating a new Package type: create().This is responsible for creating the node hierarchy for the package. The constructor for the Package class must take a single argument - the package node. The package node is a node that groups/encapsulates all Nodes that relate to the creation of a scene graph location. For example, if a package requires a LightCreate node followed by a Material node, these two nodes would be grouped under a single Group or GroupStack node. This encapsulating Group or GroupStack node would be the package node for this package.

Since Package’s constructor takes existing node instance, the create() classmethod must create this package node and all of its contents. create() should then construct an instance of the Package by passing the package Node into its constructor. A minimal example of this is as follows:

class ExamplePackage(Packages.Package):

@classmethod
def create(cls, enclosingNode, locationPath):
    packageNode = NodegraphAPI.CreateNode("Group", enclosingNode)
    # ... add nodes inside packageNode ...
    return cls(packageNode)

The Package base class’ constructor is already defined to expect a single Node argument. So all you need to do in a derived class is provide this create() function. Once the package is constructed, you can retrieve a reference to this package Node through a call to package.getPackageNode(). There is a corresponding delete() instance method that is implemented on the Package base class — by default, this deletes the package Node (amongst some other things), but you can override this if you need to provide special cleanup behaviour.

For convenience, there is a classmethod on the base Package class that will create an encapsulating Group Node for your package: Package.createPackageGroupNode(enclosingNode, locationPath)

Where possible, you should use this in favor of a standard NodegraphAPI.CreateNode() call, as createPackageGroupNode() will also add the appropriate tagging information to the Node.

Below is a complete example of an example Package that defines create(). It creates a single AttributeSet node internally that does nothing but write out an attribute named ‘testAttr’.

from Katana import NodegraphAPI
from PackageSuperToolAPI import NodeUtils
from PackageSuperToolAPI import Packages


class ExamplePackage(Packages.Package):

    # The name of the package type for display in menus, etc
    DISPLAY_NAME = "ExamplePackage"
    # The initial name to use when creating new packages
    DEFAULT_NAME = 'examplePackage'

    @classmethod
    def create(cls, enclosingNode, locationPath):
        packageNode = cls.createPackageGroupNode(enclosingNode, locationPath)

        # An expression for the location path for the package
        locExpr = '=^/' + NodeUtils.GetPackageLocationParameterPath()

        # A LocationCreate node that will construct the output location
        locationCreate = NodegraphAPI.CreateNode("LocationCreate", packageNode)
        locationCreate.getParameter("locations.i0").setExpression(locExpr)

        # An AttributeSet node that will set an attribute on the location
        attrSet = NodegraphAPI.CreateNode("AttributeSet", packageNode)
        attrSet.getParameter('paths.i0').setExpression(locExpr)
        attrSet.getParameter('attributeName').setValue('testAttr', 0)
        attrSet.getParameter('attributeType').setValue('float', 0)
        attrSet.getParameter('numberValue.i0').setValue(1, 0)

        # Store some references to the internal nodes so we can reference them
        # later
        NodeUtils.AddNodeRef(packageNode, 'locationCreateKey', locationCreate)
        NodeUtils.AddNodeRef(packageNode, 'attrSetKey', attrSet)

        # Connect up the AttributeSet node within the Group
        nodesToAdd = [locationCreate, attrSet]
        NodeUtils.WireInlineNodes(packageNode, nodesToAdd)

        return cls(packageNode)

    def getOverrideNodeAndParameter(self, attrName):
        # Remap the given attribute name onto the parameter that controls its
        # value
        node = None
        overrideParameter = None
        if attrName == "testAttr":
            node = NodeUtils.GetRefNode(self.getPackageNode(), 'attrSetKey')
            overrideParameter = node.getParameter("numberValue.i0")

        return node, overrideParameter

# Register the package class for our SuperTool
Packages.RegisterPackageClass("ExamplePackageSuperTool", ExamplePackage)