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"
}

[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

[MtoA] [Scripting] Setting the output image format


Settings like the image format and compression come from the defaultArnoldDriver node, so to set them through scripting, you need to set defaultArnoldDriver attributes.
ImageFormat_ai_translator

The image format corresponds to the ai_translator attribute.

# Python
import maya.cmds as cmds
cmds.setAttr( 'defaultArnoldDriver.ai_translator', 'exr', type='string' )
# PyMel
import pymel.core as pm
d = pm.PyNode( 'defaultArnoldDriver' )
d.ai_translator.set( 'exr )
// Mel
setAttr "defaultArnoldDriver.ai_translator" -type "string" "jpeg";

The rest of the attributes are named a little… better 😉 Here’s a Python snippet to list the driver attributes and their values:



from maya.cmds import *
sn = cmds.attributeInfo( inherited=False, short=True, type="aiAOVDriver" )
for s in sn:
    print "defaultArnoldDriver.%s = %s" %( s, cmds.getAttr( "defaultArnoldDriver.%s" % s ) )

[MtoA] Scripting Standin paths


standin_path

To set a standin path, use the aiStandin.dso attribute:

import maya.cmds as cmds
cmds.setAttr( 'ArnoldStandInShape.dso','C:/Users/SOLIDANGLE/Documents/BlueDog.ass',  type='string' )
print cmds.getAttr( 'ArnoldStandInShape.dso' )

Here’s one way to list some of the attributes on a standin shape node, and find out what attribute to set:

import maya.cmds as cmds
ntype = cmds.nodeType( "ArnoldStandInShape" )
print cmds.attributeInfo( inherited=False, t=ntype )

for x in cmds.attributeInfo( inherited=False, t=ntype ):
    print "%s = %s" % (x, cmds.getAttr( "ArnoldStandInShape.%s" % x ))

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()

SITOA: Adding Arnold materials through scripting


If you want to apply Arnold materials through scripting, there are a couple of undocumented commands you can use:

  • SITOA_AddMaterial takes the shader family (for example, Material or Texture) and the name of a shader, and connects that shader to the surface port on the Material node.
    # Add a standard material to the selected object
    SITOA_AddMaterial( "Material", "standard" )
    
    # Add ambient_occlusion to the selected object
    SITOA_AddMaterial( "Texture", "ambient_occlusion" )
    
  • SITOA_AddShader is similar, but it also takes a connection point as an argument, so you can connect to a specific port, such as the Environment or Displacement port.
    # Add a vector displacement shader
    SITOA_AddShader("Texture", "sta_vector_displacement", "displacement" )
    

However, neither of these commands return anything. So if you wanted to name the material, it’s not so easy. SITOA_AddMaterial does apply to the selection, so you could go through the selection to get the new material:

# Python
Application.SITOA_AddMaterial("Material", "standard")
sel = Application.Selection
mat = sel(0).Material
mat.Name = "MyStandardMaterial77"

You could write your own function with X3DObject.AddMaterial.

Here’s the simplest possible version. I don’t specify the path to a preset, just the name of the shader. That’s not terribly efficient, because Softimage now has to search for the preset (and that took about 0.2 seconds on my machine).

si = Application

def add_material( o, shader_name ):
	return o.AddMaterial( shader_name )

mat = add_material( si.Selection(0), 'Material', 'Standard' )
mat.Name = 'My_Standard_Mat'

Here’s a version that builds the path to the preset (just like the SITOA commands do), but that doesn’t require the shader family, just the name of the shader.

# dictionary of shader families, keyed by shader name
shader_types = {
  'ray_switch' : 'Material' , 
  'sta_vector_displacement' : 'Texture' , 
  'wireframe' : 'Material' , 
  'sta_displacement' : 'Texture' , 
  'ambient_occlusion' : 'Texture' , 
  'skin_sss' : 'Material' , 
  'sta_camera_projection' : 'Texture' , 
  'utility' : 'Texture' , 
  'bump3d' : 'Texture' , 
  'standard' : 'Material' , 
  'complex_fresnel' : 'Texture' , 
  'bump2d' : 'Texture' , 
  'motion_vector' : 'Material' , 
  'hair' : 'Material' , 
  'noise' : 'Texture' , 
  }


si = Application

def add_material( o, shader_name ):
	arnoldPlugin = si.plugins("Arnold Shaders");
	dspresets = XSIUtils.BuildPath( arnoldPlugin.OriginPath, '..', '..', 'Data', 'DSPresets' )
	mat = None
	if shader_name in shader_types:
		preset = XSIUtils.BuildPath( dspresets, 'Shaders', shader_types[shader_name], '%s.Preset' % shader_name )
		mat = o.AddMaterial( preset )
	return mat


mat = add_material( si.Selection(0), 'noise' )

mat.Name = 'My_Noise'