Emitting strands from a polygon cluster


Here’s a simple ICE tree that emits strands from a polygon cluster.
emit_strands
A general technique for these kinds of filtered emissions is to delete the points you don’t want. In this case, we check Cluster.IsElement. The tricky part is that Cluster.IsElement is a bool-per-polygon attribute, so we need to get into a per-point context. To do that, we get the emit location, which is per-point, and then at that location, the value of Cluster.IsElement. Now we’re in a per-point context, and we can use those per-point boolean values to delete points.

Note the use of Not instead of an If. We know IsElement is False for points that were not emitted from the polygon cluster, so we can logically negate it with Not to get True and feed that into Delete Point.

In pseudocode, we do this:

if not( IsElement ) delete point

instead of

if (IsElement == False) then delete point

Of course, for filtering we could just drill down into Emit Strands and set the filter on the Generate Sample Set nodes.
generatesampleset_filter

Finding empty polygon meshes


Now that there are intrinsic ICE attributes like NbPoints and NbPolygons, there a couple of ways you can check for an empty mesh:

# Assume a polymesh is selected...
o = Application.Selection(0)

# Check the intrinsic ICE attribute
nb =  o.ActivePrimitive.ICEAttributes("NbPoints")
print nb.IsDefined and nb.DataArray[0] <= 0

# Check using the object model
print o.ActivePrimitive.Geometry.Points.Count <= 0

The typical way to package this up for users it to define a filter. Then a user just has to select the filter and press CTRL+A. Here’s the Match callback for a filter that finds empty polygon meshes. Note that I switched to checking the number of polygons. That way, if somehow there was something weird like a mesh with just one point, you’d still find it.

The intrinsic attribute NbPolygons should always exist, but just to be sure I check IsDefined, and if that is False, I fall back to checking Geometry.Polygons.Count.

# Match callback for the EmptyPolygonMesh custom filter.
def EmptyPolygonMesh_Match( in_ctxt ):
	Application.LogMessage("EmptyPolygonMesh_Match called",constants.siVerbose)

	o = in_ctxt.GetAttribute( 'Input' )
	if o.type == 'polymsh':
		nb =  o.ActivePrimitive.ICEAttributes("NbPolygons")
		return (nb.IsDefined and nb.DataArray[0] <= 0) or o.ActivePrimitive.Geometry.Polygons.Count <= 0
	else:
		return False

I packaged the filter as an addon. Get it here.

Selecting the object master for a clone


Here’s a little addon that adds a Select Clone Object Master command to the viewport context menu for 3d objects.


The addon is a single self-installed plugin that includes a command, a menu, and a filter:

  • The menu item calls the command.
  • The filter controls whether or not the menu item is enabled…so that the Select Clone Object Master command is enabled only when you ALT+right click a clone.
# SelectCloneMasterObjectPlugin
# Initial code generated by Softimage SDK Wizard
# Executed Thu Jun 7 10:55:31 EDT 2012 by blairs
# 
# Tip: To add a command to this plug-in, right-click in the 
# script editor and choose Tools > Add Command.
import win32com.client
from win32com.client import constants

null = None
false = 0
true = 1

def XSILoadPlugin( in_reg ):
	in_reg.Author = "blairs"
	in_reg.Name = "SelectCloneMasterObjectPlugin"
	in_reg.Major = 1
	in_reg.Minor = 0

	in_reg.RegisterCommand("SelectCloneMasterObject","SelectCloneMasterObject")
	in_reg.RegisterFilter("Clone",constants.siFilter3DObject)
	in_reg.RegisterMenu(constants.siMenu3DViewObjectSelectContextID,"SelectCloneMasterObject_Menu",false,false)
	#RegistrationInsertionPoint - do not remove this line

	return true

def XSIUnloadPlugin( in_reg ):
	strPluginName = in_reg.Name
	Application.LogMessage(str(strPluginName) + str(" has been unloaded."),constants.siVerbose)
	return true


# Match callback for the CloneFilter custom filter.
def Clone_Match( in_ctxt ):
	Application.LogMessage("Clone_Match called",constants.siVerbose)
	o = in_ctxt.GetAttribute( "Input" )
	c = win32com.client.Dispatch( "XSI.Collection" )
	c.Items = '%s.%s' % (o.ActivePrimitive.FullName, 'CopyOp')
	return c.Count > 0
# 	Return value indicates if the input object matches the filter criterias.


def SelectCloneMasterObject_Init( in_ctxt ):
	oCmd = in_ctxt.Source
	oCmd.Description = ""
	oCmd.ReturnValue = true

	oArgs = oCmd.Arguments
#	oArgs.AddWithHandler("Arg0","Collection")
	oArgs.AddWithHandler("Arg1","SingleObj")
	return true

def SelectCloneMasterObject_Execute( Arg1 ):

	Application.LogMessage("SelectCloneMasterObject_Execute called",constants.siVerbose)
	Application.Selection.Clear()

# by Vladimir Jankijevic
# https://groups.google.com/forum/?fromgroups#!searchin/xsi_list/Finding$20the$20source$20mesh$20of$20a$20clone/xsi_list/cyUYARDMooA/J3JVxc6jJtIJ
	copyop = Arg1.ActivePrimitive.ConstructionHistory[0]
	Application.Selection.Add(copyop.InputPorts[0].Target2.Parent3DObject)

	# 
	# TODO: Put your command implementation here.
	# 
	return true

def SelectCloneMasterObject_Menu_Init( in_ctxt ):
	oMenu = in_ctxt.Source
	oMenu.AddCommandItem("Select Clone Master Object","SelectCloneMasterObject")
	oMenu.Filter = "Clone"
	return true

New in Softimage 2013: Filtering XSICollections


In Softimage 2013, the XSICollection object now has a Filter method, so you can filter XSICollections by type, family, or path name.

oFilteredCollection = XSICollection.Filter( [Type], [Families], [Path] )

I updated my find all cameras script to use XSICollection.Filter, and here’s the timing results (form the same scene, but in 2013).

# INFO : getCameras_FindObjects finished in 0.033000 seconds
# INFO : Found 8301 cameras
# INFO : getCameras_FindObjects_w_Filter finished in 0.269000 seconds
# INFO : Found 24 cameras
# INFO : getCameras_FindObjects_w_SIFilter finished in 0.044000 seconds
# INFO : Found 24 cameras
# INFO : getCameras_FindObjects2 finished in 0.001000 seconds
# INFO : Found 49 cameras
# INFO : getCameras_FindObjects2_w_Filter finished in 0.003000 seconds
# INFO : Found 24 cameras
# INFO : getCameras_FindChildren2 finished in 0.149000 seconds
# INFO : Found 24 cameras
# INFO : getCameras_SelectAllUsingFilter finished in 0.035000 seconds
# INFO : Found 24 cameras
  • FindObjects2 is fastest, with SelectAllUsingFilter a mildly surprising second (again note that I’m calling SelectAllUsingFilter with AffectSelectionList=False, so I’m not actually selecting anything).
    FindObjects2_w_Filter		0.003000 seconds
    SelectAllUsingFilter		0.035000 seconds
    FindObjects_w_SIFilter		0.044000 seconds
    FindChildren2			0.149000 seconds
    FindObjects_w_Filter		0.269000 seconds
    
  • FindObjects finds a lot more than just cameras; it finds lots of nulls too because that GUID isn’t unique to cameras
  • In this context, SIFilter is faster than the XSICollection.Filter method

Here’s the updated script for Softimage 2013:

import time
si = Application
log = si.LogMessage
from win32com.client import constants as C

import win32com.client
oCameraColl = win32com.client.Dispatch( "XSI.Collection" )

si.SetValue("preferences.scripting.cmdlog", False, "")


def timeExecution(func):
    def closure(*args, **kwargs):
        startTime = time.time()
        try:
            ret = func(*args, **kwargs)
        except Exception, e:
            delta = time.time() - startTime
            log('Failed in %f seconds' % delta)
            raise
        delta = time.time() - startTime
        log('%s finished in %f seconds' % (func.__name__, delta))
        return ret
    return closure 

@timeExecution
def getCameras_FindObjects():
	oCameraColl = Application.FindObjects( "", "{5FC0CCAE-3DC8-11D0-9449-00AA006D3165}" )
	return oCameraColl.Count

@timeExecution
def getCameras_FindObjects_w_Filter():
	oCameraColl = Application.FindObjects( "", "{5FC0CCAE-3DC8-11D0-9449-00AA006D3165}" )
	oCameraColl = oCameraColl.Filter( "camera" )
	oCameraColl.RemoveItems( oCameraColl.Filter( "", "", "CopyPaste*" ) )
	oCameraColl.RemoveItems( oCameraColl.Filter( "", "", "View*" ) )
	return oCameraColl.Count

@timeExecution
def getCameras_FindObjects_w_SIFilter():
	oCameraColl = Application.FindObjects( "", "{5FC0CCAE-3DC8-11D0-9449-00AA006D3165}" )
	oCameraColl = si.SIFilter( oCameraColl, "camera" )
	oCameraColl.RemoveItems( oCameraColl.Filter( "", "", "CopyPaste*" ) )
	oCameraColl.RemoveItems( oCameraColl.Filter( "", "", "View*" ) )
	return oCameraColl.Count


@timeExecution
def getCameras_FindObjects2():
	c = si.FindObjects2( C.siCameraID )
	return c.Count

@timeExecution
def getCameras_FindObjects2_w_Filter():
	cams = si.FindObjects2( C.siCameraID )
	oCameraColl.Items = cams
	oCameraColl.RemoveItems( cams.Filter( "", "", "CopyPaste*" ) )
	oCameraColl.RemoveItems( cams.Filter( "", "", "View*" ) )
	return oCameraColl.Count


@timeExecution
def getCameras_FindChildren2():
	cams = si.ActiveSceneRoot.FindChildren2("", "camera")
	return cams.Count

@timeExecution
def getCameras_SelectAllUsingFilter():
	cams = si.SelectAllUsingFilter("Camera", "siIgnoreComponentVisibility", False, "")
	return cams.Count



@timeExecution
def getCameras_Model_FindObjects():
	cams = si.ActiveSceneRoot.FindObjects( C.siCameraID )
	return cams.Count


log( 'Found %d cameras' % getCameras_FindObjects() )
log( 'Found %d cameras' % getCameras_FindObjects_w_Filter() )
log( 'Found %d cameras' % getCameras_FindObjects_w_SIFilter() )
log( 'Found %d cameras' % getCameras_FindObjects2() )
log( 'Found %d cameras' % getCameras_FindObjects2_w_Filter() )
log( 'Found %d cameras' % getCameras_FindChildren2() )
log( 'Found %d cameras' % getCameras_SelectAllUsingFilter() )

Selection filter for objects with ICE trees


Here’s a custom filter for selecting objects with ICE trees: psCustomFilters.xsiaddon. The filter appears as Obj_w_ICETree in the filter list of the Select panel in the MCP:

You can use this filter in the MCP Select panel (choose the filter and then press Ctrl+A to select all objects with an ICE tree).

Or you could make a toolbar button with these snippets:

// JScript
SelectAllUsingFilter("Obj_w_ICETree", siCheckComponentVisibility, null, null);
#Python
from siutils import C		# win32com.client.constants
Application.SelectAllUsingFilter("Obj_w_ICETree", C.siCheckComponentVisibility, "", "")

Beware of ICE optimizations


It seemed like such a nice, simple way to filter polygons:

  • Use ICE to check the polygon area and then set a boolean attribute.
  • Write a custom subcomponent filter to filter based on that ICE attribute.

With ICE, it’s pretty easy to check if the PolygonArea is within a certain range:

But, beware of ICE optimizations!

Because that ICE attribute isn’t used anywhere else in the scene, ICE doesn’t evaluate that tree, so my boolean attribute is never defined, and my custom filter therefore fails. I have to do something like Show Values to force evaluation:

Note: In Show Values, I used Show Values for Tagged Components Only to cut down the visual clutter.

FWIW, here’s a Python example of a custom subcomponent filter:

# psCustomFilter Plug-in
# Initial code generated by Softimage SDK Wizard
# Executed Wed Nov 23 11:31:56 EST 2011 by blairs
# 
# Tip: To add a command to this plug-in, right-click in the 
# script editor and choose Tools > Add Command.
import win32com.client
from win32com.client import constants

from siutils import si		# Application
from siutils import sidesk	# Desktop
from siutils import sidict	# Dictionary
from siutils import sifact	# XSIFactory
from siutils import simath	# XSIMath
from siutils import siproj	# ActiveProject2
from siutils import sisel	# Selection
from siutils import siuitk	# XSIUIToolkit
from siutils import siut	# XSIUtils
from siutils import log		# LogMessage
from siutils import disp	# win32com.client.Dispatch
from siutils import C		# win32com.client.constants


null = None
false = 0
true = 1

def XSILoadPlugin( in_reg ):
	in_reg.Author = "blairs"
	in_reg.Name = "psCustomFilter Plug-in"
	in_reg.Major = 1
	in_reg.Minor = 0

	in_reg.RegisterFilter("psCustomFilter",constants.siFilterSubComponentPolygon)
	#RegistrationInsertionPoint - do not remove this line

	return true

def XSIUnloadPlugin( in_reg ):
	strPluginName = in_reg.Name
	Application.LogMessage(str(strPluginName) + str(" has been unloaded."),constants.siVerbose)
	return true

# Match callback for the psCustomFilter custom filter.
def psCustomFilter_Match( in_ctxt ):
	Application.LogMessage("psCustomFilter_Match called",constants.siVerbose)

# 	Return value indicates if the input object matches the filter criterias.
	return true

# Subset callback for the psCustomFilter custom filter.
def psCustomFilter_Subset( in_ctxt ):
	log("psCustomFilter_Subset called",constants.siVerbose)

	out_coll = disp( "XSI.Collection" )
	
	in_coll = in_ctxt.GetAttribute( "Input" )
	for item in in_coll:
		log( item )
		polys = []
		for p in item.SubComponent.ComponentCollection:
			log( p.Index )

			attr = p.Parent.ICEAttributes("psCustomPolyFilter")
			if not attr.IsDefined:
				log( "Cannot apply filter. psCustomPolyFilter attribute is not defined" )
			if attr.IsDefined and attr.DataArray[ p.Index ] == -1:
				#log( "%d : %s" % ( p.Index, attr.DataArray[ p.Index ] ) )
				polys.append( p.Index )

		if len(polys) > 0:
			out_coll.Add( item.SubComponent.Parent3DObject.ActivePrimitive.Geometry.CreateSubComponent(C.siPolygonCluster, polys ) )


	in_ctxt.SetAttribute( "Output", out_coll )

# 	Return value indicates if a subset of the input objects matches the filter criterias.
	return true