Custom Operators (XSISDK)


Table of contents

Introduction

For an introduction to the XSI operator graph check out Operator Graph Primer.


Self-installing, Runtime versus SPDL

XSI currently supports 3 types of custom operators: self-installing, runtime, and SPDL.

As of XSI 5.1 self-installing plugins support customoperators. This is the prefered mechanism for deploying customoperator across a production.

A runtime operator is an operator that is entirely embedded inside the scene file. All the script code of the operator is stored directly inside the operator. (C++ cannot be used for runtime operators)

With a SPDL-based operator is an operator that is installed into the system. It uses a SPDL file to define the connections, parameters, UI and implementation of the operator. A Preset file is generated based on the SPDL file. If it is implemented in scripting then the code is directly stored in the SPDL, otherwise the SPDL refers to a .DLL or .SO file based on the C++ API.

Self-Installing Operators

Self-installing operator are defined in either compiled library (DLL or DSO) and script file living in the Application\plugins folder of the user, workgroup or factory.

The registration of the self-installing operator happens in the self-installing plugin entry callback XSILoadPlugin as illustrated below.

   function XSILoadPlugin( in_reg )
   {
       in_reg.RegisterOperator("myexpr");

Defining a self-installing operator is pretty much the same as a runtime operator. The definitions of connections happen outside the custom operator typicalling in a custom command. NOTE: The custom operator will create the necessary code for you.

The _Update callback on self-installing operators is differant from SPDL or runtime operators. It only takes 1 argument, a OperatorContext object. This argument is more like the other self-installing plugin context objects than the UpdateContext object used by the runtime or SPDL custom operators. It has convenience methods for quickly accessing input and output values as will as parameters. If you want to get a handle on the Operator object you access it via the OperatorContext.Source property as illustrated below.

   function myexpr_Update( ctxt )
   {
       var operator = ctxt.Source;

Runtime Operators

If you use the Scripted Operator user interface to create an operator then you have created a Runtime operator.

This is the most convenient way to get up and running with scripted operators. It has the advantage of being self-contained inside the scene file, so it can be moved from machine to machine without needing to worry about whether all the necessary plug-ins are installed or not.

The scripted operator user interface is rather basic for building operators. For example it does not expose the concept of groups (described below), and offers a basic script editor window. However this does not mean that runtime operators are limited.

Starting with version v4.0 it has been possible to build runtime operators using the object model. With methods like XSIFactory.CreateScriptedOp (http://softimage.wiki.avid.com/sdkdocs/XSIFactory_CreateScriptedOp.htm) it is possible to build sophisticated operators. This is a more flexibile and powerful way to build operators than the scripted operator user interface. This technique, including an example using the C++ API, is demonstrated in the Dynamic Operators example in the XSI 5.0 Example Workgroup.

However because each runtime operator contains its entire implementation you will run into problems if you have created many copies of it and want to fix a bug. (A SPDL based operator stores its code in a centralized fashion so it does not have this problem).

Centralized Implementation for an Runtime operator

It is possible for a run-time operator to call code in a custom command as a way of centralizing the code. The _Update function just calls directly to a Custom Command, passing all its arguments. As long as the command is set not to log itself and it doesn't break any of the rules about modifying scene content this is a legal workaround. See the SplatterCpp example which is part of the Dynamic Operators example in the SDK example workgroup.

There is some overhead to calling a custom command, so a SPDL based custom operator may be a slightly faster alternative for centralizing the implementation.

SPDL

SPDL based operators have some advantages over run-time operators:

  • Unique GUID makes it easier to find instances in a scene (with the FindObjects command), see example below.
  • Able to have custom UI layout and logic
  • Centralized implementation including C++ API support

However there is a learning challenge to understand the SPDL file format and to learn the workflow for modifying the SPDL.

A common workflow for creating a SPDL based operator is to first develop a runtime operator as a prototype. Once it is working well in terms of the connections and rough behavior you can convert it to a SPDL using XSIUtils.WriteSPDL (http://softimage.wiki.avid.com/sdkdocs/XSIUtils_WriteSPDL.htm).

Understanding the Guids in the SPDL

The simple answer is that you should be able to avoid needing to have a detailed understanding of them. For example if you use a script to generate the operator initially as a run-time operator, get it fully working, and then save it as a SPDL file, all the GUIDs will be filled in by XSI on your behalf. It is only when you edit the SPDL file directly that the meaning of each GUID becomes interesting.

Connections

Understanding Ports and Groups

Ports

Each operator has at least one output port and zero or more input ports.

Each port has a direction, so to read and write an particular object requires two ports (an input and an output). Deformation operators are examples that read an existing geometry and deform it via an input and output port.

Port connections are made to the specific data that needs to be read or written. Valid objects to connect with include any Property, ClusterProperty, KinematicState, Primitive or Parameters. The connection needs to go to the specific data within the X3DObject, not to the X3DObject itself. To help enforce this rule the ObjectModel prevents access to certain methods and properties designed to traverse through the nested data of an X3DObject. For example, Geometry.Clusters is blocked inside Custom Operators because the operator is expected to connect to each cluster that it wants to access.

One of the main tasks of building a custom operator is to define the ports so that all the necessary input data can be read and the result of the calculations used to update the output object(s).

Port Indexing

The Operator.PortAt method can be used to retrieve a port by its index. However it is important to understand that the index of an operator is calculated with all the input ports coming before all the output ports. So even if CustomOperator.AddOutputPort is called before CustomOperator.AddInputPort, the input port will have an small index value than the output port. Furthermore, this implies that the Port.Index property of the Port returned by CustomOperator.AddOutputPort cannot be trusted, because any Input ports added after the fact will change this index.

Applying Operators at Port Level

The documentation says it's possible to apply an operator (custom or not custom) at port level using square brackets. What it doesn't say is that this is only available using the ApplyOperator command (not in ApplyOp). Here is an example of how to use this to connect a Lattice on a primitive and on then on a cluster:

GetPrimLattice(null, null, null, null, null);
CreatePrim("Sphere", "MeshSurface", null, null);
SelectGeometryComponents("sphere.pnt[9-29]");
CreateCluster(null);
SelectGeometryComponents("lattice.pnt[(0,2,0),(0,2,1),(0,2,2),(0,2,3),(0,2,4),(0,3,0),(0,3,1),(0,3,2),(0,3,3),(0,3,4),(0,4,0),(0,4,1),(0,4,2),(0,4,3),(0,4,4),(1,2,0),(1,2,1),(1,2,2),(1,2,3),(1,2,4),(1,3,0),(1,3,1),(1,3,2),(1,3,3),(1,3,4),(1,4,0),(1,4,1),(1,4,2),(1,4,3),(1,4,4),(2,2,0),(2,2,1),(2,2,2),(2,2,3),(2,2,4),(2,3,0),(2,3,1),(2,3,2),(2,3,3),(2,3,4),(2,4,0),(2,4,1),(2,4,2),(2,4,3),(2,4,4),(3,2,0),(3,2,1),(3,2,2),(3,2,3),(3,2,4),(3,3,0),(3,3,1),(3,3,2),(3,3,3),(3,3,4),(3,4,0),(3,4,1),(3,4,2),(3,4,3),(3,4,4),(4,2,0),(4,2,1),(4,2,2),(4,2,3),(4,2,4),(4,3,0),(4,3,1),(4,3,2),(4,3,3),(4,3,4),(4,4,0),(4,4,1),(4,4,2),(4,4,3),(4,4,4)]");
Scale(null, 1, 0.222437137330754, 1, siRelative, siLocal, siObj, siXYZ, null, null, null, null, null, null, null, 0);
Translate(null, 0, -1.17562305635889, 0, siRelative, siLocal, siObj, siXYZ, null, null, null, null, null, null, null, null, null, 0);

SetValue("Context.constructionmode", siConstructionModeModelling);

// This is the standard Group Level way of calling ApplyOp
// It applies the lattice on the sphere point cluster
ApplyOp("Lattice","sphere.polymsh.cls.Point;lattice");

// This does exactly the same thing, but you can specify each and every port you want to connect
ApplyOperator("Lattice","[sphere.polymsh,sphere.kine.global,sphere.kine.global,sphere.polymsh.cls.Point,,sphere.polymsh;lattice.lattice,lattice.kine.global]");

// Set the ContructionHistory mode to Animation
SetValue("Context.constructionmode", siConstructionModeAnimation);

// It applies the lattice on the whole sphere, standard portgroup level
ApplyOp("Lattice","sphere.polymsh.cls.Point;lattice");

// This does exactly the same thing, but you can specify each and every port you want to connect
ApplyOperator("Lattice","[sphere.polymsh,sphere.kine.global,sphere.kine.global,sphere.polymsh.cls.Point,,;lattice.lattice,lattice.kine.global]");

Port Groups

A port group is a sophisticated tool for organizing the ports of an Operator. (The word "group" in this context has nothing to do with the Group (http://softimage.wiki.avid.com/sdkdocs/Group.htm) object in XSI)

Each port must belong to a single group, and an operator can define multiple groups.

There are two main purposes for groups:

1. PortGroups make it easier to connect an operator when it is created

Many actions in the user interface create operators. For example moving points, applying a twist deformer, or extruding a curve. The operators are created based on the current selection and sometimes XSI will launch Picking Sessions to ask for additional objects that are related to the operator.

As discussed above an operator has potentially many ports, and these ports are connected to specific data nested underneath an X3DObject. However when you build operators using the XSI user interface you don't usually need to select or pick all the elements that get port connections. Instead you normally just pick the relevant X3DObjects and XSI takes care of all the details of forming all the detailed connections.

For example, if a Group contains a port that connects to a Primitive and a port that connects to a KinematicState then you only need to specify the X3DObject and XSI will be able to find the primitive and the suitable KinematicState and make the connection.

This behavior may appear "magical", but in fact it is based on the concept of PortGroups. When you write a custom operator you may need to dig deeper into understanding how this works. (You only need to understand this if you want your operator to be connectable through "ApplyOp". If you build your operator as a dynamic runtime operator you may not need to worry about these details because you will explicitly connect all the ports to the intended objects).

As a broad rule, all the ports of a group need to be closely related so XSI can resolve all of them based on a single input object.

In specific:

  • All the ports need to be on the same X3DObject.
  • If you have a cluster property port you should specify it one as the input object for the group. From that, XSI will be able to find and connect any other ports, e.g. the parent cluster and any properties (including KinematicStates) nested under the X3DObject.
  • You can only have a single cluster property in a group.
  • If you have a cluster and a cluser property in a group then XSI must assume that the Cluster Property is nested under that particular Cluster.
  • Given a X3DObject as input XSI can find nested data such as the Primitive and KinematicState
  • Given a Primitive as input XSI can find the KinematicState and other unique properties under the X3DObject

To understand this better it is useful to look at the operator configurations of built-in XSI objects, some of which are demonstrated below.

As a general guideline: if you are having trouble connecting your own custom operator you will probably do well to divide your ports into MORE groups - there is no large problem with having many groups. In fact, the only reason you are likely to have to want to group several ports together is based on the multi-instance feature described next.

2. PortGroups allow an arbitrary number of objects to connect to a single operator

The second purpose comes into play when you want to build an operator that connects to an arbitrary number objects of the same type. For example you might want to have an object that can take any number of meshs and blend them together to form a new mesh. Rather than creating a huge number of ports based on some arbitraty maximum number of objects it is better to use multiple instances of a group. Part of the definition of a group is how many instances it supports - it could be 0 to 1 for an optional group, 1 to 1 for a manditory single instance group or 1 to 1000 for a group that supports up to 1000 connected objects. When an group has multiple instances active then there is a separate instance of each port within the group for each group instance, so the operator can read from all the objects. This concept is demonstrated in the real-life examples below.

Example: Envelope Operator

The concept of ports and groups is rather abstract, so it can help to look at specific examples to see how it works in practice.

The SDK Explorer is a good way to look at operator connections.

For example, this is the SDK Explorer display for the Envelope (http://softimage.wiki.avid.com/sdkdocs/FlexibleEnvelope_op.htm) operator that permits a 3 bone chain to deform a sphere object.

Image:SDKExplorerOperator.JPG

This is a typical example of an operator that supports an arbitrary number of input X3DObjects all affecting a single output X3DObject.

The Operator acts as a deformer so it has input and output connections to sphere.polymsh.

All the ports of the first group are connected to different data within the same X3DObject. In total the envelope operator is connected to the Global KinematicState, a Cluster, a ClusterProperty, the Static KinematicState and the Primitive of the single Sphere object. It is important that all the ports on a particular X3DObject belong to the same object.

Note also how "Port_1" on "Group_0" has index 7. This is because it is an output port so it is listed after all the input ports on that group.

The envelope operator can be connected to an arbitrary number of bones as inputs, and it wants to read both the Static KinematicState and the Global KinematicState of each bone. To achieve this we see the concept of group instances come into play. A second group (Group_1) has been defined with two ports: Static KinematicState and Global KinematicState. Each bone is attached to a separate instance of the Group. This means that the operator will be able to reach each bone by looking at the different instances of each group.

Given sufficient understanding of the operator, it is easy to understand the ConnectionSet argument to ApplyFlexEnv that created this operator:

ApplyFlexEnv "sphere;bone,bone1,bone2,eff", False, 2

The ";" character separates the first and second group. The "," characters separates the 4 objects that need to be connected to the second group. One powerful result of this understanding is the fact that it becomes clear how to create any envelope through scripting, by building a connection set string with the same basic structure.

(This operator is built-in to XSI but the same sort of operator could be created as a custom operator. The Operator.PortAt property can be used inside the operator to access all the different objects connected. For an example check out the VertexColorMixer example that is part of the XSI example workgroup.)

Example: Particle Op

In this example a simple particle simulation has been created that is emitting particles from a grid.

The main guts of the simulation occur within the Particle Op.

Image:ParticleOpDetails.JPG

The first group has a single port, which is an output connection to the cloud. This makes sense consider that it is doing a particle simulation.

It is connected also to the Emitter object, as Group_1. There are three ports, all based on interested nested data of the Emitter. These inputs let the operator know the geometry of the emitter, the position/orientation and scaling of the emitter in global space. It is also connected to the PEmission_emission property, which stores parameters to configure the simulation, such as the rate, spread and speed.

There is also a connected port within Group_5 to the Particle Type. This property gives the particle op information about the nature of the particles that are being emitted. If there were multiple particle types involved in the simulation we would have seen multiple instances of Group_5.

It may appear mysterious that groups 2,3, and 4 are not listed. An operator can have ports that are not connected, and which do not show up in the SDK Explorer. In more advanced simulations the particle operator can interact with forces, goals and other objects and so these extra groups are ready to form the necessary connections.

Evaluation

Forcing an operator to update

A common scripting problem is that a scene that works well in the UI may not work properly when processed via a script, especially when running in batch mode. This happens most frequently when dealing with simulation situations such as Syflex, Hair, Particles or export of animation or with Custom Operators.

The reason is because XSI is highly optimized and does not call the update method of an operator unless something "pulls" the objects that the operator outputs too. In other words, XSI does not generate data unless it is asked for. This pull often occurs when the object is drawn on the screen because XSI wants to determine the exact accurate visual representation of the script. The result is that an operator may have its inputs changed, which puts it into a dirty state, but not automatically update itself.

Fortunately XSI is not dependent on its UI, so other scenarios, including the SDK and rendering can also cause a "pull of the scene graph". For example if an operator modifies the geometry of an object, then any SDK code that retrieves the geometry will provoke the necessary Updates.

So a script that wishes to perform some sort of batch simulation or animation export should be sure to access the affected objects using the SDK at each frame change.

Related Tip: Some custom operators that run simulations need to be dirty each time the frame position changes. Use the CustomOperator.AlwaysEvaluate property to establish this behavior. (The older workaround was animate a parameter of the operator. This parameter could be hidden and just served the purpose of making the operator dirty at each frame)

Custom Operators Changing Scene Graph

A Custom operator should only modify the objects that it has output connections to. Objects that are connected via an input port should not be modified. No XSI or custom Command should be called that might have some side effect in the scene graph. No additional information should be read out of the scene other than from the input connections. (It is ok to call a custom command that performs numeric calculations or communicates with a 3rd party library)

Sometimes a Custom Operator that breaks these guidelines may appear to work properly, but will have evaluation problems at render time or when accessed from a batch script. In severe cases breaking this guideline could cause XSI to crash or corrupt a scene file.

Cycles


Parameters

Parameters on a Custom Operator are very similar to parameters on a Custom Property. However FCurve and GridData type parameters are not supported by runtime and SPDL operators. Self-installing operators do support FCurve and Grid data but as of XSI v5.11 they were not added to the custom operator wizard. You can add them manually by modifying the code generated for the _Define callback as illustrate below.

   function myexpr_Define( ctxt )
   {
     var oCustomOperator = ctxt.Source;
     
     oCustomOperator.AddParameter(XSIFactory.CreateFCurveParamDef("fc"));
     oCustomOperator.AddParameter(XSIFactory.CreateGridParamDef("grid"));
     
     return true;
   }

The operator can have an input connection from a Custom Property as an alternative to having its own parameters. This is useful when FCurve or GridData parameters are needed or when the CustomProperty acts as a central control panel for one or more operators that are deeply hidden in the scene. See the CustomDeformExample.js plugin (in the XSI SDK example workgroup) for an example of this technique.

User Data

You can store per-instance user data in the Context object that is passed to the Init, Update and Term methods. This user data is not persisted in the scene file, and each operator has its own separate user data.

In C++ this user data is often used to store a pointer to a C++ object, so that you can manage the state of the operator in a more object oriented style. While user data can also be initialized the first time the update callback is called, it is strongly suggested to perform data initialization in Init. This is the most optimal way since the function is called for newly created operators and operators loaded from persistence.

Some examples how to set the operator context with user data

The following demonstrates some techniques for storing user data in the custom operator context.

Note: These techniques are not specific to the custom operator case however and can also be used with other context objects such as custom events and custom command ones.

1) Simple setting
XSIPLUGINCALLBACK CStatus CppOp_Init( CRef& in_ctxt )
{
	OperatorContext opCtxt( in_ctxt ) ;

	CString strOpNameAsUserData = op.GetUniqueName();

	opCtxt.PutUserData( strOpNameAsUserData ) ;

	return CStatus::OK ;
}
2) C++ object user data
class CData
{
	LONG data;
};

XSIPLUGINCALLBACK CStatus CppOp_Init( CRef& in_ctxt )
{
	OperatorContext opCtxt( in_ctxt ) ;
	
	CData* p = new CData;
	p->data = 123;
	CValue val = (CValue::siPtrType) p;

	opCtxt.PutUserData( val ) ;

	return CStatus::OK ;
}

// Time to release memory
XSIPLUGINCALLBACK CStatus CppOp_Term( CRef& in_ctxt )
{
	OperatorContext opCtxt( in_ctxt ) ;
	
	CValue val = opCtxt.GetUserData();			 
	CData* p = (CData*)(CValue::siPtrType)val;
	delete p;

	return CStatus::OK ;
}
3) Advanced C++ object setting

This example demonstrates how to use COM to automatically handle memory management of C++ objects stored as user data.

#include <windows.h> // required for automation COM support i.e. IUnknown

// We need a simple IUnknown class to support self reference counting
class CSimpleUnknown : public IUnknown
{
	public:
	CSimpleUnknown() 
	{
		m_ulRefCount = 0L; 
	}

	virtual ~CSimpleUnknown() 
	{
		m_ulRefCount = 1L; 
	}

	STDMETHOD_( ULONG, AddRef ) ()
	{
		return ++m_ulRefCount; 
	}

	STDMETHOD_( ULONG, Release ) ()
	{
		if (m_ulRefCount == 1)
		{
			delete this;
			return 0;
		}
		return --m_ulRefCount;
	}

	STDMETHOD( QueryInterface )  ( REFIID in_riid, LPVOID *out_ppVoid )
	{	if ( out_ppVoid == NULL ) return ( E_POINTER ) ;
		if ( IsEqualIID( in_riid, IID_IUnknown ) )
			*out_ppVoid = (LPVOID)(IUnknown*)this ;
		else
			return E_NOINTERFACE;
		AddRef();
		return S_OK ;
	};

	private:
    ULONG m_ulRefCount;
};

// A user data class deriving from CSimpleUnknown we want to store
struct CData : public CSimpleUnknown 
{
	LONG data ;
	CData() : data( 0 ) {}
} ;

// Init callback to initialize and store CData as user data
XSIPLUGINCALLBACK CStatus CppOp_Init( CRef& in_ctxt )
{
	CData* p = new CData;
	p->data = 5 ;	

	// Important: The type of the CValue object must be changed to siUnknown before adding 
	// to context
	CValue valUnk((CValue::siPtrType)p);
	valUnk.ChangeType( CValue::siIUnknown );
	
	Context(in_ctxt).PutUserData( valUnk ) ;

	return CStatus::OK ;
}

// Retrieve the data through update
XSIPLUGINCALLBACK CStatus CppOp_Update( CRef& in_ctxt )
{
	OperatorContext opCtxt( in_ctxt ) ;

	CValue::siPtrType pUserData = Context(in_ctxt).GetUserData() ;
	CData* p = (CData*)pUserData;
	
	Foo(p);
	
	// Note: GetUserData increments the count on the CData IUnknown object,
	// so make sure to release the count to avoid a memory leak
	((IUnknown*)pUserData)->Release();
	return CStatus::OK ;
}

// Note: Term callback is not required for releasing the count on CData as this is 
// automatically taken care of by the CValue stored in the context

This "pattern" is also demontrated in the VertexColorMixer SDK example.

XSI offers several objects for persistent user data, for example the UserDataBlob, UserDataMap or CustomProperty (See User Data (XSISDK)) However there are some subtle issues when trying to use these objects to maintain operator state.

An operator can read or write to a UserDataBlob (or UserDataMap or CustomProperty). Therefore it would be tempting to think that a UserDataBlob could be used to store the state of an operator in a persistent fashion, by creating both an input and output connection to it. However be warned that this will not give the expected results. In particular, the values stored in the blob will not be available to read back in at the next evaluation. This is because the UserDataBlob is actually part of the scene graph evaluation, and it would actually be a "cycle" or "feedback loop" if XSI let the output of one operator go back into that same operator as an input. For example, a deformation operator expects to read the original geometry and deform it each time it is evaluated, even if it has already deformed the geometry for a previous frame. So, when an operator has an input/output connection to a UserDataBlob XSI will probably choose to create two separate copies of the UserDataBlob in memory such that it is never possible to read the value that the operator previously stored there.

However the user data objects can still be useful in the context of Custom Operators. A custom command (or different operator) can initialize the content of a userdatablob, and then the operator can read it, so this is a useful way to initialize or modify the state of an operator. The command and the operator can be implemented in the same dll or script so they can even have some private communication between each other. It is also possible that a save scene event could be used to save the state of operators into userdatablobs.

User Interface

You can create your own PPGLayout for your custom operator's parameters. However this is currently only available if you use a SPDL file. A convenient alternative is to use a CustomProperty as the UI for your operator. Just connect to an input port from the CustomProperty into your operator and you can read its values.

This is demontrated in the DynamicOperators example inside the sdk example workgroup (XSI v5.0)

Examples

Tips and Special Cases

Generator Operators

A Generator operator builds its own mesh. For example it may be a new type of primitive that is programmatically generated, or it may be a mesh that contains the processed results of some other mesh or input.

A generator operator needs to be at the very bottom of the Operator Stack. Otherwise it won't successfully write to the geometry.

The common way to apply a generator operator is to build a simple primitive, freeze it (to erase the XSI generator operator), and then apply the custom generator. This properly installs the operator, and it would be possible to add deformation and other operators above it.

Classification Flag

Because a generator affects the topology of the object, it may be necessary to set a certain classification flag. Otherwise Cluster Properties and other operators may get out of sync if the generator op changes its output. In the worst case this can even cause a crash.

For a SPDL based generator operator you can set this classification directly on the first parameter of the operator. (Just as is done by certain built-in XSI operators)

Parameter "myop" input
{
   GUID = {123E7885D-FE23-42D4-9CFC-F96321B8F583};
   Type = VT_EMPTY;
   Caps = Persistable; /* 0x00000004 */
   Class = 0x4061; /*E3DPROPERTY_CLASS_TOPO*/
}

You could alternatively set this only on the operator parameters that actually change the geometry generated.

For runtime scripted operators you cannot currently set the classification at the object level. However you can flag the operators parameters by using 16481 (0x4061) as the classification argument when the parameter is created. See (XSIFactory.CreateParamDef (http://softimage.wiki.avid.com/sdkdocs/XSIFactory_CreateParamDef.htm)). Note: If your operator reads from a Custom Property instead of having its own parameters, you may need to flag the parameters of the CustomProperty with this flag. The C++ signatures for CustomProperty::AddParameter do not expose the classification argument, but you can use the CComAPIHandler to call the scripting version of AddParameter:

SICALLBACK MyOp_Define( CRef& in_ctxt )
{
	Context ctxt( in_ctxt );
	CustomOperator oCustomOperator;
	Parameter oParam;
	oCustomOperator = ctxt.GetSource();

	CStatus status;
	CComAPIHandler factory; 
	status = factory.CreateInstance(L"XSI.Factory");
	
	CValueArray args(11);

	args[0] = L"MyOp_Topo_Control_Value";
	args[1] = siInt4;
	args[2] = siClassifTopo;
	args[3] = siPersistable | siKeyable;
	args[4] = L"";
	args[5] = L"";
	args[6] = 5;
	args[7] = 3;
	args[8] = 100;
	args[9] = 3;
	args[10] = 20;
	CValue oPDef;
	status = factory.Call( L"CreateParamDef", oPDef, args );
	status = oCustomOperator.AddParameter(oPDef,oParam);
	oCustomOperator.PutAlwaysEvaluate(false);
	oCustomOperator.PutDebug(1);
	return CStatus::OK;
}

Deformation Operators

You must have an input and output port (on the same group) attached to the Primitive of the object you want to deform. For both a SPDL or runtime operator you must specify the output port first.

A deformation operator nevers adds or removes points of the geometry, it just changes their positions. In other words it changes the shape but not the topology of the operator.

Classification Flag

The classification flag 16449 (0x4041) may be necessary at the object or parameter level to signify to XSI that the operator will change the shape of the geometry. This will help assure that the scene graph is properly evaluated. See the notes under Generator operators to learn how to set this flag.

Topology Operators

Topology operators are like deform operators but they actually add or remove points from the geometry. An example of a built-in topology operator is "Split Edge". Unlike a generator operator, this type of operator is not at the bottom of the stack - instead it has an input port to reads some initial geometry and then outputs a new topology to that same geometry.

Any operator like this should have its classification set to 0x4061 as described under generator operators.

XSI currently does not fully support custom topology operators. The problem is that any cluster or cluster property will not properly update when a topology operator adds or removes points that belong to the cluster. In the worst case XSI may crash. Hence custom topology operators should only be used in the more limited scenario of objects that do not have any clusters. Once the geometry is ready it would be possible to freeze the object to remove the custom topology operators (but leave the result of their evaluation), then to add the clusters and other operators.

Operators connecting to Operators

This is possible!

Operators Changing their own Parameter values

This is not recommended. Some operators have used their own Parameters as a way to store some state. The operator user data is a better way to do this. The problem with changing an operators Parameter during an evaluation is that it may dirty the operator again. Things would be particular bad if that Parameter was being read by any connected expression or other interested party. In some cases you might get away with this, but it can be risky.

Parameter Connections

You cannot connect a custom operator to a string parameter. XSI does not, in general, support "animated" strings. However you can connect to a Custom Property containing a string and read or write that string parameter.

If you wish to change the position of an object it is better to connect to the KinematicState object than to the individual posx,posy and posz parameters. This is because having multiple output connections results in a more complicated, harder to write and potentially slower custom operator. From the single kinematicstate output it is possible to set any of its parameters or to set the raw underlying transformation (the KinematicState.Transform property).

Finding Operators in the scene

You can find all "run-time" operators in the scene using a call to the FindObjects command, as demonstrated below. This can be useful if you know there are scripted operators but aren't sure where they are connected in the scene.

// Demonstration of how to use FindObjects to 
// find Runtime scripted operators in the scene.
//
// This technique does not find SPDL-based custom operators
// because once an Custom Operator is stored inside a SPDL file
// it is assigned its own GUID, which is found as the Reference 
// value at the top of the file.

function GetAllRuntimeCustomOperators()
{
	var siScriptedOperatorID = "{CCECD9D9-10A3-11d4-879F-00A0C983050D}"

	var oItems = FindObjects(null,siScriptedOperatorID)

	// For convenience to the caller,
	// always return a collection even if nothing was found
	if ( oItems == null )
		oItems = new ActiveXObject( "XSI.Collection") ;

	return oItems ;
}

// Simple usage of GetAllRuntimeCustomOperators
function SelectAllRuntimeOperators()
{
	SelectObj( GetAllRuntimeCustomOperators() ) ;
}

//
// Demonstration of GetAllRuntimeCustomOperators
//

function BuildDemoScene()
{
	var oNull = ActiveSceneRoot.AddNull() ;
	var oPosX = oNull.Kinematics.Global.Parameters( "posx" ) ;
	var oRotX = oNull.Kinematics.Local.Parameters( "rotx" ) ;
	
	AddSimpleOp( oPosX ) ;
	AddSimpleOp( oRotX ) ;	
}

function AddSimpleOp( in_param )
{
	// Build a runtime operator that drives in_param
	var oOp = XSIFactory.CreateScriptedOp( 
				"FindMe",
				FindMe_Update.toString(),
				"JScript" ) ;
	oOp.AlwaysEvaluate = true ;		
	oOp.AddOutputPort( in_param ) ;
	oOp.Connect() ;
}

function FindMe_Update(ctx,out)
{
	// Just set the output parameter
	// to current frame value
	out.Value = ctx.CurrentFrame ;
}


BuildDemoScene() ;

var oOps = GetAllRuntimeCustomOperators() ;
for ( var i = 0 ; i < oOps.Count ; i++ )
{
	logmessage( oOps(i).FullName )  ;
}

//Expected output:
//INFO : null.kine.global.FindMe
//INFO : null.kine.local.FindMe

As mentioned in the example above, you can also find SPDL-based operators easily in the scene, by calling FindObjects with the specific Class ID stored in the SPDL file.

Debugging your operator

You can set the CustomOperator.Debug property to 1 or 2 to see extra debug output on a particular operator.

Attaching a Weight Map to a Custom Operator

Native XSI Operator have sometimes, in their PPG, a "connect" icon where you can pick a weight map.

Looking at this operator SPDL file you can notice that the mechanism is implemented under the Parameter definition using the special tag "Commands = CLSID_3DConnection;"

Parameter "angle"
	{
		GUID = {23F683C0-3665-11d0-8DA2-0020AF9D8E1D};
		Name = "Angle";
		Description = "Twist angle of the operator";
		Type = VT_R8;
		Range = min to max;
		UIRange = -360.0 to 360.0;
		Caps = Persistable,Animatable;
		Class = E3DPROPERTY_CLASS_GEOMETRY;
		Commands = CLSID_3DConnection;
	}

This service is not expose for developers who wants to connect a Weight Map.

Fortunately there is a workaround. It consists of creating a dynamic group, supporting any number of instances, that accepts Weight Map connections.

Group "Group_3"
	{
		Origin = Pick;
		PickMessage = "Pick WeightMaps";

		Input "InWeight_Map"
		{
			Major = {9E04FCEE-10F3-11D0-943A-00AA006D3165};
			Minor = {84C402A0-807D-11D0-9962-00A0243F0E60};
			Interface = {00000000-0000-0000-C000-000000000046};
		}

		Min = 0;
		Max = Infinite;
	}

To dynamicaly add a weight map to this port, you need to create a button in the layout section of the SPDL file. The logic function (<button>_OnClicked()) would call PickElement() to let the user choose a weight map. Then the logic code would validate the pick element and connect the Weight Map to the Group (see Operator.ConnectToGroup (http://softimage.wiki.avid.com/sdkdocs/Operator_ConnectToGroup.htm)). `

Tools

  • XSI v5.0 does not include a custom operator wizard. Previous versions of XSI included a Microsoft Visual Studio 6 wizard, which would still work with XSI 5.0.
  • Custom Operator Wizard for Visual Studio .Net (http://www.andynicholas.com/thezone/index.php?area=downloadinfo&app=XSI&file=1) - By Andy Nicholas

This page was last modified 04:55, 2 Oct 2010.
This page has been accessed 55429 times.

© Copyright 2009 Autodesk Inc. All Rights Reserved. Privacy Policy | Legal Notices and Trademarks | Report Piracy