References

3DS Max Plug-In SDK

References

See Also: Class ReferenceMaker, Class ReferenceTarget, List of Reference Messages.

Overview

In the 3ds max architecture, elements of the scene often form dependencies on one another. The typical manner these dependencies are handled in 3ds max are through References*. A reference is a record of dependency between a reference maker and a reference target. The reference maker is said to be dependent upon the reference target. If the target changes in some way that affects the maker, the maker must be notified so it may take appropriate action.

* Note: This use of the term reference in this section should not be confused with the term reference used in the 3ds max interface and user manuals. Nor is it to be confused with the C++ definition of reference. In this section, the term reference will always apply to the notion of a dependent relationship unless specifically stated otherwise.

Below are a few examples of dependent relationships between elements of a 3ds max scene that are managed using references:

image\bullet.gif A loft model is dependent upon its path shapes. When one of the path shapes changes, the model must be notified so it may update itself.

image\bullet.gif The path controller is dependent upon the spline path the controller is assigned to follow. When the spline path changes shape, the controller must be notified so it may realign its node to the revised path.

image\bullet.gif The procedural sphere is dependent upon its animated parameters. When the parameters are changed, the object must be notified so that is can update its cached representation to reflect the new settings.

The above examples show how references have been used in 3ds max plug-in development. Any time a developer wants to set up a dependent relationship between two elements of the scene and be notified of any changes, a reference may be used.

There are two key classes involved in the 3ds max reference system. These are the ReferenceMaker class, and the ReferenceTarget class. The ReferenceTarget class is derived from the ReferenceMaker class. Most plug-in classes are derived from ReferenceTarget. Thus most plug-ins may make references themselves.

There are three key methods involved when working with references. These are MakeRefByID(), NotifyDependents(), and NotifyRefChanged().

Change Notification Methods

The reference scheme allows a reference target to notify all its dependent reference makers when it changes. This section presents an overview of how this is done.

Having a reference is similar to having a pointer to an object, however when a maker references a target, that target maintains a pointer back to the maker. The reference target keeps a list of back pointers to all the reference makers which reference it. This gives the target the ability to notify each of its dependent reference makers when it has changed in some fashion. There are several methods that must be called or implemented by the plug-in when it uses references:

image\bullet.gif The reference maker must inform the reference target that it is dependent upon it. It does this by creating a reference to the target using a method called MakeRefByID(). The target then maintains this record of dependency via its pointer back to the reference maker.

image\bullet.gif When a reference target changes it must notify its dependent reference makers of this change. It does this by calling a method NotifyDependents().

image\bullet.gif A reference maker must implement a method to receive the change notification messages sent by the target. It does this by implementing a method called NotifyRefChanged().

Let's look at each of these methods and how they are handled by a specific 3ds max plug-in. We'll use the example of the path controller. The path controller governs the position of a node in the scene allowing it to track along a selected spline path. A spline path node in the scene is chosen to follow, and a parameter controls the amount of banking applied to the node as it follows along the path. (The path controller has other parameters, but concerning references the others are similar to the two covered here). The processing of these parameters involves several references.

The path controller tracks another node in the scene. This node is a spline and it is of course free to change shape and orientation at any time. By creating a reference to the spline node using MakeRefByID(), the controller will be notified whenever the spline changes. The path controller also must monitor its own banking parameter which may be animated and change over time. By creating a reference to this parameter the controller will be informed whenever the parameter changes.

A reference target is responsible for notifying all its dependent reference makers when it has changed. The spline path is a reference target. It is therefore responsible for informing its dependent reference makers when it changes. It does this by calling a method of ReferenceMaker NotifyDependents(). Any reference target maintains a list of pointers to the items that reference it. Internally, the NotifyDependents() function loops through all these pointers and calls a method NotifyRefChanged() on each one. NotifyRefChanged() is the method implemented by the reference maker responsible for receiving and responding to the change notification messages.

The path controller also maintains a banking parameter and has created a reference to it, so the parameter has become a reference target. Whenever the user alters the value of the banking parameter, a message must be sent to notify the path controller it has changed. This is done by calling NotifyDependents(). As part of this function call it passes the message REFMSG_CHANGE. The NotifyDependents() call broadcasts this message to all the items which reference it. The bank parameter has one reference to it -- the path controller.

As a reference maker, the path controller must have a way to receive and respond to messages sent to it. The path controller must respond to the notification of the banking parameter changing and the spline path changing. It does this by implementing a method of the ReferenceMaker class called NotifyRefChanged(). The usual implementation of this method has a switch statement where each case is one of the messages the plug-in must respond to. The messages the plug-in must respond to depend upon the types of references it makes. There are additional messages which the system itself may need the plug-in to respond to. For example, say the spline which the path controller is following is deleted from the scene. The system needs to inform the plug-in that this has occurred. The plug-in developer must handle this possible condition by providing a case in NotifyRefChanged() to respond to the message REFMSG_TARGET_DELETED. See List of Reference Messages.

Details of the Change Notification Methods

This section looks in detail at the methods used by reference makers and targets for change notification.

Making References

To create a dependency upon an item, the developer creates a reference to the item. This is done by calling a method of ReferenceMaker called MakeRefByID().

RefResult MakeRefByID(

 Interval refInterval,

 int which,

 RefTargetHandle htarget

);

This method creates a reference between the object which calls the method, and the ReferenceTarget specified by the htarget parameter.

The refInterval parameter indicates the interval of time over which this reference is active. Outside this interval, the reference is not considered to be a dependency. This allows the plug-in to have dependent relationship over only portions of an entire animation time range. If a plug-in has a dependency over the entire animation it may use the pre-defined interval FOREVER for this parameter. In the current implementation all plug-ins must use FOREVER for this interval.

The which parameter indicates which reference index this newly created reference is assigned to. The system uses a virtual array mechanism to access the references an item has. The developer simply assigns an integer index to each reference. For example, the path controller might use an index of 0 for the bank amount parameter reference, and an index of 1 for the node to follow reference. The path controller would then pass either 0 or 1 as the which parameter depending upon which reference it was making. This is discussed in greater detail below under Reference Access Methods.

The hTarget parameter is the handle of the item to which a reference is being made.

The return value from this method is of type RefResult. This is usually REF_SUCCEED indicating the reference was created and is registered by the reference target.

Sending Change Notification Messages

When a reference target changes it must notify its dependent reference makers of this change. It does this by calling NotifyDependents().

RefResult NotifyDependents(

 Interval changeInt,

 PartID partID,

 RefMessage message,

 SClass_ID sclass=NOTIFY_ALL,

 BOOL propagate=TRUE,

 RefTargetHandle hTarg=NULL

);

This method broadcasts the message specified by the message parameter to all the items which reference the caller.

The partID parameter is used to pass message specific information to the items which will receive the message. See the Reference section Class ReferenceMaker NotifiyRefChanged() method for more details.

The changeInt parameter indicates the interval of time over which the change reported by the message is in effect. Currently all plug-ins must pass FOREVER for this interval.

The sclass parameter defaults to NOTIFY_ALL. If this value is passed to NotifyDependents() all dependents will be notified. Other super class values may be passed to only send the message to certain items whose SuperClassID matches the one passed.

The propagate parameter defaults to TRUE. This indicates that the message should be sent to all 'nested' dependencies. If passed as FALSE, this parameter indicates the message should only be sent to first level dependents. Normally this should be left to default to TRUE.

The hTarg parameter defaults to NULL. A plug-in developer should never pass anything for this parameter. It must always default to NULL.

Responding to Change Notification Message

A plug-in which makes references must implement a method to receive and respond to messages broadcast by its dependents. This is done by implementing the NotifyRefChanged() method of ReferenceMaker.

virtual RefResult NotifyRefChanged(

 Interval changeInt,

 RefTargetHandle hTarget,

 PartID& partID,

 RefMessage message

);

The plug-in developer usually implements this method as a switch statement where each case is one of the messages the plug-in needs to respond to. The message parameters passed into this method is the specific message which needs to be handled.

The changeInt interval is the interval of time over which the message is active. Currently, all plug-ins will receive FOREVER for this interval.

The hTarget parameter is the handle of the reference target the message was sent by. The reference maker uses this handle to know specifically which reference target sent the message.

The partID parameter contains information specific to the message passed in. Some messages don't use the partID at all. See the section below on Reference Messages and PartIDs for more information about the meaning of the partID for some common messages.

The return value from this method is of type RefResult. This is usually REF_SUCCEED indicating the message was processed. Sometimes, the return value may be REF_STOP. This return value is used to stop the message from being propagated to the dependents of the item.

Below is an example of the code structure usually used to implement NotifyRefChanged(). It is a simplified version taken from the path controller:

RefResult PathPosition::NotifyRefChanged(

  Interval changeInt,

  RefTargetHandle hTarget,

  PartID& partID,

  RefMessage message)

 {

 switch (message) {

  case REFMSG_CHANGE:

   // Code to handle the target changing...

   break;

  case REFMSG_TARGET_DELETED:

   if (hTarget == pathNode) {

    // Code to handle to path node being deleted...

    }

   break;

  };

 return REF_SUCCEED;

 }

Note: A plug-in should NOT normally call NotifyDependents() from its NotifyRefChanged() method. All the appropriate dependents are notified automatically and thus doing so is unnecessary.

Reference Access Methods

The system manages the access to an item's references by using a virtual array. If the plug-in makes references, it must implement three methods of ReferenceMaker to handle access to its references. These methods are:

int NumRefs();

The plug-in implements this method to return the total number of references it makes.

RefTargetHandle GetReference(int i);

The plug-in implements this method to return a reference handle to its 'i th' reference. The plug-in keeps track of its references using an integer array index for each one. When the system calls this method, the plug-in returns its 'i th' reference.

void SetReference(int i, RefTargetHandle rtarg);

The plug-in implements this method to store the reference handle passed into its 'i-th' reference. The plug-in simply keeps track of its references using an integer array index for each one. When the system calls this method, the plug-in stores its 'i-th' reference.

Below is an example of how these methods might be implemented

#define PATHPOS_BANK_REF 0

#define PATHPOS_PATH_REF 1

int NumRefs() { return 2; }

RefTargetHandle PathPosition::GetReference(int i)

 {

 if (i==PATHPOS_BANK_REF) {

  return bankAmount;

 } else {

  return pathNode;

  }

 }

void PathPosition::SetReference(

 int i,

 RefTargetHandle rtarg)

 {

 if (i==PATHPOS_BANK_REF) {

  bankAmount = (Control*)rtarg;

 } else {

  pathNode = (INode*)rtarg;

  }

 }

Maintenance Methods

The following are methods that may be used to delete references when they are no longer needed:

DeleteAllRefFromMe();

This deletes all references from the calling reference maker.

DeleteAllRefs();

Deletes all references to and from the calling reference maker.

DeleteMe();

This method deletes all references to and from the calling reference maker, sends the REFMSG_TARGET_DELETED message, handles Undo, and deletes the object.

Deleting and Replacing References as the Reference Structure Changes

The number of references maintained by a plug-in may change over time. This section presents information on the different approaches developers may use to allow the reference structure to change.

Basically, a developer can have two kinds of reference structures. One approach is to have the number fixed, where some references may go to NULL occasionally, but the number stays the same. Alternatively, a developer can have a variable number of references, and move them around as they are added and deleted.

The first option, where the number stays fixed, is accomplished by keeping the number of references returned by NumRefs() constant. An example of this from 3ds max is the 3D Texmap Marble (\MAXSDK\SAMPLES\MATERIALS\MARBLE.CPP). It has 4 references: the pblock, the xyzGen instance, and two sub-texmaps. When the texmap starts up fresh it doesn't have sub-maps. Yet it still returns 4 from NumRefs(). When the user clicks on a user interface button to add a sub-map, the texmap calls ReplaceReference() on the sub-texmap index and a reference to the new sub-texmap gets plugged-in. Thus the pointer to it is no longer NULL. Again, NumRefs() doesn't change, it is just that reference is no longer NULL. A user can also set the sub-texmap back to 'None' to stop using it. In this case DeleteReference() will be called (DeleteReference() breaks the connection between the pointer stored and the system -- this is done so 3ds max will not send change notification messages any longer). Again, even thought the reference was deleted NumRefs() still returns 4. DeleteReference() will set the pointer to NULL.

The other approach to structuring references involves altering the number of references returned from NumRefs(). A developer can, after deleting a reference, change NumRefs() to one less and move around the pointers so GetReference() and SetReference() still return the proper values. An example of when this might be used is if the data structure for the plug-in had a variable number of references that the user could directly alter. The Multi-SubObject material is structured like this (\MAXSDK\SAMPLES\MATERIALS\MULTI.CPP). It maintains a table of the sub-materials and the user can alter the number of these on the fly via the user interface. In its implementation of NumRefs() it returns the number of items in the table at that moment. When the user asks for fewer sub-materials (and thus fewer references) ReplaceReference() is called passing NULL and NumRefs() returns a smaller value. When the user adds additional sub-materials, new materials are created and ReplaceReference() is called passing pointers to these new materials.

Pointers and References

When a plug-in has a pointer to an item it should usually make a reference to the item as well. This will ensure the item is not deleted by the system (which would make the pointer invalid).

In 3ds max when the last reference to an item is deleted, the item itself is deleted.

For example, say your plug-in has a pointer to a sub-anim, but you have not created a reference to it. If this sub-anim appears in track view, the system will create a reference to it because track view needs a reference to the items it displays. When track view is finished displaying this sub-anim, it will delete the reference. If this is the last reference to the sub-anim, the sub-anim itself will be deleted. This would invalidate the pointer you are maintaining to the sub-anim. The way to prevent this from happening is to create a reference to the sub-anim. The system does not delete items that still have references to them. Therefore by making a reference to the item you ensure that the system will not delete it.

The one exception to this is nodes in the scene. Nodes may be deleted by the user by simply selecting the node and pressing the delete key. If an item has a reference to the node the user may still delete it. However the item that referenced the node will receive the message REFMSG_TARGET_DELETED via NotifyRefChanged(). It can then respond appropriately to the deletion.

At certain times a plug-in may need to have a two-way reference. For example, if there is an item that references another item, the referenced item cannot have a reference back to the thing that referenced it, because this would create a loop (a cyclic reference). Cyclic references are illegal. In other words if A has a reference to B, then B cannot have a reference to A.

The Ring Array plug-in has a situation where A has a reference to B, but B has a pointer to A (but doesn't have a reference to A since that would cause a loop). Again, whenever you have a pointer to something you should have a reference to the item as well. But B cannot reference A.

What is done in this case, where you want a two-way reference, is as follows. A has a reference to B, and A can give B a pointer to itself. A is then responsible for informing B if it gets deleted so B can set its pointer to A to NULL. So A is responsible for making sure B's pointer to A doesn't end up invalid. This is the way you can set up a two-way dependency yet not create a cyclic reference. The item that is being pointed at must manage the pointer for the item that maintains the pointer.

When this setup is saved to disk another problem arises. How does one save a pointer, since the value will of course be different when loaded. The solution to this is to use methods of the ISave and ILoad classes that let a developer save and restore pointers. This must be a pointer to one of the objects that the scene saves with the reference hierarchy, but it is not a pointer that itself is a reference. When the pointer is saved the method ISave::GetRefID() is used. This returns an integer ID that can be saved to disk. When loading, a method ILoad::RecordBackpatch() is used. This takes the ID and a pointer to a pointer and sets the pointer to point back at the item that was pointed at when things were saved. See Class ISave, Class ILoad for more details.

The Loading and Saving of References

The system takes care of loading and saving references when an object is saved to disk. An object does not need to explicitly save its references, nor does an object need to load its references. After a scene is loaded, an object's references will automatically be restored. The system does this by using the NumRefs(), GetReference() and SetReference() methods of the plug-in.

For example, the path controller plug-in has a reference to the spline shape node in the scene it follows. It does not save this node explicitly in its implementation of ReferenceMaker::Save(). Yet this reference will be restored whenever the file is loaded from disk. This happens because the reference is saved and restored automatically by the system.

For anything that you have a reference to that you want saved and reloaded, you need to have a class descriptor registered with the system. This is because the system needs to call ClassDesc::Create() on the item to create it upon reloading. See Class ClassDesc.

Note that since references are restored automatically at loading time 3ds max needs to call ReplaceReference(). This ReplaceReference() will cause a call to DeleteReference(), which checks if the return value of GetReference() is NULL. Therefore developers have to initialize their references to NULL, if the ClassDesc::Create() method is called with loading==TRUE. If they don't and GetReference() is called with a non initialized return value at loading time, 3ds max will crash.

While loading, that is in a plug-in's implementation of its Load() method, the references are NOT in place yet. The references are not in place until everything in the 3ds max file is loaded. If a developer needs to do something with the references when loading, a post load callback may be used. This is done using the method ILoad::RegisterPostLoadCallback(). The callback is called after all loading is complete. Inside the callback the plug-in's references will be in place and they may be processed by the plug-in.

Another way a post load callback is used is to restructure references during a Load(). For example, say a plug-in references a static number of objects followed by any number of additional objects. Later, as development progresses on the plug-in, another static object is added to the reference structure. It would be nice if old files that still used the older reference structure could be loaded. The way to solve this is as follows: After the plug-ins Load() method is called its references are put in place using SetReference(). What needs to happen is that the plug-in should check for older versions of files (by checking a saved version number) and set a flag to indicate the old version is being loaded. Then a post load callback is registered to turn off this flag once all the references have been established. With this mechanism in place, the SetReference() method can check this flag and integrate the old reference structure or the new reference structure accordingly.

For users of the r2 or later version of the SDK a new method is available to handle the above. See the method ReferenceMaker::RemapRefsOnLoad().

Referencing a Global Instance of a Non-Plugin Class

If a plug-in needs to have some sort of global instance that its other objects reference, but is itself not a specific plug-in type, then the super class ID of REF_TARGET_CLASS_ID should be used. The class of the global instance should be derived from ReferenceTarget.

Viewing Reference Messages

This section discusses a utility plug-in called the Reference Watcher. This plug-in may be used to help understand the reference structure of a choosen item and to monitor the reference messages it sends. This provides a quick, visual way to examine the use of references in MAX.

To run this plug-in you'll first need to build it. The project is in \MAXSDK\SAMPLES\HOWTO\REFCHECK. Load this project into VC++ and press F7 to build it. Place REFCHECK.DLU into your 3ds max DLL search path (for example in the STDPLUGS directory). You can then run the program by choosing How To/Reference Watcher in the utility panel.

This plug-in allows you to create a reference to a choosen Reference Target and then watch the messages sent as you work with the target. For instance, follow the steps below to analyze the reference messages sent by a node in the scene.

Reset 3ds max and create a single Box in the scene called Box01. Leave it selected after creation.

Launch the Reference Watcher from the Utility branch of the command panel.

Click on the Pick Reference Target to Watch button and choose the Box01 node from the dialog. This is done by opening the Objects branch and selecting the Box01 label then pressing OK.

Result: The Reference Watcher utility creates a reference to the box node in the scene and displays information about it. The upper most list box shows this list of reference messages. Since we haven't done anything to the node yet the message list is blank.

The second list box in the dialog shows the items that are referencing the node we've selected. This provides a way to get a general idea of who is referencing an item. In the case of the Box01 node these are:

0: Node

1: (This Reference Watcher)

2: Node Selection

3: Material Editor

4: Scene

5: Material Editor

6: Scene

7: Animatable.

The bottom most list box shows the references that the node itself has. For the Box01 node it has six reference targets. These are:

0: The Transform Controller

1: The Object Reference

2: The Pin Node for IK (not currently assigned -- NULL)

3: The Material Reference (not currently assigned -- NULL)

4: The Visibility Controller (not currently assigned -- NULL)

5: The Image Blur Controller (not currently assigned -- NULL)

As you work with the node (select it/deselect it, assign modifiers, bind to space warps, assign materials, change controllers, drag the item in the viewports, etc.), you can monitor the reference messages sent. They'll show up using the #define name from REF.H (for example REFMSG_CHANGE) in the upper most window. If you want to clear the message list so you can see what messages are sent for a particular action press the Clear Message List button first.

Developers can use this utility to help study the use of references inside MAX.

Additional Global Functions Related to the Reference Hierarchy

The following global functions are available for working with references:

Function:

void EnumRefHierarchy(ReferenceMaker *rm, RefEnumProc &proc);

Remarks:

This function provides a general purpose reference enumerator. It simply calls the RefEnumProc::proc() on each element in the reference hierarchy. See Class RefEnumProc.

Parameters:

ReferenceMaker *rm

The reference maker whose dependents will be enumerated.

RefEnumProc &proc

The callback object whose proc() method is called for each element.

Function:

ReferenceTarget *CloneRefHierarchy(ReferenceTarget *rm);

Remarks:

A new global function has been added for release 2.0 to clone a reference target and the hierarchy emanating from it. This function encapsulates the code necessary to clone a ReferenceTarget and the reference hierarchy emanating from it. It handles multiple instances correctly, using a RemapDir.

Parameters:

ReferenceTarget *rm

The reference target to clone.

Return Value:

A duplicate of the entire reference hierarchy of the item passed.

Sample Code:

In this example, a material is being copied from one MtlBase to another (source to dest). To copy all the sub-materials this function is used:

dest[i] = (MtlBase *)CloneRefHierarchy(source[i]);

Function:

BOOL DependsOn(RefMakerHandle mkr, RefMakerHandle targ);

Remarks:

This global function is available in release 2.0 and later only.

Returns TRUE if the is a path of references from mkr to targ; otherwise FALSE. Note that this return TRUE if mkr == targ.

Parameters:

RefMakerHandle mkr

The reference maker to check.

RefMakerHandle targ

The reference target to check.

Function:

void ClearAFlagInHierarchy(ReferenceMaker *rm, int flag);

Remarks:

This function is available in release 2.0 and later only.

Clears the specified Animatable flag(s) from each item in the reference hierarchy.

Parameters:

ReferenceMaker *rm

The reference maker whose dependents will be enumerated and have their flags cleared.

int flag

See List of Animatable Flags.

Summary

References are used to handle dependencies between items in the scene. A plug-in making references is responsible for implementing several methods. The method NotifyRefChanged() is used to receive messages that something it references has changed. The plug-in must also implement a method NumRef() to return the number of references it makes. Methods GetReference() and SetReference() must be implemented by the plug-in to allow the system to access its references. If an item is a reference target, it must call NotifyDependents() to broadcast a message whenever it has changed in a way which affects other elements in the scene.