Softimage SDK Programmer's Guide > Customizing with the SDK > Custom Operators >

# Tips and Tricks

Evaluation Time

The operator is not necessarily being evaluated at the current PlayControl position. Instead, use the time value from the context object (see OperatorContext.Time or OperatorContext::GetTime). This is most useful for simulation style operators where a cache might be kept for the simulation state at each frame.

Optimization

Here are a few tips to help you optimize your operator implementation:

- Referring to ports and groups by index rather than string is more efficient.

- Use OperatorContext.GetParameterValue or OperatorContext::GetParameterValue to read the parameters of the operator. This is faster and more convenient that reading it from the custom operator.

- Use OperatorContext.GetInputValue or OperatorContext::GetInputValue to easily access the data that is connected to the operator. If the input is a parameter this returns the actual parameter value (such as a number). If the input is an object (Primitive or Primitive, KinematicState or KinematicState, ClusterProperty or ClusterProperty etc.) then the SDK object representing that object is returned.

- Use cached data to avoid redundant calculations. The cache can be stored as a global variable of your plug-in, or if you want separate caches for each instance of the operator use the user data methods on the OperatorContext or OperatorContext object. For example, the speed of a scripted operator can sometimes be improved by re-using math objects (such as SIMatrix3 or CMatrix3) rather than recreating them (XSIMath.CreateMatrix3 or CMatrix3()) each time Update is called.

Operator Migration

If you have existing legacy or runtime operators, it is strongly recommended to port them to Self-Installed Custom Operators (SICO). Persisted instances of other types of custom operators are not affected by migration. Once the operator code has been ported to SICO you will need to delete and recreate any existing operators to use the new SICO version.

The types of operator migration covered in this section include Runtime to SICO and SPDL/Preset to SICO.

Runtime to SICO

Rather than embedding scripting code into the operator, it is more efficient to set up a self-installed plug-in to define your operator. Here are some steps to get you started:

1. If the code creating the operator is not already in a self-installed plug-in then put the Update callback and other code directly into the new self-installed plug-in and add a call to PluginRegistrar.RegisterOperator or PluginRegistrar::RegisterOperator.

   The same plug-in can contain both a command to create the operator and the operator itself but you can also separate these into their own individual plug-ins, and the command is not really mandatory.

2. Reconsider any global variables that were being injected into the runtime operator:

   - If they are per-instance caches then they must be embedded into the user data of the context object. You can store user data in the Init callback and release it in the Term callback.

   - If they can be global between all instances of the operator then they can become global variables of the plug-in.

3. Remove any output and input port arguments from the Update method signature and rewrite the body of the Update callback to retrieve these from the OperatorContext or OperatorContext object. The way to get the actual values of the input and output ports is slightly different, but the actual algorithm of the operator should still work once these syntactic issues are fixed.

Example: Converting from JScript Runtime to C++ SICO

Here is the original JScript version of the runtime operator. The bolded lines highlight where the Update function is getting and setting port connections:

    // ...Creation Code...
    	var code = GeometryDeform_Update.toString();
    	g_sop = XSIFactory.CreateScriptedOp( "GeometryDeform", code, "JScript" );

    	var p1 = g_sop.AddIOPort( g_prim, "", 0, -1, siBranchGroupPort  );
    	var p2 = g_sop.AddInputPort( g_obj.posy, "", 0, -1, siBranchGroupPort  );

    	g_sop.Connect(g_obj);

    // Notice that the port connections are passed in as arguments to Update
    function GeometryDeform_Update( ctx, Out, Inpolymsh, Inposy )
    {
    	Application.LogMessage( "GeometryDeform_Update: " + Out.TargetPath + ":" + Inposy.TargetPath );

    	var arr = Inpolymsh.Value.Geometry.Points.PositionArray.toArray();

    	for ( var i=0; i<arr.length; i=i+3 )
    	{
    	  var x = arr[0+i];
    	  var z = arr[2+i];
    	  var y = 0;

    	  var x0 = x;
    	  var z0 = z + 1;
    	  var l = Math.sqrt(x0 * x0 + z0 * z0)
    	  var dif = l - Inposy.Value // In2 can be any value, e.g. posx
    	  dif = dif * dif;
    	  dif = Math.sqrt(dif);

    	  if ( dif < 1 ) {
    		y = y + (1 - dif);
    	  }

    	  arr[1+i] = y;
    	}
    	Out.Value.Geometry.Points.PositionArray = arr;
    }

And this is the operator after being converted. Notice the lines in bold again which underline the difference between how the Update function gets and sets port connections for SICOs:

    //... from Creation Code (where prim is primitive of a sphere mesh)...
    	var op = XSIFactory.CreateObject( "CppGeometryDeform" ) ;

    	op.AddIOPort( prim, "", 0, -1, siBranchGroupPort ) ;
    	op.AddInputPort( obj.posy, "", 0, -1, siBranchGroupPort ) ;

    	op.Connect( ) ;
    ...

    // Notice that only the context object is an argument to Update
    function CppGeometryDeform_Update( in_ctxt )
    {

    	// The OperatorContext gives you access to the input ports
    	var prim = ctxt.GetInputValue( 0 /* input primitive of IO port*/ ) ;
    	var posyVal = ctxt.GetInputValue( 1 /* posy param */) ;

    	var inGeom = prim.Geometry ;
    	var arr = inGeom.Points.PositionArray.toArray() ;

    	for ( var i=0; i<arr.length; i=i+3 )

    	{
    	  var x = arr[0+i];
    	  var z = arr[2+i];
    	  var y = 0;

    	  var x0 = x;
    	  var z0 = z + 1;
    	  var l = Math.sqrt(x0 * x0 + z0 * z0)
    	  var dif = l - Inposy.Value // In2 can be any value, e.g. posx
    	  dif = dif * dif;
    	  dif = Math.sqrt(dif);

    	  if ( dif < 1 ) {
    		y = y + (1 - dif);
    	  }

    	  arr[1+i] = y;
    	}

    	// The OperatorContext gives you access to the output ports
    	var outPrim = ctxt.OutputTarget ;
    	outPrim.Geometry.Points.PositionArray( arr ) ;

    	return CStatus::OK ;
    }

And because you might be interested in taking the extra step of compiling the operator, here is the C++ equivalent of the above:

    //... from Creation Code (where prim is primitive of a sphere mesh)...
    	CustomOperator op = Application().GetFactory().CreateObject( L"CppGeometryDeform" ) ;

    	op.AddIOPort( prim, L"", 0, -1, siBranchGroupPort ) ;
    	op.AddInputPort( obj.GetParameters().GetItem( L"posy" ), L"", 0, -1, siBranchGroupPort ) ;

    	op.Connect( ) ;
    ...

    // Notice that only the context object is an argument to Update
    XSIPLUGINCALLBACK CStatus CppGeometryDeform_Update( CRef& in_ctxt )
    {

    	OperatorContext ctxt( in_ctxt ) ;

    	// The OperatorContext gives you access to the input ports
    	Primitive prim = ctxt.GetInputValue( 0 /* input primitive of IO port*/ ) ;
    	double posyVal = ctxt.GetInputValue( 1 /* posy param */) ;

    	Geometry inGeom = prim.GetGeometry() ;
    	CVector3Array posArray = inGeom.GetPoints().GetPositionArray();

    	for ( LONG i=0 ; i<posArray.GetCount(); i++ )
    	{
    			CVector3& pos = posArray[i] ;
    			pos.PutY( 0.0 ) ;

    			CVector3 pos0( pos.GetX(), 0.0, pos.GetZ() + 1.0 )  ;

    			double l = pos0.GetLength() ;

    			double dif = l - posyVal ;

    			if ( dif < 0 )
    				dif = -dif ;

    			if ( dif < 1 )
    				pos.PutY( 1 - dif ) ;
    	}

    	// The OperatorContext gives you access to the output ports
    	Primitive outPrim = ctxt.GetOutputTarget() ;
    	outPrim.GetGeometry().GetPoints().PutPositionArray( posArray ) ;

    	return CStatus::OK ;
    }

SPDL/Preset to SICO

Changing your legacy (preset-based) operator to use the new Self-Installed Custom Operator API is basically a matter of converting any SPDL-based parameters to use the Custom Properties API, updating the existing entry points to match the new signatures, and implementing a few new callbacks to register the operator with the PluginRegistrar or PluginRegistrar.

Tip

Before starting the conversion process, you should have a clear idea of how custom operator are implemented. For information about applying operators, see Applying Operators in Softimage; for information about using the callbacks and entry points, see Operator Callbacks.

1. If you are using C++ API to implement your operator, change the project file (.vcproj, .sln) to output the dll into \Application\Plugins instead of \Application\bin\.

2. Add the XSILoadPlugin entry point that registers the operator.

3. Examine the parameter definition in the SPDL and create equivalent parameters in the DefineLayout callback.

4. Use the port connection information in the SPDL to build a list of outputs and inputs for the code to apply the operator:

   - If the connections are basic, with no usage of multi-instance groups or optional ports, then you can apply the operator automatically.

   - If the connections are optional or dynamic, then you must apply the operator manually.

   Tip

   Because the SPDL contains GUIDs that are hard to decipher, it can help to load a scene with the SPDL-based operator and use the SDK Explorer to inspect the actual objects that connect to it.

5. However you decide to apply the operator, you need to visit each call to ApplyOp and replace it with the manual or automatic application code. You could either:

   - Add a menu callback that uses AddCustomOp to create the operator, as demonstrated in the Splatter example.

   - Add a custom command to apply the operator and change any calls to ApplyOp to use the new command. It can be implemented in the same plug-in as the operator as demonstrated in the Vertex Color Mixer example.

6. Change the Update function to use the new signature, the new context object, and the new API to get at the input and outputs (the actual algorithm should be unchanged):

   - The old Update callback required you to pass in all port connections as arguments. The new Update callback supports only one argument: the context, from which you can get the port connections and custom parameters (variables).

   - The old Update callback provided access to the UpdateContext or UpdateContext object; the new Update callback provides access to the OperatorContext or OperatorContext object, which provides easy access to the operator's ports and custom parameters.

   - You still have access to the CustomOperator or CustomOperator through the source of the OperatorContext or OperatorContext (via Context.Source or Context::GetSource), however, the OperatorContext or OperatorContext object provides faster, more convenient ways to get at the data, such as OperatorContext.GetInputValue or OperatorContext::GetInputValue which returns the data connected to the specified input port, or OperatorContext.OutputPort or OperatorContext::GetOutputPort and OperatorContext.OutputTarget or OperatorContext::GetOutputTarget which allow you to write to the output port of the operator.

7. Test the operator.

8. If necessary, replace any operators stored in scene files with the new operator.

9. Remove the Preset, SPDL and DLL associated with the SPDL-version of the operator, unless legacy scene files continue to depend on it. There should be no problems with coexistence of both versions.