Modifier Stack Branching

This section discusses the concept of modifier stack branching. This section uses the Boolean object plug-in as an example. The SDK sample code for the Boolean object is available in \MAXSDK\SAMPLES\OBJECTS\BOOLOBJ.CPP.

Normally the geometry pipeline is a sequence of object references. For example, the node may have a reference to a derived object. This derived object may reference another derived object. That derived object may then reference the base object. Normally this is where the pipeline would end.

In some cases however, the base object itself has references to other objects. For example, the boolean object (referred to as a compound object) has references to its two operands. The boolean object acts like a multiplexer that decides which parts of the history is available to the user.

In the 3ds Max user interface the modifier history is just a linear list. It does not show any kind of tree (branch) structure. Thus at any one time the user can only go down a single branch of the tree. It is up to the object to decide which branch the user is to go down. The standard way of doing this is to provide a user interface control to allow the user to select which branch they want to go down.

With the boolean object the user can go into sub-object selection and select one of the operands. Then that operand's history will become available in the modifier stack. Alternatively the boolean object provides a list of the operands and the user can select them there. When this is done this history becomes available and the user can operate on the selected history.

Methods used in Modifier Stack Branching

This section presents the class methods a developer works with to manage an object that uses modifier stack branching. The following methods from class Object are involved:

virtual int NumPipeBranches()

This method returns the number of pipeline branches combined by the object. This is not the total number of branches, but rather the number that are active. For example, in the boolean object, if the user does not have any operands selected, this methods would return zero. If they have one selected it would return one. Here is the boolean object implementation of this method:

int BoolObject::NumPipeBranches()
{
   int num=0;
   if (TestFlag(BOOL_OB1SEL) && ob1) num++;
   if (TestFlag(BOOL_OB2SEL) && ob2) num++;
   return num;
}

The boolean object is set up so that a user can only branch down one pipeline at a time. This is not always the case for all compound objects. In the 3ds Max loft object, the user may select many shapes on the loft path. For example, the user could select five shapes on the path. NumPipeBranches() would return five to indicate that five pipelines are currently active. If all five shapes were the same instance, the user could go down their history all at the same time. This is analogous to when you select five objects in the scene, and if all these objects are all instances (they have commonality) then their history is available. If you select five objects and they don't have commonality their history is not available. If they share commonality to a certain point, their history is available up to and including this point and then the history stops. The pipeline branches are similar. If there is commonality, the common history will be displayed.

virtual Object *GetPipeBranch(int i)

This method is called to return the 'i-th' branch of the compound object. As the system is calculating the history, when it reaches the base object where it would normally stop, it checks to see if there is any branching. The system calls NumPipeBranches() on the object. If this returns anything greater than zero it will then get each of the branches and continue its evaluation. Here is the boolean object implementation of this method:

Object *BoolObject::GetPipeBranch(inti)
{
   if (i) returnob2;
   if (TestFlag(BOOL_OB1SEL)) return ob1;
   return ob2;
}
virtual INode *GetBranchINode(TimeValue t, INode *node, int i)

Compound objects like the boolean object or the lofter contain multiple objects and the history of each of these objects can be edited. A problem arises because these compound objects usually apply a transformation to their input objects. For example, the boolean object has a transformation for each of its two operand objects. The boolean object has two operands and each has a transform controller. These transform controllers represent the transformation relative to the boolean object's transformation (its local space). This allows the user to move the two boolean operands around relative to each other without modifying the boolean object's node's transform controller. This arrangement simply provides an additional transformation for the operands.

If the user goes back into the history to edit one of the boolean object's operands and applies an Edit Mesh modifier, the Edit Mesh modifier does not know about the transformation that is happening in the pipeline downstream. Thus it thinks the object is positioned somewhere where it is not since it has no way of knowing about this other transformation. This is a problem (for example if you try to hit test) because the plugin thinks the object is in the wrong place.

In general every item in the pipeline applies a transformation of some sort. An Edit Mesh modifier deals with this by forcing 'Show End Result' to be off. Currently show end result only effects downstream modifiers and not downstream objects so the transformation by the compound object still applies.

The transformation applied by the compound object typically is a linear transformation which, of course, can be inverted. So one can edit in the context of the transformation if it is known what it is. Methods are available to make it possible for compound objects to provide access to this transformation:

INode *GetBranchINode(TimeValue t, INode *node, int i)

This Object method will allow a compound object to alter a node's transformation. The way it does this is by creating an item called an INodeTransformed. An INodeTransformed is simply a wrapper around an INode that adds in an extra transformation to the node's transformation. An object can implement this method by returning a pointer to a new INodeTransformed that is based on the node passed into this method. Here is the boolean object implementation of this method:

INode *BoolObject::GetBranchINode(TimeValue t, INode *node, int i)
{
   assert(i<2);
   int index = 0;
   if (i) index = 1;
   else if (TestFlag(BOOL_OB1SEL)) index = 0;
   else index = 1;
   return CreateINodeTransformed(node,GetOpTM(t,index));
}

When a modifier, like an Edit Mesh, calls GetModContexts() some of the INodes in the INodeTab may actually be pointing to INodeTransforms. When the modifier is done with the table, these wrappers need to be discarded. The method INodeTransformed::DisposeTemporary() is provided for this purpose.

Notifying Dependents of the History Change

One other task needs to be done by a compound object that deals with modifier stack branching. The system needs to be notified when the branching changes. For example, in the boolean object if the user has selected operand A, and then selects operand B (which de-selects operand A) the history must be updated to show the history for operand B. 3ds Max does not know that the user just changed operands as that is handled by the boolean object. To let the system know, the boolean object sends the message REFMSG_BRANCHED_HISTORY_CHANGED via NotifyDependents(). This lets the system know it needs to re-build the history from this point on.