[Arnold] Getting component links to node parameters with the Python API


Let’s say you have something like this:

alRemapFloat
{
 name alRemapFloat1
 input alCombineColor1.r
}

where the R component of the alCombineColor node is linked to the alRemapFloat.input parameter.

To get the component that is linked to input, you use the comp parameter of AiNodeGetLink(), like so:

node = AiNodeLookUpByName( "alRemapFloat1" )
comp = c_int()
linked_node = AiNodeGetLink( node, "input", comp);

Since R is connected, comp will be 0.

comp is -1 if the entire output is linked, or a number between 0 and 3 to indicate a specific component (0 for r, 1 for g, 2 for b, and 3 for a)

Note that you can pass a c_int to AiNodeGetLink(). ctypes takes care of converting it to a byref() for you.

Creating a polymesh with the Arnold Python api


polymesh
Here’s a snippet that shows how to create a simple four-polygon polymesh node with the Arnold Python API.

n = AiNode( "polymesh" )
AiNodeSetStr( n, "name", "grid" )

nsides = [4, 4, 5, 6]
AiNodeSetArray( n, "nsides", AiArrayConvert(len(nsides), 1, AI_TYPE_UINT, (c_uint*len(nsides))(*nsides) ) )

vidxs = [0, 1, 4, 3, 1, 2, 5, 4, 3, 4, 7, 6, 9, 4, 5, 11, 8, 10, 7]
AiNodeSetArray( n, "vidxs", AiArrayConvert(len(vidxs), 1, AI_TYPE_UINT, (c_uint*len(vidxs))(*vidxs) ) )

nidxs = [0, 1, 2, 3, 1, 4, 5, 2, 3, 2, 6, 7, 8, 2, 5, 9, 10, 11, 6]
AiNodeSetArray( n, "nidxs", AiArrayConvert(len(nidxs), 1, AI_TYPE_UINT, (c_uint*len(nidxs))(*nidxs) ) )

vlist = [-1, 0, -1, -1, 0, 0, -1, 0, 1, -0.197835326, 0, -0.742445886, 0, 0, 0, 0, 0, 1, 0.802164674, 0, -0.742445886, 1, 0, 0, 1, 0, 1, 0.270379633, 0, -1.21302056, 1, 0, 0.508926511, 0.496316135, 0, 1]
AiNodeSetArray( n, "vlist", AiArrayConvert(len(vlist), 1, AI_TYPE_FLOAT, (c_float*len(vlist))(*vlist) ) )

nlist = [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
AiNodeSetArray( n, "nlist", AiArrayConvert(len(nlist), 1, AI_TYPE_FLOAT, (c_float*len(nlist))(*nlist) ) )

m = AtMatrix( 	1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0, 1, 0,
		0, 0, 0, 1 )

am = AiArrayAllocate(1, 1, AI_TYPE_MATRIX)
AiArraySetMtx( am, 0, m )

AiNodeSetArray( n, "matrix", am  )

AiNodeSetBool( n, "smoothing", True )

AiNodeSetByte(n, "visibility", 255 )

# Assign a shader to the polymesh node
u = AiNode( "utility" )
AiNodeSetStr( u, "name", "aiUtility1" )

AiNodeSetPtr( n, "shader", u )

And here’s the resulting node in the ASS file:

polymesh
{
 name grid
 nsides 4 1 UINT
4 4 5 6
 vidxs 19 1 UINT
  0 1 4 3 1 2 5 4 3 4 7 6 9 4 5 11 8 10 7
 nidxs 19 1 UINT
  0 1 2 3 1 4 5 2 3 2 6 7 8 2 5 9 10 11 6
 vlist 12 1 POINT
  -1 0 -1 -1 0 0 -1 0 1 -0.197835326 0 -0.742445886 0 0 0 0 0 1 0.802164674 0 -0.742445886 1 0 0
  1 0 1 0.270379633 0 -1.21302056 1 0 0.508926511 0.496316135 0 1
 nlist 12 1 VECTOR
  0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0
 smoothing on
 visibility 255
 matrix
 1 0 0 0
 0 1 0 0
 0 0 1 0
 0 0 0 1
 shader "aiUtility1"
}

[Python] Linking nodes


There’s been a few questions recently about nodes that look like this in an ASS file:

MayaPlusMinusAverage3D
{
 name plusMinusAverage2
 operation "sum"
 input3D 2 1 POINT
0 0 0 0 0 0
 input3D[0] file1
 input3D[1] bulge1
}

That’s the ASS representation of this (in Maya):
mayaplusminusaverage3D
On line 6, you have the default values (two POINTs) for input3D. If there was nothing plugged into the input3D, you’d get the points (0,0,0) and (0,0,0).

Here’s an example that shows how to set up the node links for a MayaPlusMinusAverage3D node.

from arnold import *

ass_file = "mayaplusminusaverage.ass"

AiBegin()
AiMsgSetConsoleFlags(AI_LOG_ALL)

# Load the mtoa shaders, which include MayaPlusMinusAverage3D
AiLoadPlugins('C:/solidangle/mtoadeploy/2015/shaders')

AiASSLoad(ass_file, AI_NODE_ALL)

# Create a MayaPlusMinusAverage3D node
n = AiNode( "MayaPlusMinusAverage3D" )

# Set the name parameter
AiNodeSetStr( n, "name", "plusMinusAverage2" )

# Set the operation parameter
AiNodeSetStr( n, "operation", "sum" )

# input3D is of type POINT[] (an array of POINTs)

# Allocate an array; by default the array is initialized to 0s
a = AiArrayAllocate( 2, 1, AI_TYPE_POINT )
AiNodeSetArray(n, "input3D", a )

# Link input3D to the file and bulge nodes, which are assumed to already exist
f = AiNodeLookUpByName( "file1" )
b = AiNodeLookUpByName( "bulge1" )
AiNodeLink(f, "input3D[0]", n);
AiNodeLink(b, "input3D[1]", n);

AiASSWrite(ass_file, AI_NODE_ALL, False)
AiEnd()

Here’s another example. This time, one input3D is set to (0.3, 0.2, 0.4), and the other input3D is linked to a Bulge node.

MayaPlusMinusAverage3D
{
 name plusMinusAverage3
 operation "sum"
 input3D 2 1 POINT
0.3 0.2 0.4 0 0 0
 input3D[0] file1
}

mayaplusminusaverage3D_1
And here’s the Python snippet that creates the node and sets up the link:

n = AiNode( "MayaPlusMinusAverage3D" )

b = AiNodeLookUpByName( "bulge1" )

AiNodeSetStr( n, "name", "plusMinusAverage3" )
AiNodeSetStr( n, "operation", "sum" )

a = AiArrayAllocate( 2, 1, AI_TYPE_POINT )
AiArraySetPnt(a, 0, AtPoint( 0.3, 0.2, 0.4 ) )

AiNodeSetArray(n, "input3D", a )

AiNodeLink(b, "input3D[1]", n);

A couple of notes:

  • You can get the type of parameters like input3D with kick:
    kick -l ..\shaders -info MayaPlusMinusAverage3
    D
    node:         MayaPlusMinusAverage3D
    type:         shader
    output:       RGB
    parameters:   3
    filename:     ..\shaders/mtoa_shaders.dll
    version:      4.2.3.1
    
    Type          Name                              Default
    ------------  --------------------------------  --------------------------------
    
    ENUM          operation                         sum
    POINT[]       input3D                           (empty)
    STRING        name
    
  • For the full list of parameter types such as AI_TYPE_POINT, see the “AtParamEntry API” in the Arnold API Reference (or python/arnold/ai_params.py in the Arnold install).

[Arnold] Getting started with the Arnold Python API


You can download Arnold here. The Arnold download (aka the Arnold SDK) includes the Arnold library, the Arnold C++ API, the API docs, and the Python bindings for the Arnold API.

To use the Arnold Python API, you need to add the Arnold python folder to the PYTHONPATH:

set PYTHONPATH=C:\solidangle\arnold\Arnold-4.2.4.0-windows\python

Here’s a “hello world” written with the Arnold Python API:

#
# hello_world.py
#
from arnold import *

AiBegin()

AiMsgSetConsoleFlags( AI_LOG_INFO )

AiMsgInfo( 'Hello World' )

AiEnd()

Here’s a quick breakdown of the script.

  • You need to import the arnold module.
  • An Arnold session always starts with AiBegin() and ends with AiEnd(). You have to call AiBegin() to initialize Arnold and enable the Arnold API. Try commenting it out and see what happens…
  • We need to call AiMsgSetConsoleFlags() to set the log verbosity level (otherwise we won’t see our “Hello World”, because the default level is AI_LOG_NONE).
  • AiMsgInfo() sends our “Hello World” to the log.

Assuming that Python in is your PATH, you can run the hello world script like this:

python hello_world.py

and that will give you this output:


C:\solidangle\arnold\scripts>python helloworld.py
        | log started Wed Mar 04 14:31:40 2015
        | Arnold 4.2.4.0 windows icc-14.0.2 oiio-1.4.14 rlm-11.2.2 2015/02/26 15:08:42
        | running on StephenBlair-PC with pid 33812
        |  1 x Intel(R) Xeon(R) CPU E3-1240 V2 @ 3.40GHz (4 cores, 8 logical) with 16338MB
        |  Windows 7 Professional Service Pack 1 (version 6.1, build 7601)
        |
        | Hello World
        |
        | releasing resources
        | Arnold shutdown

For documentation, you use the Arnold SDK documentation. It’s for the C++ API, but the Python API is basically a one-to-one wrapper around the C++ API.

You can find the docs in the doc/api/index.html folder of the Arnold installation.

AiBegin() and AiEnd() are part of the Rendering API.
AiMsgSetConsoleFlags() and AiMsgInfo() are part of the Message Logging API, so go there to check out the possible flags, and what other logging functions are available.
arnold_api_reference

[Arnold] [Python] Changing Arnold options in Python


The global render settings are stored in the options node, which you can get with AiUniverseGetOptions(). If the universe isn’t active (you are not inside an AiBegin() / AiEnd() block so the scene doesn’t exist yet), AiUniverseGetOptions() returns None.

Once you have the options node, you can load options from an ASS file, or you can update it with the node parameter setters such as AiNodeSetInt(). The way kick, and pykick, work is that they load the ASS file first, with its options, and then update the options node with the values from the command line.

from arnold import *
import os

AiBegin()

AiMsgSetConsoleFlags(AI_LOG_ALL)

# Get the default options
options = AiUniverseGetOptions()

AiLoadPlugins( os.getenv( 'ARNOLD_PLUGIN_PATH' ) )

AiASSLoad("C:/Users/Support/project/scenes/test_640x480.ass", AI_NODE_ALL)

# Render using options loaded from the ASS file
AiRender()

# Change options and render with the changes
AiNodeSetInt(options, 'xres', 320)
AiRender()

# Load options and use them to render
AiASSLoad("C:/Users/Support/project/scenes/test_960x540.ass.gz", AI_NODE_OPTIONS)
AiRender()

AiEnd()

[Arnold] Loading plugins when you edit ASS files


If you’re using the Arnold API to update ASS files (for example, to change paths), you need to load all the plugins (aka shaders) that are referenced by the ASS file. Otherwise, the unknown nodes are skipped on load, and therefore won’t be in any ASS file you write.

For example, if you set ARNOLD_PLUGIN_PATH to point to the locations of the MtoA shaders and any custom shaders you use, then you could do something like this:

from arnold import *
import os

AiBegin()
AiMsgSetConsoleFlags(AI_LOG_ALL)
AiLoadPlugins( os.getenv( 'ARNOLD_PLUGIN_PATH' ) )
AiASSLoad("original.1001.ass", AI_NODE_ALL)

#
# Do your edits here
#

AiASSWrite("edited.1001.ass", AI_NODE_ALL, False)
AiEnd()

Changing ASS files with the Arnold Python API


If you want to change something in existing ASS files, don’t write an ad-hoc script or your own parser for the ASS file. Use the Arnold API. The Arnold API includes a set of Python bindings, so you can fairly quickly whip up a script to do whatever it is you need to do 🙂

For example, we recently discovered (and fixed) an issue where exported ASS files were missing procedural nodes. SItoA exports hair data in chunks (one chunk for every 200K hairs), but the exported ASS had just one procedural for the first chunk (chunk 0), but there should be one procedural for each chunk.

Here’s what the procedural for chunk 0 on frame 5 looks like. The missing procedurals would be for bin files like Hair.chunk.1.5.bin, Hair.chunk.2.5.bin, and so on.

procedural
{
 name Hair.SItoA.5000
 dso "sitoa_curves_proc.dll"
 data "//Projects/Support/Arnold_Scenes/Hair.chunk.0.5.bin"
 load_at_init on
}

So, for my first dive into the Arnold API, I put together a basic little script to add the missing procedurals. To do this, I had to learn how to:

  • Read and write ASS files
  • Iterate over nodes and find a specific type of node
  • Get parameter values from a node
  • Create new nodes
from arnold import *
import glob


ass_file = "Hair_Archive.ass"

AiBegin()
AiMsgSetConsoleFlags(AI_LOG_ALL)
AiASSLoad(ass_file, AI_NODE_ALL)

# Iterate over all shape nodes, which includes procedural nodes
iter = AiUniverseGetNodeIterator(AI_NODE_SHAPE);
while not AiNodeIteratorFinished(iter):
	node = AiNodeIteratorGetNext(iter)
	#print AiNodeGetName( node )

	# Is the node a procedural?	
	if AiNodeIs( node, "procedural" ):
	
		data = AiNodeGetStr( node, "data" )
		name = AiNodeGetStr( node, "name" )
	
		# Find all other chunk.<chunk-number>.<frame>.bin files
		chunks = glob.glob( data.replace( 'chunk.0', 'chunk.*' ) )

		# Add procedural nodes for chunks 1,2,3...
		for i in range(1,len(chunks)):
			n = AiNode("procedural");
			AiNodeSetStr(n, "name", "%s.%s" % (name, i) )
			AiNodeSetStr(n, "dso", "sitoa_curves_proc.dll")
			AiNodeSetStr(n, "data", data.replace( 'chunk.0', 'chunk.%s' % i ) )
			AiNodeSetBool(n, "load_at_init", True)

AiNodeIteratorDestroy(iter)


AiASSWrite(ass_file, AI_NODE_ALL, False)
AiEnd()