Polygon Islands, arrays, and indices


I was skimming through Guillaume’s posts on polygon islands and arrays, and in particular I was looking at the Get Lowest Point Index by Islands compound. That’s a pretty neat technique for sure, but I had to think about it a bit before I really understood what was going on…

So as an exercise I took a different approach. I used scripting to find the polygon islands and generate an array of island indices for the polygons, and then plugged that into ICE:

For the script, I adapted a script posted by Alan Fregtman to return a list of lists (a list of islands, and for each island, a list of poly indices).

Here’s the code:

from siutils import si		# Application
from siutils import log		# LogMessage
from siutils import disp	# win32com.client.Dispatch
from siutils import C		# win32com.client.constants
 
def getPolygonIslands( o ):
	
	if not o or o.type != "polymsh" :
		log( "Cannot work without an object" )
		return
		
	selFilter = si.Filters("Polygon_Island")
	thisPoly = XSIFactory.CreateActiveXObject("XSI.Collection")

	# List of lists
	islands = []
	
	# List of polygons that have already been checked
	usedArr = []
	
	for poly in o.ActivePrimitive.Geometry.Polygons:
		if poly.Index not in usedArr:
			thisPoly.Add(poly)
			island = selFilter.Subset(thisPoly)
			island = si.Dictionary.GetObject(island).SubComponent.ComponentCollection.IndexArray
			usedArr.extend(island)
			islands.append( island )
			thisPoly.RemoveAll()
			
	return islands
	
islands = getPolygonIslands( si.Selection(0) )

log( islands )
# INFO : [(0, 6, 7, 10, 11), (1, 2, 3, 4, 5, 18, 20, 22), (8, 9, 12, 13, 14, 15, 16, 17, 19, 21, 23, 24)]

# I use a dict instead or pre-initializing a list of the required size
dict = {}
for i in range(len(islands)):
	for j in range(len(islands[i])):
		dict[islands[i][j]] = str(i)
		
log( ",".join( dict.itervalues()) )
# INFO : 0,1,1,1,1,1,0,0,2,2,0,0,2,2,2,2,2,2,1,2,1,2,1,2,2

To get that into an ICE Tree, I put together a little plugin that adds a menu command to the ICE Tree > User Tools menu.

By using a menu item callback, I can get the ICE Tree view from the callback context, and then from the view, the ICE Tree operator via the container view attribute.

I scripted the creation of the little ICE tree, and that’s pretty tedious work. If I was going to distribute something like this, I think I’d include a pre-built compound in an addon.

# GetPolygonIslandsPlugin
# Initial code generated by Softimage SDK Wizard
# Executed Wed Mar 21 06:36:33 EDT 2012 by blairs
# 
from siutils import si		# Application
from siutils import log		# LogMessage
from siutils import disp	# win32com.client.Dispatch
from siutils import C		# win32com.client.C

null = None
false = 0
true = 1

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

	in_reg.RegisterCommand("GetPolygonIslands","GetPolygonIslands")
	in_reg.RegisterMenu(C.siMenuICEViewToolsID,"GetPolygonIslands_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."),C.siVerbose)
	return true

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

	oArgs = oCmd.Arguments
	oArgs.AddWithHandler("object","SingleObj")
	return true

def GetPolygonIslands_Execute( object ):

	Application.LogMessage("GetPolygonIslands_Execute called",C.siVerbose)

	if not object or object.type != "polymsh" :
		log( "GetPolygonIslands: Cannot work without an object" )
		return
		
	selFilter = si.Filters("Polygon_Island")
	thisPoly = XSIFactory.CreateActiveXObject("XSI.Collection")

	# List of lists
	islands = []
	
	# List of polygons that have already been checked
	usedArr = []
	
	for poly in object.ActivePrimitive.Geometry.Polygons:
		if poly.Index not in usedArr:
			log( poly.Index )
			thisPoly.Add(poly)
			island = selFilter.Subset(thisPoly)
			island = si.Dictionary.GetObject(island).SubComponent.ComponentCollection.IndexArray
			usedArr.extend(island)
			islands.append( island )
			thisPoly.RemoveAll()
			
	return islands

def GetPolygonIslands_Menu_Init( in_ctxt ):
	oMenu = in_ctxt.Source
	oMenu.AddCallbackItem( "Get Polygon Islands Index Array", "OnGetPolygonIslands" )
	return true

def OnGetPolygonIslands( in_ctxt ):

	Application.LogMessage("OnGetPolygonIslands called",C.siVerbose)

	view = in_ctxt.GetAttribute( "Target" )
	op = si.Dictionary.GetObject( view.GetAttributeValue( "container" ) )

	log( "Getting polygon islands. This may take a few seconds..." )
	islands = si.GetPolygonIslands( op.Parent3DObject )

	log( "Building polygon island index list" )
	dict = {}
	for i in range(len(islands)):
		for j in range(len(islands[i])):
			dict[islands[i][j]] = str(i)
			
	s = ",".join( dict.itervalues() )
	
	# Temporarily disable command logging
	bLog = si.Preferences.GetPreferenceValue( "scripting.cmdlog" )
	if bLog == True:
			si.Preferences.SetPreferenceValue( "scripting.cmdlog", False )

	# Add String node to ICE tree
	oStringNode = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\StringNode.Preset", op.FullName )
	oStringNode.Parameters( "value_string" ).Value = s

	#
	# Set up ICE branch to set the attributes:
	# self._polyPolygonIslandIndex 
	# self._vertPolygonIslandIndex attributes
	#
	oStringToArray = si.AddICENode("StringToArray", op.FullName)
	si.ConnectICENodes( oStringToArray.InputPorts("Value"), oStringNode.OutputPorts("result") )

	oSelectInArrayNode = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\SelectInArrayNode.Preset", op.FullName)
	oIntegerNode = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\IntegerNode.Preset", op.FullName)
	si.ConnectICENodes( oSelectInArrayNode.InputPorts("array"), oIntegerNode.OutputPorts("result") )
	si.ConnectICENodes( oSelectInArrayNode.InputPorts("array"), oStringToArray.OutputPorts("result") )
	si.DeleteObj( oIntegerNode )
	
	oGet_Polygon_Index = si.AddICECompoundNode("Get Polygon Index", op.FullName)
	si.ConnectICENodes(oSelectInArrayNode.InputPorts("index"), oGet_Polygon_Index.OutputPorts("Polygon_Index"))

	oSetDataNode = si.AddICECompoundNode("Set Data", op.FullName )
	si.SetValue( oSetDataNode.FullName + ".Reference", "self._polyPolygonIslandIndex", "")
	si.AddPortToICENode(oSetDataNode.FullName + ".Value", "siNodePortDataInsertionLocationAfter")
	si.SetValue(oSetDataNode.FullName + ".Reference1", "self._vertPolygonIslandIndex", "")

	si.ConnectICENodes( oSetDataNode.InputPorts("Value"), oSelectInArrayNode.OutputPorts("value") )
	
	oSelectInArrayNode = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\SelectInArrayNode.Preset", op.FullName)
	oSelectInArrayNode1 = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\SelectInArrayNode.Preset", op.FullName)
	oGetDataNode = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\GetDataNode.Preset", op.FullName)
	si.SetValue( oGetDataNode.FullName + ".reference", "this.VertexToPolygons", "")

	si.ConnectICENodes(oSelectInArrayNode.InputPorts("array"), oGetDataNode.OutputPorts("value") )

	si.ConnectICENodes( oSelectInArrayNode1.InputPorts("index"), oSelectInArrayNode.OutputPorts("value") )
	si.ConnectICENodes( oSelectInArrayNode1.InputPorts("array"), oStringToArray.OutputPorts("result") )
	si.ConnectICENodes( oSetDataNode.InputPorts("Value1"), oSelectInArrayNode1.OutputPorts("value") )

	# Put back original pref value for command logging
	si.Preferences.SetPreferenceValue( "scripting.cmdlog", bLog )

	return true

Vector multiplication in ICE versus the Dot Product


In general, there isn’t a unique definition for the multiplication of one vector by another, but there are several common “vector products”, such as the dot product and the cross-product.

The dot product of the two vectors (x, y, z) and (a, b, c) is ax + by + cz.

I kinda assumed that in ICE, the Multiply node would give me the same result, but it turns out that Multiply gives (ax, by, cz):

Finding interior points with valence two


A point with “valence 2” is a point with two incident (neighbour) edges. You could write a script to find these points, but it’s even easier to do with ICE:

Notice that some of the tagged points look like they have more than 2 neighbour edges. What’s happening there is that there are several vertices on top of each other:

Here’s a Python script that builds the ICE tree and then uses it to select all the interior points with valence 2.
hat tip: Fabricio Chamon

I also did this as a custom filter, maybe I’ll post that later.

from siutils import si
from siutils import log		# LogMessage
from siutils import disp	# win32com.client.Dispatch
from siutils import C		# win32com.client.constants

#
# Build the ICE tree that finds the interior points with valence two
#
def BuildICETree( oObject ):
	oIceTree = si.ApplyOp("ICETree", oObject.FullName, C.siNode, "", "", 0)(0)
	oIceTree.Name = "PS_ValenceTwoFilter"

	oAnd = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\CombineLogicNode.Preset", oIceTree )

	#
	# Get self.VertexIsCorner -> Not -> And
	#
	oNot = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\NotNode.Preset", oIceTree )
	oGetVertexIsCorner = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\GetDataNode.Preset", oIceTree )
	oGetVertexIsCorner.Parameters( "Reference" ).Value =  "self.VertexIsCorner"
	si.ConnectICENodes( oNot.InputPorts("Value"), oGetVertexIsCorner.OutputPorts( "value" ) )
	si.ConnectICENodes( oAnd.InputPorts( "Value1" ), oNot.OutputPorts("result") );

	#
	# Get self.VertexToEdges -> Get Array Size -> = -> And
	#
	oGetVertexToEdges = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\GetDataNode.Preset", oIceTree )
	oGetVertexToEdges.Parameters( "Reference" ).Value =  "self.VertexToEdges"
	oArraySize = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\GetArraySizeNode.Preset", oIceTree )
	oCompare = si.AddICENode("$XSI_DSPRESETS\\ICENodes\\CompareNode.Preset", oIceTree )

	si.ConnectICENodes( oArraySize.InputPorts("Array"), oGetVertexToEdges.OutputPorts("value") )
	si.ConnectICENodes( oCompare.InputPorts("first"), oArraySize.OutputPorts("size") )
	oCompare.InputPorts("second").Value = 2

	si.AddPortToICENode( oAnd.InputPorts("Value1"), "siNodePortDataInsertionLocationAfter")
	si.ConnectICENodes( oAnd.InputPorts("Value2"), oCompare.OutputPorts("result") )

	#
	# Set Data -> ICETree
	#
	oSetData = si.AddICECompoundNode("Set Data", oIceTree )
	si.SetValue( oSetData.FullName + ".Reference", "self._PsValenceTwoFlag", "")

	si.ConnectICENodes( oSetData.InputPorts("Value"), oAnd.OutputPorts( "result" ) )

	si.ConnectICENodes( oIceTree.InputPorts("port1"), oSetData.OutputPorts("Execute") )
	si.DisplayPortValues(oSetData.InputPorts( "Value" ), True, 0, True, "", 0, 0, 0, 1, False, True, 1, 0.5, 0, 1, False, 0, 10000, 1, False, False, 0, 10, False, True, False, 100)
	
	return oIceTree

#
# Select all points with the ICE attribute _PsValenceTwoFlag=True
#
def SelectInteriorPoints_with_ValenceTwo( oObject ):
	a = oObject.ActivePrimitive.ICEAttributes("_PsValenceTwoFlag")
	if a is not None:
		d = a.DataArray
		if len(d) > 0 and a.IsConstant == False:
			Application.SelectGeometryComponents( "%s.pnt[%s]" %( oObject.FullName, ",".join(["%s" %(ix) for ix in range(len(d)) if d[ix] == -1])  ) )


#--------------------------------------------------------------
# Select interior points with valence 2
#--------------------------------------------------------------


if si.Selection.Count > 0 and si.ClassName( si.Selection(0) ) != "CollectionItem" :
	oObject = si.Selection(0);
else:
	oObject = si.PickObject( "Pick object" )(2)

if oObject != None and oObject.IsClassOf( C.siX3DObjectID ):
	tree = BuildICETree( oObject )
	SelectInteriorPoints_with_ValenceTwo( oObject )
	si.DeleteObj( tree )

ICE Trigonometry nodes: degrees or radians?


The docs don’t say whether the Trigonometry nodes like Cos and Sin work with degrees or radians, so you have to figure it out yourself. Fortunately, that’s pretty easy to do. You just check which of these returns -1: cos( 180 ) or cos( π) ?

I went a little overboard, but this shows that Cos and Sin expect angular measurements expressed in degrees.

This is based on some basic trigonometry, which can be pretty handy when working with ICE:

  • 2*π radians is equivalent to 360 degrees, so π is 180 degrees.
  • cos( θ ) is the X coordinate of a point on the unit circle, so we know that at 180 degrees (or π if using radians), Cos will return -1.

Aligning particles and making instances look at something


Dealing with particle orientation and rotation can be frustrating, especially when you’re first learning.

Here, I’ve got a bunch of faces instanced onto a disc, and I want all of them to to look the closest point on the grid that’s in the center of the disc. Because the initial local Z axis of some particles faces “away” from the grid, you get can some weird popping and flipping. Here’s an illustration of the problem from an earlier test, when I was aligning the faces to look at a specific point. Notice how only the instances on one side of the null have problems.

So, I use the dot product to determine whether the local Z axis faces “away” from the target grid, and if so, I make an adjustment of 180 degrees, and then the faces align nicely.

A negative dot product tells me that the angle between the local Z axis and the vector to the closest point on the grid is more than 90 and less than 270.

Making a Null follow a particle


Given a particle emission, how do you make a null (or some other 3d object, for that matter) follow a specific particle?

  1. Select the null.
  2. In the ICE toolbar, click Kinematics > Effects > Transform Objects by Particles.
  3. Pick the point cloud.

The null is now constrained to particle 0. There’s an ICETree on the null where you can change the particle index, to constrain the null to a particle with a different ID.

For a more detailed, low-level look into this, see the video ICE Kine – Drive from particles by Eric T.

Evenly distributing points on a sphere with the golden section spiral


This post is based on the recent xsibase thread Golden Section Spiral in ICE, which in turn is based on Patrick Boucher’s 2006 article Points on a sphere.

So…here’s an ICE version of the Golden section spiral algorithm for evenly distributing points on a sphere. It’s a typical example of basic “thinking in ICE”: when you see a for loop in the alogrithm/pseudocode, you should see “in ICE” an array (or data set) flowing through a graph.

Here’s my GoldenSectionSpiral compound (although if you’ve never converted a scripted alogithm to ICE, you should do it yourself just for the exercise 😉 I thought the spiral effect was interesting, so I put in an option to not convert radians to degrees (which is necessary to get the real evenly distributed points).