Writing a Channel Modifier for MODO

One of my favourite things to code for MODO, is a channel modifier. A channel modifier is the simplest of plugins. Not necessarily in it's implementation, as it requires a basic knowledge of the MODO evaluation system. Yet the simplicity of the single operation on inputs and outputs, makes them really fun to write.

The best way to think of a channel modifier, is as a function, or subroutine in a programming language. Like a function, it has certain inputs, along with an operation, which takes those inputs and evaluates some output/s. They slot into the evaluation system and when evaluated, calculate the output/s from the input/s.


Background

Before we begin, I want to provide a bit of background information and a bit of basic knowledge about how Items, Channels and Evaluation works inside of MODO. I'm only providing the basics here, but this should be enough to allow you to follow along with this post. If you know all of this, feel free to "skip to the end".

Items

Almost everything inside of MODO is an item; Locators, Meshes, Shaders, Groups, Scenes...etc are all items. An item is an object, with certain properties such as a name, a type, channels...etc.

In their most basic form, an item is useless. By itself, with no customisation, is does nothing. In fact, you wouldn't even be able to see it within the interface. It simply exists. However, a developer can customise the item, providing it with certain functionality and properties, that determine how it's displayed to and used by the user. A lot of items have similar properties, for example, many items have a transform to define a position, rotation and scale in 3D space. A lot of items share the same channels, properties and rules about where they show up in the UI. Because of this, the MODO SDK provides certain item types, which provide a shared set of functionality across multiple items of the same type. This is called the Supertype.

A supertype is an item type that defines a basic set of functionality for all items that belong to that supertype. You can almost think of it as a class of item, defining rules and properties for all items of the same class. For example, it may provide custom drawing that will be shared across each item of that type. It may define certain default channels, which automatically show up on each item of that type. Or it may even dictate how items of that type are represented in the user interface. The most common Supertype is the Locator. This adds the default transform channels, the ability to draw the item in 3D space, along with the ability to view the item in the MODO item list.

The supertype we'll be concerning ourselves with in this article, is the chanModify supertype, which provides a set of common functionality and channels across all channel modifiers.

Channels

As was briefly alluded to in the previous section, Items have Channels.

Channels can be thought of as the properties or attributes of an item. They can store almost any value, and some can change over time, either through keyframe animation, or through evaluation. There are many different types of channel, such as Float, Integer, Boolean...etc, and they are fully extensible, allowing you to write your own custom channel type for storing arbitrary data.

Items are usually made up of many channels, which can be written to and read from, to store state or modify how certain operations on the item are performed.

Evaluation

Evaluation is a very in-depth subject to discuss. There are many nuances and "gotcha's" that you need to be aware of when writing a full evaluation modifier. However, for a channel modifier, a simple overview should be sufficient, as the complexity of the evaluation system is mostly abstracted by the Channel Modifier API.

Channels inside of MODO can have their value defined using two methods; stored values or evaluation. Stored values are usually single values or time varying values stored in the channel. These values are directly set by the user, using commands and tools. They are persistent, and are saved in the scene file when the scene is closed. Evaluation however, is a dynamic calculation of a channel value. The evaluation is performed using a modifier, which will take certain inputs and calculate the value of output channels as needed.

Modifiers, and by extension channel modifiers, are limited to only reading and writing channel values. It is unable to read anything other than an input channels and is unable to write or modify anything other than an output channels. This may initially seem rather limiting, however, as the majority of properties on items are represented as channels, there is very little that is unable to be modified as part of evaluation. For example, deformation, particle simulation and images can all be represented as channels and dynamically modified in some way using a modifier.

The basic life cycle of a modifier, is that it allocates input channels and output channels. When something in the scene attempts to read a channel that your modifier is outputting, your modifier is evaluated. To do this, it will read any input channels that it needs to calculate an output, which could potentially result in other modifiers being evaluated to get the correct input values. Once an output channel value has been evaluated by the modifier, it is cached, and not-evaluated again until the input channel values change. This allows for a very fast evaluation system, as modifiers are only evaluated as their output values are required.

A modifier may be evaluated more than once per frame and in the case of things like nodal shading or particle simulation, potentially hundreds, if not thousands of times per frame.

Note: Modifiers are not directly associated with any item in the scene. They are a separate system that can modify any channel on any item, using any arbitrary rules it desires. It is completely possible to have a modifier with no front facing interface displayed to the user. However, in the case of a channel modifier, the modifier is related to the channel modifier item and can only read from the input channels and write to the output channels on the channel modifier item itself.


Overview

Ok, with the background knowledge out of the way, let's take a look at what we're going to build.

We're going to create a very simple channel modifier, that takes a single integer channel input and returns True or False if the value is even. For example, if the input is 1, the modifier will output False, and if the input is 2, the modifier will output True.

Obviously this is an extremely simple example of a channel modifier, however, it should provide a basic overview and introduction to the API, allowing you to experiment with more interesting examples on your own.


The Channel Modifier

A channel modifier, is a special item type that is a combination of both item and modifier. Like all items, it is made up of two classes; a Package and an Instance. The Package defines the common properties of all items of that type, such as it's supertype and the channels it exposes. The Instance represents the individual copies of that item in the scene.

Like other interfaces that are implemented with the MODO SDK, we inherit from specific base classes to define the type of thing we're creating, and then override any functions we need to, to provide any custom functionality. Our Package implementation, inherits from ILxPackage, and the Instance implementation, inherits from ILxPackageInstance and ILxChannelModItem.

Instance

class Instance : public CLxImpl_PackageInstance, public CLxImpl_ChannelModItem
{
	public:
		static void initialize ()
		{
			CLxGenericPolymorph	*srv = NULL;
	
			srv = new CLxPolymorph						<Instance>;
			srv->AddInterface		   (new CLxIfc_PackageInstance	   <Instance>);
			srv->AddInterface		   (new CLxIfc_ChannelModItem	   <Instance>);
			
			lx::AddSpawner			("cmIsEven.inst", srv);
		}
	
		unsigned	 cmod_Flags		(ILxUnknownID item_obj, unsigned int index)						LXx_OVERRIDE;
		LxResult	 cmod_Allocate		(ILxUnknownID cmod_obj, ILxUnknownID eval_obj, ILxUnknownID item_obj, void **ppvData)	LXx_OVERRIDE;
		LxResult	 cmod_Evaluate		(ILxUnknownID cmod_obj, ILxUnknownID attr_obj, void *data)				LXx_OVERRIDE;
};

The first thing we define is the Instance. As was stated earlier, this inherits from both PackageInstance and ChannelModItem. It needs to inherit from PackageInstance, so that our Package class can spawn instances. However, you can see that we're not actually interested in modifying any of it's functionality, so we don't override any of the PackageInstance functions. We also inherit from ChannelModItem, this is the modifier which will allocate input channels and output channels and perform the Evaluation. We're implementing three functions:

cmod_Flags
This defines which channels are drawn as inputs and which channels are drawn as outputs, in the schematic. When the channel modifier is drawn, it will either have input connections, output connections or multiple input connections. This allows us to return a flag, which specifies which connection type the channel has.

unsigned Instance::cmod_Flags (ILxUnknownID item_obj, unsigned int index)
{
	/*
	 *	 If the channel being queried matches one of our input or
	 *	 output channels, return input or output. These are searched
	 *	 by name, and then the index is compared.
	 */
     
	CLxUser_Item		 item (item_obj);
	unsigned		 chan_index = 0;
    
	if (item.test ())
	{
		if (LXx_OK (item.ChannelLookup ("input", &chan_index)) && index == chan_index)
			return LXfCHMOD_INPUT;
		if (LXx_OK (item.ChannelLookup ("output", &chan_index)) && index == chan_index)
			return LXfCHMOD_OUTPUT;
	}
}

cmod_Allocate
This function is used to specify the input channels and output channels for our modifier. Input channels are the channels the modifier expects to read, and output channels are the channels the modifier expects to write.

LxResult Instance::cmod_Allocate (ILxUnknownID cmod_obj, ILxUnknownID eval_obj, ILxUnknownID item_obj, void **ppvData)
{
	/*
	 *	 Allocate the channels as either inputs or outputs to the
	 *	 modifier. Inputs will be read and outputs will be written.
	 */
     
	CLxUser_Item		 item (item_obj);
	CLxLoc_ChannelModifier	 chan_mod (cmod_obj);
	unsigned		 chan_index = 0;
    
	if (!item.test () || !chan_mod.test ())
		return LXe_FAILED;

	if (LXx_OK (item.ChannelLookup ("input", &chan_index)))
		chan_mod.AddInput (item, chan_index);
	
	if (LXx_OK (item.ChannelLookup ("output", &chan_index)))
		chan_mod.AddOutput (item, chan_index);

	return LXe_OK;
}

cmod_Evaluate
Finally, the Evaluate function performs the actual evaluation for our modifier. It will read the inputs, perform some logic and write to the output channels.

LxResult Instance::cmod_Evaluate (ILxUnknownID cmod_obj, ILxUnknownID attr_obj, void *data)
{
	/*
	 *	 Read the inputs using the attributes interface and calculate
	 *	 the output channel values.
	 */

	CLxLoc_ChannelModifier	 chan_mod (cmod_obj);
	CLxUser_Attributes	 attr (attr_obj);
	int			 input_chan = 0, output_chan = 0;
    
	if (!chan_mod.test () || !attr.test ())
		return LXe_FAILED;

	/*
	 *	Read the input channel. Channels are accessed by
	 *	index, in the order they're added to the modifier.
	 */
	
	chan_mod.ReadInputInt (attr, 0, &input_chan);

	/*
	 *	Is the input even?
	 */

	 output_chan = input_chan % 2 ? 0 : 1;
     
	/*
	 *	Write the result to the output channel?
	 */

	chan_mod.WriteOutputInt (attr, 0, output_chan);

	return LXe_OK;
}

You will also see that we have an static initialize function. This function is used to add our plugin server into MODO and declare any interfaces that we present. We use the AddSpawner helper function, which will allow our Package to spawn Instances as needed.

Package

class Package : public CLxImpl_Package
{
	public:
		static void initialize ()
		{
			CLxGenericPolymorph	*srv = NULL;
	
			srv = new CLxPolymorph						<Package>;
			srv->AddInterface		   (new CLxIfc_Package		   <Package>);
			srv->AddInterface		   (new CLxIfc_StaticDesc	   <Package>);
			
			lx::AddServer			("cmIsEven", srv);
		}

		Package () : _inst_spawn ("cmIsEven.inst") {}
	
		LxResult	 pkg_SetupChannels	(ILxUnknownID addChan_obj)		LXx_OVERRIDE;
		LxResult	 pkg_TestInterface	(const LXtGUID *guid)			LXx_OVERRIDE;
		LxResult	 pkg_Attach		(void **ppvObj)				LXx_OVERRIDE;
	
		static LXtTagInfoDesc	   descInfo[];
	
	private:
		CLxSpawner <Instance>	 _inst_spawn;
};

The next thing we define is the Package. The package is used to spawn instances of our item. It also defines the properties that are universal to all items of this type, such as the channels is presents to the user and the Supertype. We're implementing three functions:

pkg_SetupChannels
The SetupChannels function is called by MODO as the item type is created. It is used to add new channels to the item and set any defaults. We'll use this to add our input integer channel and output boolean channel.

LxResult Package::pkg_SetupChannels (ILxUnknownID addChan_obj)
{
	/*
	 *	Setup the channels that appear on the item. We're adding two
	 *	channels; "input" and "output".
	 */

	CLxUser_AddChannel	 add_chan (addChan_obj);
	
	if (!add_chan.test ())
		return LXe_FAILED;
	
	add_chan.NewChannel ("input", LXsTYPE_INTEGER);
	add_chan.SetDefault (0.0, 0);

	add_chan.NewChannel ("output", LXsTYPE_BOOLEAN);
	add_chan.SetDefault (0.0, 0);
	
	return LXe_OK;
}

pkg_TestInterface
The TestInterface function is a special, yet required function that allows the Package to define which COM interfaces are implemented by the Instance is spawns. Other plugins may query this package with an interface, and the Package is expected to return True or False if it supports the interface.

LxResult Package::pkg_TestInterface (const LXtGUID *guid)
{
	/*
	 *	Call the Test Interface function on the spawner.
	 */

	return _inst_spawn.TestInterfaceRC (guid);
}

pkg_Attach
The Attach function is called to spawn an Instance interface for the item. This function will be called as the user adds items of this type to the MODO scene.

LxResult Package::pkg_Attach (void **ppvObj)
{
	/*
	 *	Spawn the Package Instance using the spawner.
	 */

	_inst_spawn.Alloc (ppvObj);
	
	return ppvObj[0] ? LXe_OK : LXe_FAILED;
}

You may also see that we have a static LXtTagInfoDesc struct in the package class. This is used for returning server tags that define some basic properties of the item. In the case of the channel modifier, it usually returns the supertype.

/*
 *	In the server tags, we just set the item supertype.
 */

LXtTagInfoDesc Package::descInfo[] =
{
	{ LXsPKG_SUPERTYPE,	LXsITYPE_CHANMODIFY },
	{ 0 }
};

Similar to the Package Instance, the Package has a static initialize function. This function is used to add our plugin server into modo and declare any interfaces that we present. We use the AddServer helper function, which will register the Package as a first class item within MODO.

Entry Point

Finally, we need an Initialize function. This is the entry point for the plugin, and will be called by MODO to register the server as a first class item. We just call the Initialize functions on the Package and Instance.

void initialize ()
{
	Package::initialize ();
	Instance::initialize ();
}

Conclusion

Hopefully this has provided a good introduction to writing a Channel Modifier item in MODO. Compiling the code will provide a new item that can be added to the Schematic, and used as part of larger rigs. I admit, this example is a little dull, but hopefully it provides the framework for expanding and experimenting.

The full source code is provided below.


The Code

Listed below is the full source code for this article. This outlines all of the required headers, along with the correct structure and layout for the code.

#include <lxsdk/lx_chanmod.hpp>
#include <lxsdk/lx_package.hpp>
#include <lxsdk/lx_plugin.hpp>
#include <lxsdk/lxidef.h>

#define	SERVER_NAME	"cmIsEven"
#define	CHAN_INPUT	"input"
#define	CHAN_OUTPUT	"output"

/*
 *	Define the Package Instance and Package.
 */

class Instance : public CLxImpl_PackageInstance, public CLxImpl_ChannelModItem
{
	public:
		static void initialize ()
		{
			CLxGenericPolymorph	*srv = NULL;
	
			srv = new CLxPolymorph						<Instance>;
			srv->AddInterface		   (new CLxIfc_PackageInstance	   <Instance>);
			srv->AddInterface		   (new CLxIfc_ChannelModItem	   <Instance>);
			
			lx::AddSpawner			(SERVER_NAME".inst", srv);
		}
	
		unsigned	 cmod_Flags		(ILxUnknownID item_obj, unsigned int index)						LXx_OVERRIDE;
		LxResult	 cmod_Allocate		(ILxUnknownID cmod_obj, ILxUnknownID eval_obj, ILxUnknownID item_obj, void **ppvData)	LXx_OVERRIDE;
		LxResult	 cmod_Evaluate		(ILxUnknownID cmod_obj, ILxUnknownID attr_obj, void *data)				LXx_OVERRIDE;
};

class Package : public CLxImpl_Package
{
	public:
		static void initialize ()
		{
			CLxGenericPolymorph	*srv = NULL;
	
			srv = new CLxPolymorph						<Package>;
			srv->AddInterface		   (new CLxIfc_Package		   <Package>);
			srv->AddInterface		   (new CLxIfc_StaticDesc	   <Package>);
			
			lx::AddServer			(SERVER_NAME, srv);
		}

		Package () : _inst_spawn (SERVER_NAME".inst") {}
	
		LxResult	 pkg_SetupChannels	(ILxUnknownID addChan_obj)		LXx_OVERRIDE;
		LxResult	 pkg_TestInterface	(const LXtGUID *guid)			LXx_OVERRIDE;
		LxResult	 pkg_Attach		(void **ppvObj)				LXx_OVERRIDE;
	
		static LXtTagInfoDesc	   descInfo[];
	
	private:
		CLxSpawner <Instance>	 _inst_spawn;
};

/*
 *	Implement the Package Instance methods.
 */

unsigned Instance::cmod_Flags (ILxUnknownID item_obj, unsigned int index)
{
	/*
	 *	 If the channel being queried matches one of our input or
	 *	 output channels, return input or output. These are searched
	 *	 by name, and then the index is compared.
	 */
     
	CLxUser_Item		 item (item_obj);
	unsigned		 chan_index = 0;
    
	if (item.test ())
	{
		if (LXx_OK (item.ChannelLookup (CHAN_INPUT, &chan_index)) && index == chan_index)
			return LXfCHMOD_INPUT;
		if (LXx_OK (item.ChannelLookup (CHAN_OUTPUT, &chan_index)) && index == chan_index)
			return LXfCHMOD_OUTPUT;
	}
}

LxResult Instance::cmod_Allocate (ILxUnknownID cmod_obj, ILxUnknownID eval_obj, ILxUnknownID item_obj, void **ppvData)
{
	/*
	 *	 Allocate the channels as either inputs or outputs to the
	 *	 modifier. Inputs will be read and outputs will be written.
	 */
     
	CLxUser_Item		 item (item_obj);
	CLxLoc_ChannelModifier	 chan_mod (cmod_obj);
	unsigned		 chan_index = 0;
    
	if (!item.test () || !chan_mod.test ())
		return LXe_FAILED;

	if (LXx_OK (item.ChannelLookup (CHAN_INPUT, &chan_index)))
		chan_mod.AddInput (item, chan_index);
	
	if (LXx_OK (item.ChannelLookup (CHAN_OUTPUT, &chan_index)))
		chan_mod.AddOutput (item, chan_index);

	return LXe_OK;
}

LxResult Instance::cmod_Evaluate (ILxUnknownID cmod_obj, ILxUnknownID attr_obj, void *data)
{
	/*
	 *	 Read the inputs using the attributes interface and calculate
	 *	 the output channel values.
	 */

	CLxLoc_ChannelModifier	 chan_mod (cmod_obj);
	CLxUser_Attributes	 attr (attr_obj);
	int			 input_chan = 0, output_chan = 0;
    
	if (!chan_mod.test () || !attr.test ())
		return LXe_FAILED;

	/*
	 *	Read the input channel. Channels are accessed by
	 *	index, in the order they're added to the modifier.
	 */
	
	chan_mod.ReadInputInt (attr, 0, &input_chan);

	/*
	 *	Is the input even?
	 */

	 output_chan = input_chan % 2 ? 0 : 1;
     
	/*
	 *	Write the result to the output channel?
	 */

	chan_mod.WriteOutputInt (attr, 0, output_chan);

	return LXe_OK;
}

/*
 *	Implement the Package methods.
 */

LxResult Package::pkg_SetupChannels (ILxUnknownID addChan_obj)
{
	/*
	 *	Setup the channels that appear on the item. We're adding two
	 *	channels; "input" and "output".
	 */

	CLxUser_AddChannel	 add_chan (addChan_obj);
	
	if (!add_chan.test ())
		return LXe_FAILED;
	
	add_chan.NewChannel (CHAN_INPUT, LXsTYPE_INTEGER);
	add_chan.SetDefault (0.0, 0);

	add_chan.NewChannel (CHAN_OUTPUT, LXsTYPE_BOOLEAN);
	add_chan.SetDefault (0.0, 0);
	
	return LXe_OK;
}

LxResult Package::pkg_TestInterface (const LXtGUID *guid)
{
	/*
	 *	Call the Test Interface function on the spawner.
	 */

	return _inst_spawn.TestInterfaceRC (guid);
}

LxResult Package::pkg_Attach (void **ppvObj)
{
	/*
	 *	Spawn the Package Instance using the spawner.
	 */

	_inst_spawn.Alloc (ppvObj);
	
	return ppvObj[0] ? LXe_OK : LXe_FAILED;
}

/*
 *	In the server tags, we just set the item supertype.
 */

LXtTagInfoDesc Package::descInfo[] =
{
	{ LXsPKG_SUPERTYPE,	LXsITYPE_CHANMODIFY },
	{ 0 }
};

/*
 *	Startup the servers.
 */

void initialize ()
{
	Package::initialize ();
	Instance::initialize ();
}