Keyframe and Procedural Controller Data Access
See Also: Class IKeyControl, Class INode, Class Control, Class Matrix3, Class AngAxis, Class Quat, Class ScaleValue, Matrix Representations of 3D Transformations.
Overview
This topic presents information on accessing the data of MAX's keyframe and procedural controllers. There are sections which explain how the keyframe data for the PRS controller can be interpreted, various APIs for keyframe data access, and procedural controller data access.
Example keyframe controllers are the Tension/Continuity/Bias (TCB), Bezier, and Linear controllers. Each keyframe controller in 3ds max is used to animate a value. The keys of the controller store the values at the key frames. The controller's job is to interpolate between these keys. This topic discusses the way developers may access and interpret the keys stored by keyframe controllers (in particular the keys stored by the Position/Rotation/Scale transform controllers).
3ds max also allows the use of procedural controllers. Procedural controllers are those that compute their value algorithmically, based on user specified parameters or outside data, rather than interpolating between stored keyframes. Examples include the Noise Controller, Expression Controller, Audio Controller, Path Controller, and Waveform Controller. This topic presents information on how procedural controllers may be sampled to retrieve their data.
Source code is provided demonstrating how developers may access both keyframe and procedural controller data.
Interpretation of Keyframe Key Data for the PRS Controller
The default keyframe transform controller in 3ds max is a PRS controller. This means that it handles orienting a node in the scene using separate controllers for Position, Rotation and Scale. To generate a complete transformation, first the position is applied, then the rotation, then the scaling.
Position and scale keys are stored in the local coordinate system of the controller, with each key independent of the others. Rotation keys are stored in local coordinates as well, but each is stored relative to the previous key in the track. Thus the first key stored represents the actual value for the rotation key in terms of the relative space of the controller. The second key is just an offset from the first key. Thus to get the actual rotation for the second key you need to multiply it on the left by the first key. To get the third key, you need to multiply it by the second computed key (which was computed from the first). These computed rotation key values will then be the absolute quaternions for each key for that controller.
It is important to understand however that the entire animation track for position, rotation and scale are all relative to the matrix passed into the controller when its GetValue() method is called. For additional details on how a controller updates GetValue() see the Advanced Topics section Node and Object Offset Transformations.
Note the following:
· When developers access key data from MAX, the data is returned in the local coordinate system of the controller.
· The key values that appear in the Key Info dialog for a controller are in the local coordinate system of the controller (with the appropriate units for display -- for example, angles are shown in degrees in Key Info but are stored in radians).
Keyframe Access Classes and Methods
This section discusses access to keyframe controller data. The 3ds max API provides a class IKeyControl. This class provides an interface into the TCB, Bezier, and Linear keyframe controllers allowing a developer to add, delete, retrieve and update the keys of the controller.
Use of the IKeyControl methods requires you to include \MAXSDK\INCLUDE\ISTDPLUG.H, i.e.:
#include "ISTDPLUG.H"
The standard 3ds max PRS keyframe controllers provide access to their keys using this class. To get an interface you can use to call methods of IKeyControl use the following macro (defined in \MAXSDK\INCLUDE\ANIMTBL.H):
#define GetKeyControlInterface(anim)
((IKeyControl*)anim->GetInterface(I_KEYCONTROL))
A plug-in developer may use this macro as follows:
IKeyControl *ikc = GetKeyControlInterface(anim);
The return value will either be NULL or a pointer to a valid controller interface. A NULL pointer indicates the controller does not support this interface to allow access to its data.
The following is an example of getting a position controller interface from a node in the scene. The first thing that happens is the position controller is retrieved from the node (using methods of class INode and Control) and then the controller interface is returned via the GetKeyControlInterface() macro:
Control *c;
c = node->GetTMController()->GetPositionController();
IKeyControl *ikeys = GetKeyControlInterface(c);
if (!ikeys) return; // No interface available to access the keys...
The methods of IKeyControl may be called using the ikeys pointer. For example the method GetKey(int i,IKey *key) retrieves the 'i-th' key and stores the result in key. You'll need to check the ClassID of the controller so you can pass the appropriate class to the method to retrieve the keys. There are three type of controllers and five data types that are supported by the IKeyControl interface. The controller types are Tension/Continuity/Bias (TCB), Bezier, and Linear. The data types are floating point (float), Position (Point3), Rotation (Quat or AngAxis), and Scale (ScaleValue). The type of key you pass to the GetKey() or SetKey() methods depends on both the controller and data type. The following classes are used for key storage for each valid possibility:
Tension/Continuity/Bias:
Bezier:
Linear:
See the following code for an example:
ITCBPoint3Key tcbPosKey;
int numKeys = ikeys->GetNumKeys();
if (c->ClassID() == Class_ID(TCBINTERP_POSITION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &tcbPosKey);
DebugPrint(_T("\nPosition Key: %d=(%.1f, %.1f, %.1f)"),
i, tcbPosKey.val.x, tcbPosKey.val.y, tcbPosKey.val.z);
}
}
Note how an instance of the ITCBPoint3Key class was passed to GetKey() since the ClassID indicates the controller is a TCB controller and we want position keys (which operate on Point3s).
Keyframe Interpolation in MAX
The function used for interpolation between keys depends on the type of controller. In general, 3ds max uses cubic (polynomial of degree three) splines for interpolation. Rotations are done using spherical linear interpolation (slerps). Registered developers who are interested in the exact details of keyframe interpolation have the source code 3ds max uses internally as part of the Debug SDK. The bezier interpolation code is available in \MAXSDKDB\SDKSRC\INTERP.CPP and the TCB interpolation is in \MAXSDKDB\SDKSRC\TCBINTRP.CPP. This code is not available for non-registered developers. See the Advanced Topics section on Debugging for more details on the Debug SDK.
Several developers have tried to use the IKeyControl interface and then operate on the keys to get and interpolate them in 'world space'. This is really a problematic thing to try do because the keys are interdependent. For instance, consider scale keys. In world space scale keys are dependent on the position and rotation of the controller as well as the parent transformation. This is because first an object is scaled, then it is rotated, then its position is applied, then the parent transformation is applied. So the scale is rotated. If you scale an object along its X axis and then rotate it the scale becomes rotated. The scale is no longer rotated about the world X axis, it's rotated about some other axis. This is normally what you want, i.e. you don't want the object to rotate through the scaled spaced (this would cause it to skew). So to talk about the scale in world space is a strange concept.
Further, the interpolation of keys that are in world space is equally problematic. If keys are put into world space, then interpolated in world space, the resulting animation will be very different that what would be seen inside MAX. For example, consider an object that is rotating 360 degrees about its local Z axis and is tilted at some arbitrary angle. If you interpolate the way 3ds max does, all the interpolation is done in the local coordinate system. Thus the object would rotate about its local Z axis (which is tilted). However if you were to try to interpolate about in world space the rotation would occur about the world Z axis. The object will still end up in the same place, but it will be a very different animation than what 3ds max would have.
The bottom line is that developer need to understand the way 3ds max computes its node transformation based on the keyframes and use the information to interpolate between keys accordingly.
Procedural Controller Data Access
3ds max allows users to use procedural controllers in addition to keyframe controllers. Procedural controllers are those that compute their value algorithmically rather than interpolate between stored keyframes. Developers may need to retrieve values from these controllers but won't be able to use the IKeyControl interface since no keys are available. In these cases, one must sample the controller to retrieve its value at each frame required. This is done by calling the Control::GetValue() method directly. The following sample code samples the controller for each frame in the current animation interval and DebugPrint()s the values to the VC++ IDE debug window. Note how the parent matrix of the node is passed into the GetValue() call. This allows GetValue() to update the appropriate matrix. In the code below, the position coordinates printed by DebugPrint() will be in world space.
void Utility::SampleController(INode *n, Control *c) {
TimeValue t;
Point3 trans;
Matrix3 pmat;
Interval ivalid;
int tpf = GetTicksPerFrame();
int s = ip->GetAnimRange().Start()/tpf,
e = ip->GetAnimRange().End()/tpf;
// Sample the controller at every frame in the anim range
for (int f = s; f <= e; f++) {
t = f*tpf;
ivalid = FOREVER;
pmat = n->GetParentTM(t);
c->GetValue(t, &pmat, ivalid, CTRL_RELATIVE);
trans = pmat.GetTrans();
DebugPrint(_T("\nPosition at frame: %d of %d=(%.1f, %.1f, %.1f)"),
f, e, trans.x, trans.y, trans.z);
}
}
A developer could also get the relative values from the PRS controller by passing in the identity matrix and not the parent matrix to GetValue(). This is appropriate for the PRS transform controller but not the Look At transform controller. With a Look At controller, its relative values are actually a function of the input matrix. If you pass in the identity to its GetValue() method, it will think that the object is positioned at the center of the world and the results won't be meaningful.
Sample Code
The following sample code demonstrates access to the controller data for the first node in the current selection set. The code checks to see if the controller provides its data via the IKeyControl interface. If it does, this interface is used to get the keys. If it does not, the controller is sampled at each frame to get its data. For the keyframe controllers, note how the ClassIDs are checked to ensure the proper classes are used when getting key values. Also note how the rotation keys are derived by multiplying on the left by the previous key. For the procedural controllers, note how the parent matrix of the node is passed to the GetValue() method.
// Display the data of the controller. For TCB, Bezier and Linear
// keyframe PRS controllers the position, rotation and scale values
// are displayed in the local space of the controller. For
// procedural controllers (or those that don't support the
// IKeyControl interface), position data is displayed in world space.
void Utility::KeyTest() {
int i, numKeys;
INode *n;
Control *c;
Quat newQuat, prevQuat;
IKeyControl *ikeys;
ITCBPoint3Key tcbPosKey;
ITCBRotKey tcbRotKey;
ITCBScaleKey tcbScaleKey;
IBezPoint3Key bezPosKey;
IBezQuatKey bezRotKey;
IBezScaleKey bezScaleKey;
ILinPoint3Key linPosKey;
ILinRotKey linRotKey;
ILinScaleKey linScaleKey;
// Get the first node in the selection set
if (!ip->GetSelNodeCount()) return;
n = ip->GetSelNode(0);
// --- Process the position keys ---
c = n->GetTMController()->GetPositionController();
ikeys = GetKeyControlInterface(c);
if (!ikeys) {
// No interface available to access the keys...
// Just sample the controller to get the position
// data at each key...
SampleController(n, c);
return;
}
numKeys = ikeys->GetNumKeys();
DebugPrint(_T("\nThere are %d position key(s)"), numKeys);
if (c->ClassID() == Class_ID(TCBINTERP_POSITION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &tcbPosKey);
DebugPrint(_T("\nTCB Position Key: %d=(%.1f, %.1f, %.1f)"),
i, tcbPosKey.val.x, tcbPosKey.val.y, tcbPosKey.val.z);
}
}
else if (c->ClassID() == Class_ID(HYBRIDINTERP_POSITION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &bezPosKey);
DebugPrint(_T("\nBezier Position Key: %d=(%.1f, %.1f, %.1f)"),
i, bezPosKey.val.x, bezPosKey.val.y, bezPosKey.val.z);
}
}
else if (c->ClassID() == Class_ID(LININTERP_POSITION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &linPosKey);
DebugPrint(_T("\nLinear Position Key: %d=(%.1f, %.1f, %.1f)"),
i, linPosKey.val.x, linPosKey.val.y, linPosKey.val.z);
}
}
// --- Process the rotation keys ---
c = n->GetTMController()->GetRotationController();
ikeys = GetKeyControlInterface(c);
if (!ikeys) return;
numKeys = ikeys->GetNumKeys();
DebugPrint(_T("\nThere are %d rotation key(s)"), numKeys);
if (c->ClassID() == Class_ID(TCBINTERP_ROTATION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &tcbRotKey);
newQuat = QFromAngAxis(tcbRotKey.val.angle, tcbRotKey.val.axis);
if (i) newQuat = prevQuat * newQuat;
prevQuat = newQuat;
DebugPrint(_T("\nTCB Rotation Key: %d=(%.1f, %.1f, %.1f, %.1f)"),
i, newQuat.x, newQuat.y, newQuat.z, newQuat.w);
}
}
else if (c->ClassID() == Class_ID(HYBRIDINTERP_ROTATION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &bezRotKey);
newQuat = bezRotKey.val;
if (i) newQuat = prevQuat * newQuat;
prevQuat = newQuat;
DebugPrint(_T("\nBezier Rotation Key: %d=(%.1f, %.1f, %.1f, %.1f)"),
i, newQuat.x, newQuat.y, newQuat.z, newQuat.w);
}
}
else if (c->ClassID() == Class_ID(LININTERP_ROTATION_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &linRotKey);
newQuat = linRotKey.val;
if (i) newQuat = prevQuat * newQuat;
prevQuat = newQuat;
DebugPrint(_T("\nLinear Rotation Key: %d=(%.1f, %.1f, %.1f, %.1f)"),
i, newQuat.x, newQuat.y, newQuat.z, newQuat.w);
}
}
// --- Process the scale keys ---
c = n->GetTMController()->GetScaleController();
ikeys = GetKeyControlInterface(c);
if (!ikeys) return;
numKeys = ikeys->GetNumKeys();
DebugPrint(_T("\nThere are %d scale key(s)"), numKeys);
if (c->ClassID() == Class_ID(TCBINTERP_SCALE_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &tcbScaleKey);
DebugPrint(_T("\nTCB Scale Key: %2d=(%.1f, %.1f, %.1f)"),
i, tcbScaleKey.val.s.x, tcbScaleKey.val.s.y,
tcbScaleKey.val.s.z);
}
}
else if (c->ClassID() == Class_ID(HYBRIDINTERP_SCALE_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &bezScaleKey);
DebugPrint(_T("\nBezier Scale Key: %2d=(%.1f, %.1f, %.1f)"),
i, bezScaleKey.val.s.x, bezScaleKey.val.s.y,
bezScaleKey.val.s.z);
}
}
else if (c->ClassID() == Class_ID(LININTERP_SCALE_CLASS_ID, 0)) {
for (i = 0; i < numKeys; i++) {
ikeys->GetKey(i, &linScaleKey);
DebugPrint(_T("\nLinear Scale Key: %2d=(%.1f, %.1f, %.1f)"),
i, linScaleKey.val.s.x, linScaleKey.val.s.y,
linScaleKey.val.s.z);
}
}
}
// Display the position data of controller in world coordinates for each
// frame in the animation range
void Utility::SampleController(INode *n, Control *c) {
TimeValue t;
Point3 trans;
Matrix3 pmat;
Interval ivalid;
int tpf = GetTicksPerFrame();
int s = ip->GetAnimRange().Start()/tpf,
e = ip->GetAnimRange().End()/tpf;
// Sample the controller at every frame in the anim range
for (int f = s; f <= e; f++) {
t = f*tpf;
ivalid = FOREVER;
pmat = n->GetParentTM(t);
c->GetValue(t, &pmat, ivalid, CTRL_RELATIVE);
trans = pmat.GetTrans();
DebugPrint(_T("\nPosition at frame: %d of %d=(%.1f, %.1f, %.1f)"),
f, e, trans.x, trans.y, trans.z);
}
}
Summary
This section provided an overview of accessing 3ds max controller data. Since 3ds max supports both keyframe and procedural controllers different techniques are required to retrieve the data. Keyframe controller can usually use the IKeyControl interface. Procedural controller must be sampled using GetValue(), usually at each frame. The values that a controller stores are in the local coordinate system of the controller. The matrix passed to GetValue() for a transform controller defines the coordinate system of the transformation.