Lesson 4: Animation Controllers

Users animate their objects in 3ds Max by changing the object's properties over time. 3ds Max uses "animation controllers" for this purpose. Animation controllers - or "controllers" for short - are plug-ins whose main responsibility is to provide 3ds Max with the value of a parameter at a given time, whenever 3ds Max inquires them. In mathematical terms, controllers implement a function of time whose output is a point in either one dimensional space (float controllers) or in a multi dimensional space (e.g. position or rotation controllers). A common application of controllers is to update the position, rotation or scale (PRS) parameters of a node associated with an object. Note that the scene graph does not store the geometric objects, but rather nodes associated with those objects and their own PRS parameters.

The type of the inquired parameter determines the type of the controller. The following is the list of basic controller types (with the type of parameter they control in parentheses):

Controllers are divided into two categories based on their behavior: procedural and keyframe. Procedural controllers typically does not take any input other than the initial value at time zero, and generates outputs when inquired. They use an algorithm that generates outputs for different time values. A keyframe controller on the other hand provides an interface with which the user can set the value of the animated parameter at certain points in time. The controller usually caches these values and uses them to interpolate the value of the animated parameter at other points in time. We will first introduce procedural controllers in a basic example, and then look at keyframe controllers.

Procedural Controllers

Procedural controllers are extended from the base class Control. The main responsibility of these controllers is to answer a query from 3ds Max on a parameter's value at the given time, and to include the validity Interval for that answer. Including a validity interval reduces the communication cost between 3ds Max and your plug-in since 3ds Max does not query the controller during the intervals in which the value does not change.

Procedural controllers do not need to maintain a cache since 3ds Max never tries to write any value to the parameter they are controlling. Developers can use an algorithm that produces values for different times. This should be done in the member function Control::GetValue() and is explained in the example below, where a procedural controller plug-in named SampleProCtrl is assigning a periodic movement to an object:

void SampleProCtrl::GetValue(TimeValue t, void *ptr, Interval &valid, GetSetMethod method)
{
    Point3 p3OurAbsValue(0, 0, 0);
    p3OurAbsValue.x = 15*sin((float)t/960);
    p3OurAbsValue.y = (float)t/192;
	   valid.Set(t,t+1); //This answer is only valid at the calling time.

	   if (method == CTRL_ABSOLUTE)
	   {
	      	Point3* p3InVal = (Point3*)ptr;
		      *p3InVal = p3OurAbsValue;
	   }
	   else // CTRL_RELATIVE
	   {
		      Matrix3* m3InVal = (Matrix3*)ptr;
		      m3InVal->PreTranslate(p3OurAbsValue);
	   }
}

Controller::GetValue() parameters:

This controller animates the object with a periodic movement along the x axis with the mathematical function x=a.sin(w.t). The terms a and w are just two coefficients to control the amplitude and frequency of the periodic movement so the movement can be observed easily in the viewport (a = 15 and w = 1/960 in this example). There is also a linear movement along the y axis (y=b.t where b is a coefficient representing the linear speed and is set to 1/192). The function Matrix3::PreTranslate() is used if a relative answer is required. It applies an incremental translation transformation to the matrix m3InVal that is initialized to the reference point.

The project Lesson4a implements an animation controller plug-in. The class name is SampleProCtrl and it appears in 3ds Max with the name "Sample Procedural Controller". To assign it to an object in the view port, while the object is selected in 3ds Max click on the Motion tab, expand Assign Controller and select Position from the Transform: Position/Rotation/Scale tree. Then click on the green check mark under the title Assign Controller to find "Sample Procedural Controller" and assign it to your object. You can then click on the Play Animation button to see the effect of your animation controller. You can also use the Curve Editor to assign it to an object.

Although we are not holding any reference in our very basic controller, you can see that the four basic functions discussed in Lesson 3 are implemented here. That is because an animation controller plug-in extends from the control class, which indirectly extends from the ReferenceMaker class. You also need to implement the pure virtual function Control::Copy() to be able to instantiate from your plug-in class. Notice that the x, y and z curves are observed all in the same graph in the curve editor and you cannot have them in separate graphs. You will need to break your position controller into basic independent controllers and implement two other functions, Control::NumSubs() and Control::SubAnim() to observe them as separate graphs in the curve editor. We talk about these functions later in this lesson.

Keyframe Controllers

A keyframe controller extends from the base class Control, similar to a procedural controller. It only differs from a procedural controller in the way it implements the member functions. Similar to a procedural controller, 3ds Max calls Control::GetValue() to read values of an animated parameter with a keyframe controller. However, it can also write values to an animated parameter of a keyframe controller. When calling to write a value to the controller, 3ds Max indicates at which time and for how long the value it provides is valid. It is the responsibility of the keyframe controller to maintain a data cache to store those values and interpolate between them for unknown values. 3ds Max has no restriction on how the controller plug-in handles this cache. While it is possible to implement and maintain your own cache to keep this data, it is very common to use multiple default basic controllers to form the aggregated animation controller. In such case, it is a good practice to hold one reference per basic controller and access them through the references.

The simplest type of controller is the float controller. It can be combined to form the elements of an aggregated controller. For example, three float controllers can be used to control each of the x, y and z elements in a point3 or position controller. While developers can implement any interpolator (e.g. constant, linear, polynomial, etc) for float controllers, it is normal in the computer graphics applications to use Bezier interpolation. 3ds Max SDK already has a default implementation of a float controller that uses this type of interpolator. Developers can use the function Control *NewDefaultFloatController() to instantiate a new object of a float controller with Bezier interpolation. We will use this function for our example here.

Example: an X-Y controller

We will demonstrate creating a simple position controller that only moves the object along the X and Y axis. Instead of maintaining our own X and Y animation values, we are going to use two Bezier float controllers, each responsible for one dimension. We will hold two references in our plug-in class, each making a reference to one of those Bezier float controllers. The following code snippet shows these two references as private data members in our plug-in class:

class SampleKFCtrl: public Control
{
private:

    Control* mpXCtrl;
    Control* mpYCtrl;

    enum MyRefs 
    {
        kXCtrlRef,
        kYCtrlRef,
        kNumRefs
    };

public:

    ...
};

And we initiate them in the constructor as following:

SampleKFCtrl::SampleKFCtrl()
{
    ReplaceReference(kXCtrlRef, NewDefaultFloatController());
    ReplaceReference(kYCtrlRef, NewDefaultFloatController());
}

Now we have two references to two basic float controllers, and we will use them in the functions Control::SetValue() and Control::GetValue(). Our plug-in can be considered a medium between 3ds Max and our basic controllers:

void SampleKFCtrl::GetValue(TimeValue t, void *ptr, Interval &valid, GetSetMethod method)
{
    Point3 p3OurAbsValue(0, 0, 0);
    mpXCtrl->GetValue(t, &p3OurAbsValue.x, valid, CTRL_ABSOLUTE);
    mpYCtrl->GetValue(t, &p3OurAbsValue.y, valid, CTRL_ABSOLUTE);

    if (method == CTRL_ABSOLUTE)
    {
        Point3* p3InVal = (Point3*)ptr;
        *p3InVal = p3OurAbsValue;
    }
    else // CTRL_RELATIVE
    {
        Matrix3* m3InVal = (Matrix3*)ptr;
        m3InVal->PreTranslate(p3OurAbsValue);
    }
}
void SampleKFCtrl::SetValue(TimeValue t, void *ptr, int commit, GetSetMethod method)
{
    Point3* p3Val = (Point3*)ptr;
    mpXCtrl->SetValue(t, &p3Val->x, commit, CTRL_ABSOLUTE);
    mpYCtrl->SetValue(t, &p3Val->y, commit, CTRL_ABSOLUTE);
}

There are two virtual functions without any default implementation that you need to implement in your plug-in or your plug-in will not compile. The first one is Control::NotifyRefChanged() that we introduced in Lesson 3 of the learning path. This function is called whenever a reference in our class is changed (for example, when the user modifies the curve of one of the float sub-controllers in the curve editor). The other virtual function is Control::Copy(), and is used to let the user smoothly assign our controller to an object that has already been animated by another controller. When the user assigns a different controller to the object in the track view, the function Animatable::AssignController() is invoked, which in turns calls the Control::Copy() in our plug-in so we get a chance to copy the keyframe values from the old controller. We might want to use at least the position of the object at the time zero so the object keeps its original coordinates.

As indicated earlier, we are using two default float controllers that are assigned to our reference variables (mpXCtrl and mpYCtrl) in our constructor. We should remember from our previous lesson that we should use references to the objects derived from ReferenceTarget if we have a dependency on them. This holds true here and we use references and call Control::ReplaceReference() (which then calls Control::setReference() to assign the float controllers to our sub-controllers). We also need to implement the member function Control::setReference() for this purpose.

Our plug-in at this point can handle the values for the position of the object it is assigned to. The user can use either auto key or set key animation mode to animate his objects using this controller. However, he still cannot visualize the individual sub-controller in track view curve editor, and neither can he modify the curves there. Remember that the curve editor plots one or more 2D curves, each demonstrating the value of one float parameter over time. As a result, the curve editor cannot plot an animated parameter unless it can be divided into a finite number of float sub-controllers. Although we are using float sub-controllers, 3ds Max has no way to know it unless we expose them as sub-animatables and implement the two functions Control::NumSubs() and Control::SubAnim(). The first function here returns the number of float sub-controllers in our animation controller (e.g. 2 for an XY position controller), and the second function should return a pointer to the float sub-controller whose index is given. These two functions are sufficient for the curve editor to display the controller. The following shows how these two functions can be implemented (do not forget to include the declaration in your header file):

int SampleKFCtrl::NumSubs() { 
    return 2; 
}

Animatable* SampleKFCtrl::SubAnim(int n) { 
    return (n==0) 
        ? mpXCtrl 
        : mpYCtrl; 
}
The project Lesson4b implements a key frame controller plug-in. The main class is SampleKFCtrl and it appears in 3ds Max with the name Sample Key Frame Controller.