Houdini 20.5 Character KineFX

Functions in APEX Script

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.

A function is a block of code that performs a specific operation. In APEX Script, users can create their own functions or use the built-in APEX functions.

User-defined functions

User-defined functions in APEX Script are represented as subnet nodes in the APEX network view. The contents of the subnet node contains the function logic.

APEX Script code:

# Function definition
def test(a: Int, b: Int):
    c: Int = a + b
    return c

# Function call
x = test(1, 2)
Function subnet node (left); subnet contents (right)

Users can define their own functions in APEX Script similar to Python:

  • Functions are defined using the def keyword followed by the function name.

  • Information, called arguments, can be passed into a function through a comma-separated list of values within a set of parentheses (). These arguments follow the function name.

  • The block of code that is executed when the function is called must be indented.

  • Information can be returned by a function, and these return values are specified using the return statement. A function can return multiple values separated by commas.

  • Functions must be defined before they are called.

Function with no arguments:

# Function definition
def test():
    a = 1.5
    return a

# Function call; result is x = 1.5
x = test()

The number of arguments in the function call must match the number of arguments in the function definition. It is good practice to annotation the function arguments:

# Function definition
def test(a: Int, b: Int):
    c: Int = a + b
    return c

# Function call
x = test(1, 2)

You can call a function with keyword arguments, which are arguments with the syntax <key>=<value>, where <key> is the name of the argument. When calling a function using all keyword arguments, the order of the arguments does not matter.

Note

If you use both keyword and positional (non-keyword) arguments in a function call, keyword arguments must be placed after positional arguments.

# Function definition
def test(a: Int, b: Int):
    c: Int = a + b
    return c

# Function call with keyword arguments
x = test(b=1, a=2)

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

# Function takes in Base.skel and returns Base.shp
def test(Base__dot__skel: Geometry):
    Base__dot__shp = Geometry()
    return Base__dot__shp

# Graph input is Base.skel
geo = BindInput(Base__dot__skel=Geometry())
x: Geometry = test(geo)
Rig geometry names in subnet inputs and outputs

A function can return multiple values, and you can specify the return types following the -> after the argument list:

def test(a: Vector3, b: Float) -> tuple(Vector3, Float):
    a = a * b
    b = b + 1.0
    return a, b

x, y = test(a=(3.0, 3.0, 3.0), b=5.5)

If you want to ignore certain return values, assign those values to an underscore (_):

def test(a: Int):
    a1: Int = a + 1
    a2: Int = a + 2
    a3: Int = a + 3
    a4: Int = a + 4
    return a1, a2, a3, a4

_, _, x, _ = test(1)

You can specify default values for the function arguments. If the function is called without arguments, the default values are used:

def foo(a: String = 'test', b: Int = Int(Float(1.5))):
    return a, b

# Result: x = 'test', y = 1
x, y = foo()

Expressions in function calls:

def foo(a: String, b: Int):
    return a, b

m = 3
n = 4
p = 'test'

# Result: x = 'test3', y = 7
x, y = foo(p+String(m), m+n)

Functions can be nested inside other functions:

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

# Call foo within this function
def bar(a: Int) -> Int:
    return foo(a)+4

# Result: x1 = 3, x2 = 7
x1 = foo(1)
x2 = bar(1)

Functions can be daisy-chained:

geo.computeTransform().transform(xform=Matrix4())

Saving functions as subgraphs

Functions can be saved to disk as subgraphs using the APEX Script SOP. These subgraphs can then be used in APEX Script code as global functions. Any subgraphs that you have, including ones built by other users, could be used as functions in APEX Script.

To save a function as a subgraph:

  1. In an APEX Script SOP, turn on the Subgraphs parameter.

  2. In the Subgraphs section, define the function in the Subgraph snippet parameter, and put the @subgraph decorator above the function definition.

    For example:

    @subgraph
    def test(a: Int, b: Int):
        c: Int = a + b
        return c
    
  3. The subgraph must be saved as a .bgeo file in @/apexgraph, where @ expands to the directories in the HOUDINI_PATH. If you don’t specify the directory, the subgraph will be saved to $HOME/apexgraph. You can set the subgraph location and filename in the Geometry File parameter, or in the subgraph decorator argument, @subgraph(<file_path>).

  4. Click the Save Subgraphs button.

In the same APEX Script SOP, or in a new APEX Script SOP, you can now use the saved subgraph as a function when writing APEX Script code.

Built-in functions

A library of built-in functions is available within APEX Script. These functions can be called using either the APEX namespace, or an object-oriented syntax where the function operates on a variable. The list of available APEX functions can be found in the APEX nodes index, the APEX network view TAB menu, or in the autocomplete feature when typing in the Snippet parameter of the APEX Script SOP, APEX Autorig Component SOP, or APEX Rigscript Component SOP.

Different syntax for calling functions:

geo = BindInput(Geometry())

# Call function using the APEX and SOP namespaces 
a = apex.sop.copytopoints(geo)

# Object-oriented syntax for calling a function
b = geo.copytopoints()

In the example below, the array that is passed into the append() function is an in-place port. When using the APEX namespace, the input array is updated:

x = ['aaa']

x = apex.array.append_String(x, 'bbb')

# Result: x = ['aaa', 'bbb']

If the result of append() is assigned to a different variable, the original array is not updated:

x = ['aaa']

y = apex.array.append_String(x, 'bbb')

# Result: x = ['aaa'], y = ['aaa', 'bbb']

When using the object-oriented syntax, the array is passed into the function as if it were the first input, and is always updated in-place:

x = ['aaa']

x.append_String('bbb')

# Result: x = ['aaa', 'bbb']

If the object-oriented append() is assigned to a variable, the result is the 2nd returned value of append() (the index of the appended value):

x = ['aaa']

idx = x.append_String('bbb')

# Result: x = ['aaa', 'bbb'], idx = 1

Call a function with arguments:

box = apex.sop.box(t=Vector3(1,1,1), scale=Float(2))

Functions without a namespace, for example Sine(), do not need to be prefixed with the APEX namespace:

# With APEX namespace
a = apex.sine(1.0)

# Without APEX namespace
b = sine(1.0)

By default, APEX Script uses the exact version of the APEX function that is called, and not the latest version. For example, IntBitMaskToBool() calls the first version of the function. If you want to use the second version of IntBitMaskToBool, call IntBitMaskToBool.v2_0(). See the special function HoudiniVersion() for information on how to use a specific version of APEX functions.

Templated functions

Templated functions are functions that operate on different types of data. For example, geo::SetPrimAttribValue<T> is a templated function, where <T> could be one of several different types. Templated functions can be specified in two ways:

Use the specific function name that contains the type:

geo.setPrimAttribValue_String(prim, 'name', 'test')

Use the valuetype argument to specify the type, valuetype=<type>:

geo.setPrimAttribValue(prim, 'name', 'test', valuetype=String)

Special functions

APEX Script provides special functions that allow you to perform various graph operations, including the ability to add information to graph nodes, use a specific version of APEX nodes, and invoke graphs from within a graph.

Function

Description

BeginMetadata, EndMetadata

Sets metadata on the block begin and end nodes for loops and if blocks (for example, ForBegin, IfEnd). The available metadata are listed in the special function arguments.

y = 0

for x in range(3):
    # Set metadata on ForBegin node
    BeginMetadata(__name='test_begin', __color=(1,0,1), __tags=['tag1', 'tag2'])

    y = y + 1

    # Set metadata on ForEnd node
    EndMetadata(__name='test_end', __pos=(1,1,1), __tags=['out_tag'])

BindInput, BindOutput

Defines the inputs and outputs of a graph. For more information, see this example for how to use BindInput() and BindOutput(), and specifying graph inputs for example uses of BindInput().

HoudiniVersion

Sets the version of the APEX functions to use. For example, HoudiniVersion('20.5.250') tells APEX Script to use the set of APEX functions from Houdini version 20.5.250.

You could also specify the Houdini version in the header template:

  1. Turn on the Header parameter.

  2. In the Header section, turn on Add Version.

  3. Turn on Edit to change the Houdini version.

Two other arguments are available for use with the HoudiniVersion() function - newest and ignore.

  • HoudiniVersion('newest') - Uses the newest versions of the APEX functions.

  • HoudiniVersion('ignore') - Uses the exact version of the APEX function that is called. For example, IntBitMaskToBool() calls the first version of the function. If you want to use the second version of IntBitMaskToBool, call IntBitMaskToBool.v2_0().

If HoudiniVersion() is not present, this is the same as calling HoudiniVersion('ignore').

InputMetadata, OutputMetadata

Sets metadata on the graph input (parms) and output nodes inside a subnet. The available metadata are listed in the special function arguments.

def test(a,b):
    # Set metadata on subnet input node
    InputMetadata(__name='abc', __pos=(1,0,0), __properties={'test': 1.5})

    c: Float = a + b

    # Set metadata on subnet output node
    OutputMetadata(__color=(1,0,1), __tags=['out_tag1','out_tag2'])

    return c

x = test(1,2)

InvokeOutputs

See invoke graph.

SetGraph

See associate APEX node ID to a graph.

StickyNote

Creates sticky notes in the graph. The arguments listed below, in addition to the special function arguments, can be used to set certain properties on the sticky note:

Argument

Type

contents

String

dimensions

Vector2

is_background_hidden

Bool

is_collapsed

Bool

text_color

Vector3

text_size

Float

Example:

StickyNote(contents='hello', dimensions=(3.0, 3.0), is_background_hidden=False, is_collapsed=False, text_color=(1.0, 0.0, 0.0), text_size=0.0, __color=(0,0,1))

_portalias

Creates another identifier for a variable without making a copy. This could introduce graph sorting issues, so use with caution.

Create an alias of a variable:

x = 1.5
y = _portalias(x)

# y = 1.5
BindOutput(y)

Create an alias of a variable that is assigned to a function:

def test():
    x = 1
    return x

m = test()
m_alias = _portalias(m)

# m_alias = 1, n = 3
n = m_alias + 2

Use an alias in a function:

def test(a: Int, b: Int):
    c: Int = a + b
    d = _portalias(c)
    d += 2
    return d

# m = 5
m = test(1, 2)

Decorators

A decorator performs an operation on another function, and is specified with the @ symbol above the function it operates on.

For example:

@subgraph
@namespace('my_namespace')
@safeguard_inputs(True)
def test(a: Int, b: Int):
    c: Int = a + b
    return c

Decorator

Description

subgraph

The @subgraph decorator allows the function that follows it to be saved as a subgraph. The <file_path> argument is optional:

@subgraph([<file_path>])

Example:

@subgraph('$HIP/subgraph_test.bgeo')

namespace

You can specify a namespace for the function with the namespace decorator. Then after saving the function as a subgraph, you can call the function using <namespace>.<function>. The syntax for the namespace decorator is:

@namespace('<namespace>')

For example, you can define the following subgraph in the APEX Script SOP, Subgraphs section:

@subgraph
@namespace('my_namespace')
def test(a: Int, b: Int):
    c: Int = a + b
    return c

After saving the function as a subgraph, you can call the subgraph function with:

x = my_namespace.test(1,2)

safeguard_inputs

The safeguard_inputs decorator helps to prevent graph sorting issues by making copies of function input arguments where necessary. The syntax for the safeguard_inputs decorator is:

@safeguard_inputs(True|False)

When @safeguard_inputs is set to True, APEX Script makes a copy of a function input argument (adds a Value node to the graph) if:

  • The input argument feeds into an in-place port.

    and

  • The name of that input does not match any output names on the function.

If @safeguard_inputs is set to False, APEX Script does not create copies of any function arguments. This could introduce graph sorting issues, so use caution when setting @safeguard_inputs to False.

If @safeguard_inputs is not included before a function, it is set to True by default.

Special function arguments

The special function arguments listed below can be used to specify certain information on a node:

Argument

Type

Description

__name

String

The name of the node.

__color

Vector3

The color of the node.

__pos

Vector3

The position of the node in the graph.

__properties

Dict

The properties stored on the node.

__tags

StringArray

The tags stored on the node.

Example:

def abc(a: Vector3, b: Float):
    a = a * b
    b = b + 1.0
    return a, b

# Subnet node name is set to the function name
subnet_a = abc(a=(3.0, 3.0, 3.0), b=5.5)

# Special function arguments used to set the name and color of the subnet node
subnet_b = abc(a=(3.0, 3.0, 3.0), b=5.5, __name='test_b', __color=(1,0,0))
Create subnet nodes using special function arguments

Function mappings

APEX Script provides convenient name mappings for some of the built-in APEX functions.

Graph functions

The examples in this section use the following definitions:

graph = ApexGraphHandle()

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

# Dictionary
d = {'xord': 3}

Function

Maps to

Example

addNode(<name>, <callback>, [<color>])

graph::AddNode

graph.addNode('abc', 'Add<Float>', (1,0,1))

addWire(<srcport>, <dstport>)

graph::ConnectInput

graph.addWire(a.xform_out, b.parent)

addWire(<srcnode>, <srcname>, <dstnode>, <dstname>)

graph::FindAndConnectInput

graph.addWire(a, 'xform', b, 'parent')

connectedPorts(<portid>)

graph::GetConnectedPorts

x = graph.connectedPorts(b.parent)

connectNodes(<srcport>, <dstport>)

graph::ConnectInput

graph.connectNodes(a.xform_out, b.parent)

connectNodes(<srcnode>, <srcname>, <dstnode>, <dstname>)

graph::FindAndConnectInput

graph.connectNodes(a, 'xform', b, 'parent')

debug(<doinvoke>)

graph::Compile

If <doinvoke> is not specified, it defaults to True.

graph.debug()

findOrAddPort(<nodeid>, <portname>)

graph::FindOrAddPort

x = graph.findOrAddPort(a, 'xform[in]')

freeze

Value<ApexGraphHandle>

“Saves” the graph in a Value node.

g1 = graph.freeze()

getInputPorts(<node>)

graph::NodeInputs

x = graph.getInputPorts(a)

getOutputPorts(<node>)

graph::NodeOutputs

x = graph.getOutputPorts(a)

getPort(<nodeid>, <portname>)

graph::FindPort

x = graph.getPort(a, 'xform[in]')

matchNodes(<pattern>)

graph::FindNodes

x = graph.matchNodes('test*')

matchPorts(<pattern>)

graph::FindPorts

x = graph.matchPorts('test*:xform[in]')

ports(<pattern>)

graph::FindPorts

x = graph.ports('*xform*')

promoteInput(<portid>, [<subportname>], [<parmnodeid>], <parmname>)

graph::PromoteInput

graph.promoteInput(a.parent, 'p')

promoteOutput(<portid>, [<outputnodeid>], <outputname>)

graph::PromoteOutput

graph.promoteOutput(b.xform_out, 'x')

setNodeParms(<nodeid>, <parms>)

graph::UpdateNodeParms

graph.setNodeParms(a, d)

Node functions

The examples in this section use the following definitions:

graph = ApexGraphHandle()

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

# Dictionaries
d = {'abc': 3} 

Function

Maps to

Example

ancestors()

graphutils::NodeAncestors

# Pack node into a subnet
node_arr: ApexNodeIDArray = [a]
graph.packSubnet('my_test', node_arr)

# Returns 'my_test'
anc = a.ancestors()

callbackName()

graphutils::NodeCallbackName

x = a.callbackName()

data()

graph::NodeData

(name, callback, pos, color, parms, tags, properties, output) = a.data()

inPorts()

graph::NodeInputs

x = a.inPorts()

name()

graph::NodeName

x = a.name()

outPorts()

graph::NodeOutputs

x = a.outPorts()

parent()

graphutils::NodeParent

x = a.parent()

parms()

graphutils::NodeParms

x = a.parms()

path()

graphutils::NodePath

x = a.path()

port(<portname>)

graph::FindPort

x = a.port('xform[in]')

properties()

graphutils::NodeProperties

x = a.properties()

setColor(<color>)

graph::UpdateNode

a.setColor((1,0,0))

setName(<name>)

graph::UpdateNode

a.setName('aaa')

setParms(<parms>)

graph::UpdateNodeParms

a.setParms(d)

setPos(<pos>)

graph::UpdateNode

a.setPos((1,2,3))

setProperties(<properties>)

graph::UpdateNodeProperties

a.setProperties(d)

setTags(<tags>)

graph::UpdateNodeTags

a.setTags(['tag1','tag2'])

tags()

graphutils::NodeTags

x = a.tags()

Port functions

The examples in this section use the following definitions:

graph = ApexGraphHandle()

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

# Ports on node 'test_a'
a_in = a.t_in
a_out = a.xform_out

# Ports on node 'test_b'
b_in = b.parent_in
b_out = b.t_out

Function

Maps to

Example

connect(<dstport>)

graph::ConnectInput

Connect ports on the same nesting level in a graph.

a_out.connect(b_in)

connected()

graph::GetConnectedPorts

x = a_out.connected()

connectPath(<dstport>, [<name>])

graph::AddWirePath

Connects ports on different nesting levels in a graph, for example, connecting to a node in a subnet. The name of the connection port that is created on the subnet node is set to <name>.

# Add a node to the graph
c = graph.addNode('test_x', 'TransformObject')

# Pack the node into a subnet
node_arr: ApexNodeIDArray = [c]
graph.packSubnet('test_subnet', node_arr)

# Connect to node in subnet
a_out.connectPath(c.xform_in, 'abc')

data()

graph::PortData

(name, alias, datatype, porttype, isconnected, ispromoted) = a_out.data()

disconnect()

graph::ConnectInput

b_in.disconnect()

node()

graph::PortNode

x = a_out.node()

promote(<name>)

graph::PromotePort

a_out.promote('aa')

promoted()

graph::GetPromotedPort

x = a_out.promoted()

promoteInput(<name>)

graph::PromoteInput

a_in.promoteInput('tt')

promoteOutput(<name>)

graph::PromoteOutput

b_out.promoteOutput('tt')

subport(<portname>)

graph::GetSubPort

If you use subport() to create a subport on a node, you won’t see the new subport in the graph until you connect something to it.

# Add nodes to graph
c = graph.addNode('test_c', 'Add<Float>')
d = graph.addNode('test_d', 'Add<Float>')

# Create subport 'dd'
d_in = d.b_in.subport('dd')

# Connect to subport 'dd'
c.result.connect(d_in)

Data functions

The examples in this section use the following definitions:

# Dictionaries
d = {'abc': 3}

# Vectors
v1 = (1,2,3)
v2 = (2,4,5)

# Matrix
m = Matrix4(2)

Function

Maps to

Example

computeTransform(<mode>)

sop::kinefx::computetransform

geo: Geometry = BindInput()
geo.addJoint('joint_1', Matrix4(2))
geo.addJoint('joint_2', Matrix4(3))

x = geo.computetransform(mode=2)

cross(<b>)

CrossProduct

v = v1.cross(v2)

distance(<b>)

Distance

v = v1.distance(v2)

distanceTo(<b>)

Distance

v = v1.distanceTo(v2)

dot(<b>)

DotProduct

v = v1.dot(v2)

freeze()

Value<Dict>, Value<Geometry>

“Saves” the dictionary or geometry in a Value node.

d2 = d.freeze()

invert()

Invert<Matrix4>

This function is performed in-place - the input matrix is updated, so there is no need to assign to an output variable.

m.invert()

matrixToRotateTo(<b>)

transform::Dihedral<Matrix4>

v = v1.matrixToRotateTo(v2)

normalize()

Normalize

v = v1.normalize()

normalized()

Normalize

This function is performed in-place - the input vector is updated:

v1.normalized()

If normalized() is assigned to a variable, the variable is set to the length of the vector:

v = v1.normalized()

smoothRotation(<b>, <rord>)

transform::SmoothRotation

v = v1.smoothRotation(v2, 1)

Content library example

This example in the content library shows how to set up a sine wave control on an elephant’s trunk:

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