Scripting shaders

August 10th, 2007 by Bernard Lebel - Viewed 22263 times -




ABSTRACT

This article is sort of a tutorial-cookbook for scripting beginners. It discusses quick and simple techniques to script minor tasks related to shaders. We will try to focus more on Object Model techniques than Command Model ones. Please feel free to contribute to it if you know better!

SHADERS

Shader name spaces
Shaders live in name spaces. In a single name space, no two shaders can have the same name. A bit like objects under models. There are several name spaces available. The material, the override, the camera, the light and the pass are all name spaces. Some name spaces are easier to deal with than others, like materials and lights. We could probably group name spaces in two larger categories: the material type (materials, lights and overrides), and the stack type (passes and cameras). The material type is always easy to handle (well, overrides have some particularities, but we shall address them later). Stack types on the other hand a somewhat problematic, as the way they expose shaders differently depending on the context.

One more name space deserves few lines. This is the TransientObjectContainer. This space is an invisible location where unconnected pieces of data live, like unconnected shaders. Notice that when you create a new shader in the Render Tree and open its property page right away, it’s name is prefixed with “TransientObjectContainer”. As soon as you connect this shader, it is now prefixed with the material name (you may need to re-open the ppg to see the change). When a shader is disconnected from all inputs, it returns to the TransientObjectContainer space. If you close the Render Tree, the shader is destroyed.

The TransientObjectContainer Namespace

One advantage of name spaces is that the shader name is garanteed to be unique within that name space. So if you know the name space of a shader, you can retreive it anyway you like and be sure it’s the right shader. When I talk about the name of a shader, I mean name as in the “Name” property.

Shader fullnames
Dealing with the shader fullname (as in the “FullName” property) is somewhat problematic. Only a handful of shaders have a “unique” fullname, because the fullname of a shader depends entirely from where it is read. In other word, most shaders have multiple fullnames, which are potentially all valid. A shader fullname is like a path in the render tree to that shader. It includes the name of all shaders traversed to reach this shader.

Let’s do a quick exercice.
Create an object. Create a Phong material for this object. Open the render tree for that material. Disconnect the Phong shader from the Shadow input of the material node.
Create a cell shader. Plug it in the diffuse input of the Phong shader, and in the Photon input of the material node.

Then run this code, which prints the name of shader inputs in a render tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xsi = Application
 
def printShaderSources( oShader ):
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				xsi.logmessage( oSource.fullname )
 
				# Parameter has a shader source,
				# call function again with that source
				printShaderSources( oSource )
 
# Call the function using the selected object's material
printShaderSources( xsi.selection(0).material )

This should print:

# INFO : sphere.Material.Phong
# INFO : sphere.Material.Phong.Cell
# INFO : sphere.Material.Cell

At line 16, we print the fullname of the shader currently visited. You can see in the output that we got two different values: sphere.Material.Phong.Cell and sphere.Material.Cell. They are both valid and could be used to retrieve the shader using a string. The first is the result of accessing the shader through the Phong shader, while the second one is because we read it from the material node.

The shaders that have a unique fullname are the material, the override, the light, and basically the root shaders in pass and camera shader stacks.

Collecting shaders the hard way: recursive traversal of the render tree
Aka “walking” the render tree, traversing the render tree is a very common task. There are several ways to do it, it depends on what you want to do.

In “Shader fullnames”, we’ve seen one way of doing it. This way is not clever at all. Shaders will be evaluated as many times as the number of inputs they plug in. In complex render trees, this means lots of cpu cycles can be lost. Worse, if you’re doing things like shader replacement, you may end up replacing the same shader more than once.

The trick is to remember which shader was visited. For the following examples, we’ll use a similar test scene: create a Phong material, plug a Cell shader in in its diffuse input and in the shadow input of the material. But don’t disconnect anything.

If you traverse the render tree without checking if a shader has been visited, you’ll get the same shader printed sevaral times:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xsi = Application
 
def printShaderSources( oShader ):
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				xsi.logmessage( oSource.name )
 
				# Parameter has a shader source,
				# call function again with that source
				printShaderSources( oSource )
 
# Call the function using the selected object's material
printShaderSources( xsi.selection(0).material )

Output:

# INFO : Phong
# INFO : Cell
# INFO : Phong
# INFO : Cell
# INFO : Cell

The simplest way to prevent this is to have a list of all shader names. Each time you visit a shader, check if its name is in the list, if not, evaluate its parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
xsi = Application
 
def printShaderSources( oShader ):
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				# Check if shader was already visited
				if not oSource.name in aShaderNames:
 
					xsi.logmessage( oSource.name )
 
					aShaderNames.append( oSource.name )
 
					# Parameter has a shader source,
					# call function again with that source
					printShaderSources( oSource )
 
aShaderNames = []
 
# Call the function using the selected object's material
printShaderSources( xsi.selection(0).material )

Output:

# INFO : Phong
# INFO : Cell

This approach has one big problem. There is one big exception to the name space rule: in a material, one shader can have the same name as the material. So if you collected the material name before all else and another shader has the same name, it will be skipped. You can deal with this in a plenty of ways, but they all come down to what to do if a name already exists. My suggestion is to simply rename the shader if it’s not of the material type. Append a 1 to its name. So all names are garanteed to be unique.

A very different approach to render tree traversal is to store all shaders in a collection that has the Unique flag enabled. You don’t even have to check if the name exists.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import win32com
xsi = Application
xsifactory = win32com.client.Dispatch( 'XSI.Factory' )
 
def getShaderSources( oShader ):
 
	# Add shader right away to the collection
	oShaders.add( oShader )
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				# Parameter has a shader source,
				# call function again with that source
				getShaderSources( oSource )
 
# Create collection to store shaders of a material
oShaders = xsifactory.createobject( 'XSI.Collection' )
 
# Enable the Unique flag of the collection,
# which means no shader can be stored twice
oShaders.unique = True
 
# Call the function using the selected object's material
getShaderSources( xsi.selection(0).material )
 
# Finally, print all shader names
for oShader in oShaders:
	xsi.logmessage( oShader.name )

Output:

# INFO : Material
# INFO : Phong
# INFO : Cell

Collecting shaders the easy way: FindShaders()
Materials, lights and cameras implemente the FindShaders() method. It allows you to collect all shaders as a flat collection and then get them by name or index, or you can loop over the collection items.

The example in the FindShaders (Material) entry of the documentation is pretty clear on this matter. This method is an alternative to recursively traverse the render tree. I have not tested performance, though.

Storing a material’s render tree into a data structure
This is an expansion of the previous topic. If you ever have to do complex shader replacement, render tree export, or render tree reconstruction, storing the render tree in your own data structure will be essential. In this structure you will store the shaders, as well as the connections that exists between them. The following techniques I propose are based on my experience in this area, it is by no mean an absolute way of doing things.

First, each shader must be uniquely identified. That can be done using their name, although you must make sure that there is no name collision with the material. Personally I prefer to use an integer. The reason is that if you ever have to rename a shader, you may end up having to update the structure to reflect the name change. With an integer you don’t bother about this at all. For the remainder of this topic, I’ll use an integer as the master key for shaders. I’ll refer to this master key as the shader ID.

Using that ID, you’ll be able to store masses of information. You want to record the shader name, the shader object, the parameters that have a shader input, and the ID of the input shaders. The map will look like this:

dShadersByID
{
	id (integer) :
	{
		'name' : shader name (string),
		'object' : shader object (instance),
		'parameters' (dictionary) :
		{
			'parameter name' : shader id,
			'parameter name' : shader id,
			...
		}
	},
	id (integer) :
	{
		'name'  : shader name (string),
		'object' : shader object (instance),
		'parameters' (dictionary) :
		{
			'parameter name' : shader id,
			'parameter name' : shader id,
			...
		}
	},
	...
}

Let’s talk about how to populate this structure. Aside from the actual main structure, I’d have another map, where the the shader name is the key, and its ID is the value. It will be a lot easier to retreive the ID of an existing shader when one is encountered.

When a new shader is visited, each parameter is tested for the presence of a shader source. If one is found, it must be checked if the source has been evaluated already, if so, retreive its ID, and write for that parameter. Otherwise, visit that new shader.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import win32com
c = win32com.client.constants
xsi = Application
 
def getShaderSources( oShader ):
 
	"""
	We assume that when this function is called,
	the passed shader has never been visited.
 
	The function doesn't know if the passed
	shader is just a shader or the material.
	"""
 
	# Get new ID for this shader
	global iID
	iID += 1
	iShaderID = iID
 
	# Map the ID to the shader name
	dShadersByName[oShader.name] = iID
 
	# Store this shader in the main shader structure
	dShadersByID[iID] = {
		'name': oShader.name,
		'object': oShader,
		'parameters': {}
	}
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				# Check if the source is the material and has the same name as the material
				if oSource.type != c.siMaterialType and oSource.name == oMAT.name:
 
					# Rename source
					oSource.name = '%s1' % oSource.name
 
				# Check if that source shader has already been visited
				if oSource.name in dShadersByName:
 
					# Get the source ID
					iSourceID = dShadersByName[oSource.name]
 
					# Store the source ID for this parameter
					dShadersByID[iShaderID]['parameters'][oParameter.scriptname] = iSourceID
 
				else:
					# Source shader has never been visited
					# Visit it, and map its ID to the current parameter
					dShadersByID[iShaderID]['parameters'][oParameter.scriptname] = getShaderSources( oSource )
 
	# Return the shader ID to the caller
	return iShaderID
 
 
 
# Main data structure
dShadersByID = {}
 
# ID mapped to the shader name
dShadersByName = {}
 
# Starting ID
iID = -1
 
# Constants to indicate which shader is the material
oMAT = xsi.selection(0).material
 
# Call the function using the selected object's material
getShaderSources( oMAT )

To verify the dShadersByID structure, you can simply convert it to string and print it:

1
xsi.logmessage( str(dShadersByID) )

Using our example of the Cell shader, the output should be like this (I formatted it so it’s easier to read):

{
	0:
	{
		'object': "COMObject "unknown""(3),
		'name': u'Material',
		'parameters':
		{
			u'shadow': 1,
			u'Photon': 2,
			u'surface': 1
		}
	},
	
	1:
	{
		'object': "COMObject "unknown""(3),
		'name': u'Phong',
		'parameters':
		{
			u'diffuse': 2
		}
	},
	
	2:
	{
		'object': "COMObject "unknown""(3),
		'name': u'Cell',
		'parameters': {}
	}
}

So we got our map of the render tree. We can then use to navigate the render tree and do any operation we want without risking loosing relationships. For instance, if you wanted to replace the Phong shader, you know you have to look for ID 1. Once replaced, you can find in other shaders to which inputs is the phong connected, and perform the connections. You also know, looking at the phong entry, that ID 2 is connected into its diffuse parameter.

The next topics should help you tackle these operations.

Creating a shader
You can create a “floating” shader, that is, a shader object not connected to anything, by using the CreateObjectFromPreset() command.

1
oShader = xsi.createobjectfrompreset( "shaders/material/blinn.preset" )

To find out what string you should use as the first parameter, just create the shader in the Render Tree. This will give you the path of the preset file, and this is all you need to create the shader.
The command also accepts a second argument, for its name. Usually I don’t specify a name unless I’m creating trees that enforce the project standards.

The command returns the shader object. This object lives in the TransientObjectContainer, it’s not connected to anything. The cool thing is that contrary to unconnected shaders in the Render Tree, which are destroyed when the Render Tree is closed, TransientObjectContainer shaders created by script remain there as long as you don’t create a new scene or explicitely remove them. Remember however that because the shader is not connected, it may hard to retrieve if you destroy the variable that holds the command return value.

Connecting a shader
Once you have a shader, you may connect it into a texturable parameter. You can use the SIConnectShaderToCnxPoint() command, but personnally I find the Connect() method of the parameter object to be faster and cleaner. All you need is to have the input parameter as an object.

1
oParameter.connect( oShader )

What I usually do when building shader trees in script is to create all the shaders in one phase (using the CreateObjectFromPreset() command), and then perform all the connections in another phase.

If you are connecting the shader in a rather large amount of parameters, then perhaps using the SIConnectShaderToCnxPoint() command might be faster, as it allows you to pass a list of input parameters:

1
2
3
4
5
6
7
8
9
10
sPhong = 'Sources.Materials.DefaultLib.Scene_Material.Phong'
 
xsi.siconnectshadertocnxpoint(
   oShader.fullname,
    ','.join( [
        '%s.diffuse' % (sPhong,),
        '%s.ambient' % (sPhong,),
        '%s.specular' % (sPhong,)
    ] ),
    False)

Disconnecting a shader
As with connecting shaders, there are two ways to disconnect shaders. The first one is the RemoveShaderFromCnxPoint() command, which works like SIConnectShaderToCnxPoint() command.

The other way is to use the Disconnect() method on the parameter you want to remove the connected shader from.

1
oParameter.disconnect()

Now you have to be very careful: if you disconnect the shader and the shader is no longer connected to any parameter, it is destroyed!

Getting the “sub-parameters” after a disconnection
If you have a compound parameter, like a color or a vector, and you disconnect a shader from it, you won’t be able to access its sub-parameters (like red or x) right away, for some reason. If you try to read or write to the red parameter, for example, you’ll raise an exception.

1
2
oDiffuse.disconnect()
xsi.logmessage( oDiffuse.parameters( 'red' ).value )

Output:

# ERROR : Traceback (most recent call last):
#   File "", line 14, in ?
#     xsi.logmessage( oDiffuse.parameters( 'red' ).value )
# AttributeError: 'NoneType' object has no attribute 'value'
#  - [line 14]

The solution is to “rebind” the variable to the disconnected parameter.

1
2
3
4
5
6
oDiffuse.disconnect()
 
# Rebind parameter and variable
oDiffuse = oDiffuse.parent.parameters( 'diffuse' )
 
xsi.logmessage( oDiffuse.parameters( 'red' ).value )

Output:

# INFO : 0.699999988079071

Thanks to Fran├žois Painchaud for this one.

Getting all shaders of a same kind
A common task is to change the value of a shader parameter globally. For example, you may want to enable elliptical filtering in all image shaders. Materials may have varying render trees, so using overrides would be prove very tedious.

To get all shader instances of a same kind, you can run the command FindObjects(). This is very fast and reliable.

1
oBlinns = xsi.findobjects( None, '{8FAC63AC-E392-11D1-804C-00A0C906835D}' )

This will return a XSICollection containing all shader instances as objects. The first argument is always null or None. The second argument is a shader ClassID

Getting the ClassID of a shader
To know the ClassID of a shader, you can create it in the Render Tree, open its property page, right-click on the space next to the shader name, and choose Edit.

Shader classid

A text page will open either in the script editor or in a floating text editor. The ClassID is generally located at the third line, it starts with Reference.

Reference = "{8FAC63AC-E392-11D1-804C-00A0C906835D}";

If you already have the shader object in your script and want to find out its ClassID, you can use the GetIdentifier() method of the DataRepository object.

1
sClassID = oRepository.getidentifier( oShader, c.siObjectCLSID )

To create a DataRepository instance in Python:

1
2
3
import win32com
xsiutils = win32com.client.Dispatch( 'XSI.Utils' )
oRepository = xsiutils.datarepository

Finding a specific kind of shader in a render tree
We’ve seen pretty much all means of getting and identifying shaders. But you might be looking for shaders of a specific type inside a shader name space. There are several ways to do that:

The first approach is to run FindObjects(). Since it works globally, you’ll have to loop each shader and check if its Root property matches the one we’re looking for. Remember that the Root property suffers from an important limitation that we hinted at in Shader fullnames and shall discuss more in PASSES.

The second approach is to use FindShaders() or recursive traversal of the render tree to collect shaders. Remember however that those last techniques also suffer from certain limitations. FindShaders() works only on materials, cameras and lights, and recursive traversal of the render tree (as it has been presented in this article so far) will work only on materials, lights and overrides. For traversal of other trees you’ll have to make important modifications to your code. More on that in PASSES.

No matter how you collect shaders, you must test their class id against the one you’re looking for.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import win32com
 
c = win32com.client.constants
xsi = Application
xsiutils = win32com.client.Dispatch( 'XSI.Utils' )
oRepository = xsiutils.datarepository
 
sPhongCLSID = '{67120BBE-C98C-11D1-9723-00A0243E3672}'
 
# Get shaders of the selected material
oShaders = xsi.selection(0).findshaders( c.siShaderFilter )
 
# Iterate shaders
for oShader in oShaders:
 
	# Get current shader classid
	sShaderCLSID = oRepository.getidentifier( oShader, c.siObjectCLSID )
 
	# Compare classids
	if sShaderCLSID == sPhongCLSID:
 
		xsi.logmessage( oShader.fullname )

Dealing with texture projections, tangents, and similar shader parameters
Shader parameters that point to scene objects like texture projections, tangent properties, color at vertices, weight maps and the likes, deserve special attention. They are not dealt with using the typical means of evaluating and setting parameters, and may get you stuck easily. Let’s look at how to use each option.

The HasInstanceValue property allows you to know if you’re dealing with that kind of parameter. So if you use very generic code that handle any kind of parameter, that should be the first thing to check.

1
2
if oParameter.hasinstancevalue:
	# do something

The next thing to do is to find out to which scene object is this parameter pointing to. This is where it gets nasty. The SDK doesn’t provide any form of relationship between such parameters and the objects they point to. You have to tests every friggin object you suspect might be used!

What you can do, if you work with a material, is to use its UsedBy property get the objects using that material. Then, you have to find the appropriate cluster properties on those objects. Finally, you can test if those cluster properties are used by the parameter.

Let’s imagine we want to know the texture projections used by a shader parameter named oParameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Get objects using this material
oUsers = oMat.usedby
 
# Loop objects using this material
for oUser in oUsers:
 
	# Loop its clusters
	for oCluster in oUser.activeprimitive.geometry.clusters:
 
		# Check if the cluster is a sample cluster
		if oCluster.type == 'sample':
 
			# Loop the cluster properties of the current cluster
			for oClusterProp in oCluster.localproperties:
 
				# Check if the cluster property is a texture projection
				if oClusterProp.type == 'uvspace':
 
					# Test if the parameter points to this object
					sInstanceValue = oParameter.getinstancevalue( oProjection )
 
					# If returns empty string, it means it does NOT point to that object
					if sInstanceValue != '':
						# do something

Be careful. The GetInstanceValue() method has a significant limitation: it allows you to test against properties of X3DObjects only. If you pass objects like groups or partitions to the GetInstanceValue() method, it will fail. You may pass the members of those objects for the method to succeed.

Setting the parameter to point to cluster properties is done exactly the same way. You have to know in advance what cluster property to use, and then run the SetInstanceValue() method.

The last SDK doc entries worthy of mentioning are IsSupportedInstanceValue (which allows you find out what kind of cluster property does the parameter supports).

MATERIAL LIBRARIES

We can’t talk of materials without talking about material libraries. The two go together. Let’s look at few ways to deal with those.

Getting all material libraries
The object model way:

1
oLibs = xsi.activeproject.activescene.materiallibraries

The command model way:

1
oLibs = xsi.findobjects( None, "{61782E14-9177-412e-BF9B-2B44B9A668DD}" )

Now be careful with the command model way. It may return an extra library that apparently lives in the TransientObjectContainer. If you run this:

1
2
3
4
xsi = Application
oLibs = xsi.findobjects( None, "{61782E14-9177-412e-BF9B-2B44B9A668DD}" )
for oLib in oLibs:
	xsi.logmessage( oLib.fullname )

It may print:

# INFO : Sources.Materials.DefaultLib
# INFO : library_source

I suspect that the second one is an instance of the first, to be used as a pointer to the current library. But what do I know ;-)

Getting a material’s library
If you need to get the library object from the material, simply use the Library property of the material object.

1
oLib = oMaterial.library

Looping materials from libraries
It’s very straightforward:

1
2
3
4
5
6
7
# Loop over all matlibs
for oLib in xsi.activeproject.activescene.materiallibraries:
 
	# Loop materials in the current library
	for oMat in oLib.items:
 
		# do something

MATERIALS

The majority of the work done with shaders will probably be done through materials.

Creating materials
As usual, there are several means for creating materials. If you want to create a material on an object, you can use the ApplyShader() command. This, unfortunately, doesn’t give you control the library the material is created in, it’s always the current library. Alternatively, you could use the AddMaterial() method. Since it’s the Object Model approach, it’s probably going to be faster, especially if the creation is done in a loop.

Personally, I prefer to create the material in a library, and then assign it to the target object. The SICreateMaterial() command allows you not only to specify the library, but also the objects that shall use this material. But it depends a lot on performance, I usually favor the fastest execution.

Assigning materials
If you don’t use the SICreateMaterial() command, you could use SIAssignMaterial() (which is a low-level version of AssignMaterial). You could also use the CopyPaste() command (which is printed when you drag and drop a material onto an object, but personally I prefer more explicit approaches.

Unassigning materials
There is no other way than to use the UnAssignMaterial() and SIUnAssignMaterial() commands.

Getting the material of an object
It may seem very straightforward to do that, but there are a few things to talk about. The first and foremost way to get an object’s material is to use the Material property.

1
oMat = oObject.material

This will work on X3DObjects, polygon clusters, groups, layers and partitions. Basically anything that supports material assignment. However, there is a chance the object it returns is not the one you’re looking for. The Material property returns the “current” material, or that is, the inherited material. If an object is in a group that has a material, the Material property will return the group material, not the object one.

If you’re looking to get the actual material of the object and ignore inheritance, you’ll have to go through the local properties of the object.

1
2
3
4
5
6
7
import win32com
c = win32com.client.constants
xsi = Application
 
for oProp in xsi.selection(0).localproperties:
	if oProp.type == c.siMaterialType:
		xsi.logmessage( oProp.fullname )

The LocalProperties approach will also work on any object that supports material assignment.

Getting the objects using a material
A material has the UsedBy property, which returns all objects using that material.

1
xsi.logmessage( oMat.UsedBy.getastext() )

The UsedBy property, however, has always suffered from severe limitations. In the past, it would return only X3DObjects. Now it also returns polygon clusters. However, it won’t return groups, layers and partitions using that material. Instead, it will print the X3DObjects and clusters that have inherited the material from the group/layer/partition.

If you want to find the groups/layers/partitions using that material, you’ll have to look the owners of the material. The first owner is always the material library. The other owners are the objects using this material. In the case of a material assigned to a group:

1
2
3
oMat = xsi.selection(0)
for oOwner in oMat.owners:
	xsi.logmessage( '%s - %s' % (oOwner.fullname, oOwner.type) )

Output:

# INFO : Sources.Materials.MatLib - library_source
# INFO : Group - #Group

Deleting materials
To my knowledge, there is no other way to delete a material than to use the DeleteObj() command.

Shotcuts to access material components
The object model offers a few shortcuts to retrieve data of a material. You should read the Material and Shader entries of the documentation to find out about those.

PASSES

Passes are special. They support shaders through their stacks. However, passes cannot be dealt like with like materials. Two situations come to mind, both being somewhat related.

In the first situation, if you take the script presented in “Storing the render tree into a data structure”, it will fail if you supply the pass object as the starting value for the function. Passes do not allow accessing shaders through its Parameters, unlike materials. So you have to do clever things to get stack shaders from the stack.

The second situation is the opposite: if you use the Root property of a shader to get to the, well, shader tree root, you’ll get the pass object. This is wrong, for the very same reason as the preceding examples, that is, passes do not expose shaders through their Parameters.

To make things worse, if you open the render tree with a pass selected, it looks like the pass is a materials and shaders plug directly into them. This is a lie. And to seal the deal, the render tree view shows only the first shader of each stack, even though the stacks support multiple shaders at the top level. Another lie. Fortunately, the Object Model helps solving those problems.

Accessing shader stacks from passes
There is no shortcut to access a pass stack. You have lookup the pass’s NestedObjects, using the stack name. Once you get the stack object, you can check its NestedObjects to see if there are shaders. This will return a collection of the “top” shaders of the stack.

Create a pass with an environment shader, selected that pass, and run this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xsi = Application
 
aStackNames = [
	'Environment Shader Stack',
	'Output Shader Stack',
	'Volume Shader Stack'
]
 
# Get selected pass
oPass = xsi.selection(0)
 
# Iterate pass stacks
for sStackName in aStackNames:
 
	# Get the stack object
	oStack = oPass.nestedobjects( sStackName )
 
	# Check if the stack has shaders
	if oStack.nestedobjects.count > 0:
 
		# Iterate root shaders of this stack
		for oStackShader in oStack.nestedobjects:
 
			xsi.logmessage( oStackShader.fullname )

Unfortunately, you cannot use reliable string searching. Select the Environment shader you created. It should print this line in the History Log:

1
Application.SelectObj("Passes.Default_Pass.Environment", "", "")

Now, rename the shader to something already in use in that pass, like “Background_Objects_Partition”. Select the shader again. It should print this:

1
Application.SelectObj("Passes.Default_Pass.EnvironmentShaderStack.Background_Objects_Partition", "", "")

So now you got the stack name, probably to avoid name collisions in the pass name space. Because this behavior is not constant, how can you reliably use strings to find pass shaders? If Softimage reads this, I politely invite you to correct this by putting the stack name as at all times. That will save us, poor coders, lots of trouble.

Getting the “root” shader from a shader
As previously mentioned, the Root property of a shader returns the pass object, which can be incorrect depending on how close to reality you want the information to be. To me this is very incorrect. In this case, the “root” shader is a shader visible in the stack. Because you can put multiple shaders in a stack, it means that there can be an infinite amount of root shaders for the the pass.

Walking down the render tree (down being “away from the root shader”) is easy, but walking up is not so. I don’t know any fast, clever, or elegant way of doing this. The only constant is that no two shaders can be named the same way in the pass name space(1).

For the following chunk of code, do this:

  • Put an Environment shader on the default pass.
  • Open the render tree, and connect a Cell shader into the tex input of the Environment shader.
  • Then, create a _2D_background_color shader (output shader)
  • Create another Cell shader, and plug it in the bg input of the _2D_background_color shader(2). Normally, the second Cell shader should have been renamed Cell1.

To find the root shader for a given shader, I’ve written a sort of state machine that remembers in which stack and root shader it is walking down. Run this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import win32com
 
c = win32com.client.constants
xsi = Application
xsifactory = win32com.client.Dispatch( 'XSI.Factory' )
 
 
class PassShaderState:
 
	def __init__( self, sPassName, sShaderName ):
 
		# CONSTANTS
 
		# Name of the shader we're looking for
		self.passname = sPassName
		self.shadername = sShaderName
 
		# Stack names
		self.stacknames = [
			'Environment',
			'Output',
			'Volume'
		]
 
 
		# VARIABLES
 
		# Shader identification
		# All point to scene objects
		self.currentstack = None
		self.currentroot = None
		self.currentshader = None
 
 
 
 
	def getroot( self ):
 
		# Get the pass object
		oColl = xsifactory.createobject( 'XSI.Collection' )
		oColl.items = 'Passes.%s' % self.passname
 
		# Did we find the pass
		if oColl.count

Storing the pass shaders into a data structure
We’ve seen already how to store a material’s render tree into a data structure. The presented approach cannot be used without modifications for pass shaders, again because of the foggy relationship that exists between the pass object, its stacks and its root shaders. Here I will not present code to achieve this task (as my solution is extremely elaborate), but rather explain what approach I use to do it. My goal was to have the ability to re-create the pass shaders simply by reading a flat XML list of shaders.

With a material it’s simple: when you get to the material, you can create anything you want the same way, and plug anything you want. With shader stacks it’s different.

The data structure I presented above works, but will need extra informations. Each shader will have to be flagged as being a stack root shader or not. The reason is that creating stack root shaders requires can only be done through the SIApplyShaderToCnxPoint() command, while creating the shaders underneath use the CreateObjectFromPreset() command, presented earlier.

Root shaders should state to which stack they belong (including the pass name, of course). When creating shaders with SIApplyShaderToCnxPoint(), you must specify the shader stack. Having this information already available as a string will make it easy. Btw, in the data structure, store the Type property of the stack, not its Name.

CAMERAS

The camera is similar to the pass, with a significant difference: it has only one stack. Once the camera shader stack is accessed, code that handles pass shader stacks should work with the camera shader stack. A word of advice: while I’m not aware of any such plans from Softimage, it would be a good idea to design your code to assume that there might be more than one stack in a camera. Let’s imagine Softimage, in some near future, adds a new stack to the camera…….

LIGHTS

Lights can be handled pretty much like a material. For instance, you can traverse the light’s render tree using the same code we used with the material one, but pass it the light’s primitive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xsi = Application
 
def printShaderSources( oShader ):
 
	# Loop parameters of the shader
	for oParameter in oShader.parameters:
 
		# Check if the parameter has a source
		oSource = oParameter.source
		if oSource != None:
 
			# Check if classname of the source is a shader
			sClassName = xsi.classname( oSource )
			if sClassName == 'Shader' or sClassName == 'Texture':
 
				xsi.logmessage( oSource.name )
 
				# Parameter has a shader source,
				# call function again with that source
				printShaderSources( oSource )
 
# Call the function using the selected light's primitive
printShaderSources( xsi.selection(0).activeprimitive )

Output:

# INFO : soft_light
# INFO : soft_light

Additionnally, the light object (not the primitive) have a Shaders property. This will access the shaders connected into the primitive. If you want to access the soft_light shader (or any shader connected into the light’s primitive), use:

1
xsi.logmessage( xsi.selection(0).shaders(0).fullname )

OVERRIDES

Overrides are another beast. Their implementation looks and behaves similarly to other shader implementations in XSI, although there are several differences that need to be taken care of. Here we’ll talk about creating overrides by script.

Shader overrides
You’ve probably noticed that shader overrides use strings separated by dashes. I suspect this is because the dot character is reserved to namespace notation, having it in a parameter script name would break things. Also you deal with relative names, where the material name is kind of masked by “material”. For instance, if you override the Surface input of a material, the created parameter looks like “material-surface”.

If you want to re-create a shader parameter in an override, you’ll have to format that string a little.
First, replace the dashes by dots, obviously.
Second, you’ll have to find a material among the group/layer/partition members that has the parameter you want to override, and use the material name in replacement of “material”. So “material-surface” become something like “cylinder.Material.surface”.

Absence of members
The #1 problem, in my opinion, is that you can’t create an override group/layer/partition doesn’t expose the parameter you want to override. This makes re-creating overrides by script very difficult. I know 3 solutions to this:

  • Save a preset of the override. Probably the most reliable way to do it, however it requires that you create the override at least once and store a preset. Another big advantage is that this approach works even if the group/layer/partition has no members to override.
  • Create temporary objects. This is easy if you have to overrides parameters such as visibility or geometry approximation, but for shaders it can become extremely elaborate. Definitely consider other solutions.
  • Wait for the group/layer/partition to be populated before attempting to create the override parameters. Better than #2, but still not 100% reliable.

Refresh issues
Another problem is that when you manage to create a parameter in an override, that parameter is not always immediately available through the object model. For reasons I don’t have a clue about, there are situations where if you try to reach the created parameter through the Parameters property of the override, it would fail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xsi = Application
 
# Get the selected override
oOverride = xsi.selection(0)
 
# Define parameter to override
sParameterName = 'viewvis'
sParameterFullName = 'cube.visibility.%s' % sParameterName
 
# Create the override entry
xsi.SIAddEntryToOverride( oOverride, sParameterFullName )
 
# Get the created override
oOverride.parameters( sParameterName ).value = True

The solution is that once the parameter is created, use a string to retrieve it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xsi = Application
 
# Get the selected override
oOverride = xsi.selection(0)
 
# Define parameter to override
sParameterName = 'viewvis'
sParameterFullName = 'cube.visibility.%s' % sParameterName
 
# Create the override entry
xsi.SIAddEntryToOverride( oOverride, sParameterFullName )
 
# Get the created override
oColl = xsifactory.createobject( 'XSI.Collection' )
oColl.items = '%s.%s' % ( oOverride.fullname, sParameterName )
oParameter = oColl(0)
oParameter.value = True

FURTHER SCRIPTING

There are many more topics we could cover, including texture layers, OGLTextures, shader input type, the ProgID, etc. These are all things I’m not deeply familiar with so I’ll leave it to you to learn about those.

NOTES

(1) XSI 6.01 had this nasty bug. If you select the pass, open the render tree and start connecting shaders all over the place, they would all have a unique name. So far so good. But if you selected one of the stack shaders, open the render tree and connected shaders from there, you could name a shader the same as in another stack. In the end, it means that several shaders could have the same name in the pass name space. This was corrected in XSI 6.02.
(2) If your _2D_background_shader doesn’t expose a bg input, you can modify the spdl. Simply enable the texturable flag.
(3) The real value is “COMObject “unknown”", where ” are replaced by “smaller than” and “greater than” tags.

7 Responses to “Scripting shaders”

  1. Woah! Another mammoth post! Thanks for taking the time to share your material scripting experiences.

  2. Michal says:

    Thank you Bernard, very informative and useful.

  3. Michael says:

    Is there an easy way to find out in which slot the Cell Node is connected in the “The simplest way to prevent…” example. A linux way please :)

  4. Jentzen Mooney says:

    Bernard, Thank you for taking the time and share.. I look forward to reviewing in full during the week.
    I love XSI-Blog keep it going guys!

  5. Michael >>> Is there an easy way to find out in which slot the Cell Node is connected in the “The simplest way to prevent…” example. A linux way please :)

    I’m not sure what you mean by “simplest way to prevent….”

    I don’t know any simple of doing what you’re after. This is what I refer to in “”Getting the “root” shader from a shader”". It’s super easy to walk down the render tree (going from output to inputs, or from root to branches) but it’s hard to walk up (from branches to root). I’ve never found a SDK entry that discusses output-input relationship between shaders.

    My best advice, unfortunately, would be to either loop the shaders after a FindShaders(), or recursively traverse the render tree, until you find parameters that have a cell shader connected into them.

  6. Michael says:

    Thanks, I was refering to the 5th code block (it’s locate under the “simplest way to prevent…” sentence). I thought there would be some kind of shader.checkDiffuseSlot() which would return NULL or Cell/Image/… (that was is connected to this slot). I will try and use the recursive traverse to check this.

  7. Btw, something I forget to say in the article: you can get classid of objects using the SDK explorer, in the advanced tab.

    With shaders it’s more tedious, as you might have to dig deep into the Explorer to find the shader. The method I described with editing the spdl is probably faster than the SDK Explorer with shaders.