On this page
This feature is still under development. The current functionality is unfinished and subject to change, and may have thin or no documentation. Please bear this in mind when using it.

APEX Script allows users to access and manipulate graph elements, add graph logic directly into another graph, and invoke other graphs from within a graph. See APEX Script for graph building for an introduction to APEX Script.

Specifying graph inputs and outputs

BindInput() and BindOutput() are special functions that define the inputs and outputs of a graph. See this example for how to use BindInput() and BindOutput().

The examples below show how to specify different types of graph inputs:

# Initialize float value
f = BindInput(3.1)

# Uninitialized string type
s: String = BindInput()

# Initialize to empty array
a = BindInput(DictArray())

# Default dictionary key-value pair: {'<key>': <value>}
d = BindInput({'abc': 1.5})

# Create multiple graph inputs
x, y, z = BindInput(2.6, 'test', Dict())

# Type conversion when initializing
i = BindInput(Int(Float(1.5)))

Note

If you don’t specify a type for the graph input, a warning will be raised. Only do the following if you know exactly how you will be using the variable:

a = BindInput()

The input port a that is defined in this way is not displayed in the APEX network view because its type is ambiguous.

You can rename the input and output ports on a graph using keyword arguments (arguments with the syntax <key>=<value>):

i: Int = BindInput(int_test=3)

x,y = BindInput(2, my_str='hello')

BindOutput(y, int_test=i)

Note

Keyword arguments must be placed after positional (non-keyword) arguments.

Rename graph inputs and outputs

Keyword arguments that contain __dot__ are replaced with '.'. This allows APEX Script to handle rigs with geometry names like Base.skel and Base.shp:

x = BindInput(Base__dot__shp=Geometry())

Base__dot__skel: Geometry = BindInput()

BindOutput(Base__dot__skel, Base__dot__shp=x)
Rig geometry names in graph inputs and outputs

Specifying dictionary inputs

In this example, we use values from two dictionaries as inputs to an APEX Script SOP:

Dictionary inputs to APEX Script SOP

Attribute Adjust Dictionary SOP

The Attribute Adjust Dictionary SOP defines a dictionary of input values. We set the following values for the two SOPs:

input_dictionary1

  • Attribute Name = parms1. By default, Attribute Class is set to Detail, so we have a detail attribute called parms1.

  • In the Set Values section, we set two dictionary entries:

    • Key = int1, Type = Integer, Value = 2

    • Key = abc, Type = String, Value = hi

input_dictionary2

  • Attribute Name = parms2

  • In the Set Values section:

    • Key = int2, Type = Integer, Value = 4

    • Key = abc, Type = String, Value = hi again

To view the new parms1 and parms2 dictionaries in the geometry spreadsheet:

  • Select one of the Attribute Adjust Dictionary SOPs.

  • At the top of a pane, click the New Tab icon and select New Pane Tab Type ▸ Inspectors ▸ Geometry Spreadsheet.

  • Select Detail on the top toolbar:

    parms1: {"abc": "hi", "int1": 2 }
    
    parms2: {"abc": "hi again", "int2": 4 }
    

APEX Script SOP

  1. We have the following code in the Snippet parameter:

    int1: Int = BindInput()
    int2: Int = BindInput()
    abc: String = BindInput()
    
    BindOutput(int1,int2,abc)
    

    Note

    If you don’t bind the output or do anything with the input values, the graph will not displayed any logic because nothing is being done with the inputs.

  2. Specify the input dictionaries. In the Invocation section:

    • Under Input1 Bindings, turn on Dictionary To Bind, and specify the dictionary connected to the 1st input of the APEX Script SOP, in our case, parms1 (from the Attribute Adjust Dictionary SOP’s Attribute Name parameter).

    • Under Input2 Bindings, turn on Dictionary To Bind, and specify the dictionary connected to the 2nd input of the APEX Script SOP, in our case, parms2.

    The APEX Script SOP looks in the input dictionaries, parms1 and parms2, and tries to find entries with keys int1, int2, and abc. For keys that exist in multiple dictionaries, the latter dictionary entry is used. In our case, both dictionaries have an entry with key abc, so the abc entry from the 2nd dictionary is used. In addition, a warning is raised on the APEX Script SOP.

  3. By default, the first entry of Output Dictionary Bindings is populated with the following:

    • Apex Outputs Group is the set to output. This is the name of the output node in the graph.

    • Output Attrib is set to parms. We can set this to any name for the output dictionary.

    To view the graph outputs in the geometry spreadsheet:

    • In the geometry spreadsheet, beside the node name, select Invoked from the drop-down menu.

    • Click Detail on the top toolbar. The graph outputs are stored in the dictionary specified in Output Attrib, in our case, parms:

      parms: {"abc": "hi again", "int1": 2, "int2": 4 }
      

Binding multiple outputs

We can set the graph to have multiple input and output nodes by using BindInput and BindOutput functions with the special function argument __name to name nodes:

  1. In an APEX Script SOP, we have the following code in the Snippet parameter:

    # Default 'parms' name used
    f = BindInput(3.1)
    
    # 2nd input node is named 'input2'
    s: String = BindInput('abc', __name='input2')
    
    # Default 'output' name used
    BindOutput(f)
    
    # 2nd output node is named 'output2'
    BindOutput(s, __name='output2')
    
    Graph with multiple input and output nodes
  2. By default, the first entry of Output Dictionary Bindings is filled out with the following:

    • Apex Outputs Group is the set to output. This is the name of the output node in the graph.

    • Output Attrib is set to parms. We can set this to any name for the output dictionary.

    To view the graph outputs in the geometry spreadsheet:

    • In the geometry spreadsheet, beside the node name, select Invoked from the drop-down menu.

    • Click Detail on the top toolbar. Only the parms dictionary is displayed because we have only told the APEX Script SOP to output the values of the output node.

  3. To also output the output2 node values:

    • Click beside Output Dictionary Bindings.

    • For the new entry, set Apex Outputs Group to output2, and Output Attrib to, say, parms2.

    On the geometry spreadsheet, there are now two output dictionaries displayed, parms and parms2:

    parms:  {"f": 3.1 }
    
    parms2: {"s": "abc" }
    

Graph nodes and ports

APEX Script provides functions that allow you to perform various graph operations and access graph elements like nodes and ports. These functions are primarily used in rigging for building up the rig logic of a character. See the graph::* nodes for the graph operations that are available in APEX Script.

The syntax to find a graph node with a specific node name:

graph.<node_name>

The syntax to find a specific input or output port on a node:

graph.<node_name>.<port_name>_[in|out]

Connect two ports:

# Create graph
graph = ApexGraphHandle()

# Add two TransformObject nodes to graph
a = graph.addNode('test_a', 'TransformObject')
b = graph.addNode('test_b', 'TransformObject')

# Set port variables
p_a = graph.test_a.xform_out
p_b = graph.test_b.xform_in

# Connect ports
graph.addWire(p_a, p_b)

Find all nodes based on a pattern:

# Create graph
graph = ApexGraphHandle()

# Add two TransformObject nodes to graph
a = graph.addNode('test_a', 'TransformObject')
b = graph.addNode('test_b', 'TransformObject')

# Find all nodes that begin with 'test'
n = graph.findNodes('test*')

Find a port on a node:

# Create graph
graph = ApexGraphHandle()

# Add TransformObject node to graph
a = graph.addNode('test_a', 'TransformObject')

# Find input port 'xform' on node
p = graph.findPort(a, 'xform[in]')

Get data on nodes and ports (see function mappings for how to use the data() function):

# Create graph
graph = ApexGraphHandle()

# Add TransformObject node to graph
a = graph.addNode('test_a', 'TransformObject')

# Get node data
(a_name, a_callback, a_pos, a_color, a_parms) = a.data()

# Output node data
BindOutput(a_name, a_callback, a_pos, a_color, a_parms)

# 'xform' output port of TransformObject node
p = a.xform_out

# Get port data
(p_name, p_alias, p_datatype, p_porttype, p_isconnected, p_ispromoted) = p.data()

# Output port data
BindOutput(p_name, p_alias, p_datatype, p_porttype, p_isconnected, p_ispromoted)

See the list of ports returned from a function:

# x is an array of APEX port IDs
x = graph.matchPorts('*xform*')

# Initialize an array of strings
arr = StringArray()

# Add the port names to the array
for p in x:
    p_name = p.data()
    arr.append(p_name)

# Output the array of port names
BindOutput(arr)

Variadic ports

Variadic ports are ports that can take in multiple connections. As you make connections to a variadic port, subports are created on the node.

Variadic inputs

In APEX Script, variadic inputs are stored in a dictionary with the following key-value pair:

  • key - subport name

  • value - subport value

The example below adds three values. The first argument is a regular input, and the second argument is a variadic input stored as a dictionary with two entries. The subport names of the variadic input are explicitly set:

x = 3

sum = Add_Int(1, {'bbb': 2, 'ccc': x})
Named variadic input subports

If the variadic input is the last port on the node, there is no need to specify the subport names:

x = 3

sum = Add_Int(1, 2, x)

If no values are specified for the non-variadic ports, the variadic port name must be specified:

<function>(<variadic_port>={<subport1>:<value1>, <subport2>:<value2>, ...})

sum = Add_Int(b={'x': 4, 'y': 5})

Variadic outputs

To name the subports of a variadic output, use the keyword argument <variadic_arg_name>_out=[<subport_names>], where <subport_names> is an array of strings.

The example below gets the variadic outputs from IntBitMaskToBool:

input: Int = BindInput(8)

# Variadic output subports named 'aa', 'bb', 'cc', 'dd'
a,b,c,d = input.IntBitMaskToBool.v2_0(mask__out=['aa', 'bb', 'cc', 'dd'])

# Output subport values
BindOutput(input,a,b,c,d)
Named variadic output subports

If the keyword argument is not provided, the subport names are set to the variable names:

input: Int = BindInput(8)

# Variadic output subports named 'a', 'b', 'c', 'd'
a,b,c,d = input.IntBitMaskToBool.v2_0()

# Output subport values
BindOutput(input,a,b,c,d)
Variadic output subports set to APEX Script variable name

Dictionary entries can be of any type, so to extract the entries of a dictionary using dict::Extract, you must first specify the types of the variables you want to extract the dictionary entries to:

# Specify dictionary entry types
a: Int
b: Float
c: Vector3

dict_test = {
    'aa': 5,
    'bb': 0.2,
    'cc': (5,10,15)
}

a,b,c = dict_test.extract(args__out=['aa', 'bb', 'cc'])

BindOutput(a,b,c)
Get variadic output subports from dict::Extract

Graph templates

In APEX Script, a function can be treated like a graph template to easily add logic to an existing graph. You can use APEX Script to write expressions and logic that is bundled up in a function, and then insert this function directly into an existing graph to modify its functionality.

In APEX rigging, a character rig is represented as a graph. If you have a function that has a reverse foot or lookat functionality, you could insert this function into an incoming character rig graph to procedurally build up the character’s final rig.

Note

Calling a function simply executes the function logic in the current APEX Script. To use the function logic in another graph (like an incoming rig), the function must be inserted into the incoming graph.

In this example, we start with an input rig graph that contains only an input and output node:

Original rig graph

We then add a simple mathematical operation to the input rig graph using an APEX Script SOP:

Add logic to rig
  1. In the APEX Script SOP, use the predefined headers to automatically add the APEX Script commands that take in and modify an input graph:

    • Turn on the Header parameter.

    • In the Header section, set Template to Graph.

  2. Specify the input to the APEX Script SOP. In the Invocation section, under Input1 Bindings, turn on Bind To Geometry Parameter, and set the parameter to geo.

  3. To output the updated rig graph, set Bind Output Geometry to output:geo.

  4. Add the following code to the Snippet parameter. The code adds function test to the input graph as a subnet using graph::AddSubnet:

    # Functionality to add to the input graph
    def test(a: vector3, b: float) -> tuple(vector3, float):
        a = a * b
        b = b + 1.0
        return a, b
    
    # Add function 'test' to the input graph as a subnet named 'my_test'
    my_subnet = graph.addSubnet(test, 'my_test')
    
    # Connect subnet to the input node of the input graph
    my_subnet.a_in.promote('aa')
    my_subnet.b_in.promote('bb')
    
    # Connect subnet to the output node of the input graph
    my_subnet.a_out.promote('aa')
    my_subnet.b_out.promote('bb')
    
  5. To see the updated rig graph in the APEX network view:

    • On the APEX Script SOP, in the Visualizer section, set Show to Output 1.

      or

    • Connect the 1st output of the APEX Script SOP to a Null SOP, and select the Null SOP.

    Updated rig graph

    Dive into the my_test subnet to view the graph logic of function test from the APEX Script code:

    Contents of subnet 'my_test'

Note

In the example above, graph::AddSubnet is used to add the function to the input graph as a subnet. You could also use graph::Merge to merge the function directly into the input graph without packing it into a subnet. However, the resulting graph could contain multiple graph input and output nodes, which you would then have to connect to the appropriate nodes. The cleaner approach is to use graph::AddSubnet.

Adding rig logic without graph templates

Without using a function as a template, the APEX Script code to create the same logic as the previous example is much more convoluted. In this case, the only way to add functionality to an incoming rig graph would be to:

  1. Add the individual mathematical nodes.

  2. Script each node connection.

  3. Pack up the above logic into a subnet.

  4. Connect the subnet to the main graph.

Below is the same logic as the previous example without using a function as a template:

# Add nodes to graph
mult = graph.addNode('MultiplyExpr', 'Multiply<Vector3,Float>')
add = graph.addNode('AddExpr', 'Add<Float>')
val = graph.addNode('Value_right', 'Value<Float>', parms={'value':1.0})

# Connect Multiply node to graph input and output
mult.a_in.promote()
mult.b_in.promote('b')
mult.result_out.promote('a')

# Connect Add node to graph input and output
add.a_in.promote('b')
add.result_out.promote('b')

# Connect Value node to Add node
val.value_out.connect(add.b_in)

# Pack nodes (preserving the connections) into a subnet
# Output names of subnet is set to default values
node_arr: ApexNodeIDArray = [mult, add, val]
graph.packSubnet('my_test', node_arr)

Invoking another graph

InvokeOutputs() is a special function that is used to execute another graph. When you have an input that is a graph, InvokeOutputs() is used to execute that graph input.

In this example, InvokeOutputs() executes the function foo:

def foo (a: Int, b: Int) -> tuple[Int, Int]:
    return b, a

x: Int
y: Int

x, y = InvokeOutputs(foo, parms={"a": 1, "b": 2}, outputs=["output:a", "output:b"])
Logic to invoke another graph
Function foo being invoked

The syntax of InvokeOutputs() is:

InvokeOutputs(<function>, parms=<dict>, outputs=[array of strings])

where:

  • parms - Dictionary of parameter values to pass into the function.

  • outputs - An array of strings that specify the values returned by the function. The strings are in the format <output_node>:<output_port>, where <output_node> is the output node in the function graph (function foo, in our case).

The outputs argument is matched up with the function return values. In the example above:

  • output:a is set to function return value a. Variable x is then set to output:a.

  • output:b is set to function return value b. Variable y is then set to output:b.

If the function return statement contains an expression, set the outputs argument to result<x>, where x is the position of the function return value starting at 0:

def foo(a: Int):
    return a+1

x: Int = InvokeOutputs(foo, parms={"a": 1}, outputs=["output:result0"])

# Different way of specifying the return value type
y = Int(InvokeOutputs(foo, parms={"a": 2}, outputs=["output:result0"]))

If the function return statement contains multiple expressions, the order of the return values is important. For the return values that are not expressions, the corresponding outputs argument must be set to output:<variable>:

# Function return values using expressions (a+b and a*b)
def foo(a: Int, b: Int):
    return a+b, a, a*b

x: Int
y: Int
z: Int

# Function returns a+b in output:result0, and
# a*b in output:result2
x, y, z = InvokeOutputs(foo, parms={"a": 1, "b": 2}, outputs=["output:result0", "output:a", "output:result2"])

Tip

It is more readable and less error-prone for a function to return only variables and not expressions.

Retrieve only certain values from the function:

def foo(a: Int, b: Int):
    return a+b, a, a*b

x: Int

# Only retrieves the a*b return value from function foo
x = InvokeOutputs(foo, parms={"a": 1, "b": 2}, outputs=["output:result2"])

Associating APEX node IDs to a graph

If you have multiple graphs, you can use the special function setGraph() to associate node IDs to a particular graph:

graph1: ApexGraphHandle = BindInput()
graph2 = ApexGraphHandle()

my_node: ApexNodeID = BindInput()

# Associate my_node with graph2
my_node.setGraph(graph2)

Associate multiple node IDs to different graphs:

graph1: ApexGraphHandle = BindInput()
graph2 = ApexGraphHandle()

node_arr = ApexNodeIDArray()

# Append APEX node IDs to array
for i in range(4):
    node_arr.append(Value_ApexNodeID_())

# Associate node ID in node_arr[1] to graph2
x = node_arr[1]
x.setGraph(graph2)

# Associate node ID in node_arr[2] to graph1
# Different way of calling setGraph()
y = node_arr[2]
setGraph(y, graph1)

Converting graphs to APEX Script code

The APEX Script, APEX Autorig Component, and APEX Rigscript Component SOPs provide the option to convert graphs to APEX Script code.

For the APEX Script SOP:

  1. Connect an existing graph into the 2nd input of the APEX Script SOP.

  2. In the Decompile section, set the drop-down menu to Second Input.

  3. Click the Convert to Snippet button.

The APEX Script code will appear in the Snippet parameter.

See the special functions that may be added to the decompiled code.

How-to

To...Do this

See the nodes or ports returned from a function

View a single port ID:

# x is an APEX port ID
name = x.data()
BindOutput(name)

View a list of port IDs:

# x is an array of APEX port IDs
x = graph.matchPorts('*xform*')

# Initialize an array of strings
arr = StringArray()

# Add the port names to the array
for p in x:
    p_name = p.data()
    arr.append(p_name)

# Output the array of port names
BindOutput(arr)

To view the port names:

  1. In the APEX Script SOP, Invocation section, click beside Output Dictionary Bindings.

  2. In the geometry spreadsheet, click Detail on the top toolbar.

KineFX

Overview

Preparing character elements

Rigging with APEX graphs

Building rig graphs with APEX Script

Rigging with rig components

Animating in the viewport

SOP-based animation

Deformation

Animation retargeting

Pre-H20

Panes

Appendix