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.

APEX Script for building and modifying graphs

APEX Script code can be directly represented as an APEX graph, as shown in this example. APEX Script can also be used as a tool to build new graphs and modify existing graphs.

Build a new graph

In this example, we use APEX Script to create a graph that contains a single node.

  1. In the APEX Script SOP, add the following code to the Snippet parameter:

    # Initialize a graph
    graph = ApexGraphHandle()
    
    # Add a TransformObject node to the new graph
    graph.addNode('test_a', 'TransformObject')
    
  2. The APEX Script snippet itself is a graph. You can view the graph representation of the APEX Script code in the APEX network view:

    APEX Script as a graph
  3. To output the new graph that the APEX Script code created:

    • Add the following commands to the end of the APEX Script snippet to write out the graph as geometry:

      geo = graph.writeToGeo()
      BindOutput(geo)
      

      The APEX Script graph is updated to include the functionality that outputs the new graph:

      APEX Script outputs a graph
    • Set Bind Output Geometry to output:geo.

  4. To see the new 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.

      Graph created by APEX Script

Modify an existing graph

In this example, we use APEX Script to add a node to an existing graph.

  1. Create a graph in the APEX Edit Graph SOP that contains only an input and output node:

    Original graph
  2. Connect the graph as an input to the APEX Script SOP:

    Update original graph
  3. In the APEX Script SOP, use the predefined headers to automatically add the APEX Script commands that allow you to take in and modify an input graph:

    • Turn on the Header parameter.

    • In the Header section, set Template to Graph.

  4. 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.

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

  6. The APEX Script SOP now takes in the input graph and outputs it unchanged. To see the graph that is output by the APEX Script SOP:

    • 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.

  7. To update the input graph, add the following code to the Snippet parameter:

    # Add a TransformObject node to the input graph
    graph.addNode('test_a', 'TransformObject')
    
    Updated graph

Specifying graph inputs and outputs

BindInput() and BindOutput() are special functions that allow you to define the inputs and outputs of a graph. See this simple 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 are using the variable afterwards.

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

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 with the following syntax:

<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 look at 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 the 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 allow you to 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 only way to add functionality to an incoming rig graph is 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.

Without graph templates, the APEX Script code to create the same logic as the previous example is much more convoluted:

# 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

Preparing character inputs

Rigging with rig components

Rigging with APEX graphs

Building graphs with APEX Script

Animating in the viewport

Technical Animation

Deformation

Animation Retargeting

Pre-H20

Panes

Appendix