The Node and Object Offset Transformations
See Also: Class INode, Class Interval, Class Quat, Class ScaleValue, Matrix Representations of 3D Transformations, Keyframe and Procedural Controller Data Access.
Overview
This topic provides information on two transformations related to nodes -- the node transformation matrix and the object-offset transformation. This section also presents information on how these transformation are constructed, how they are used by MAX, and how they may be used by developers. This section also discusses the methods of the INode class that access these transformations.
The Pivot Point -- The Node Transformation Matrix
The transform center, or pivot point, is the location about which a rotation takes place, or to and from which a scale occurs. All nodes in 3ds max have a pivot point. You can think of the pivot point as representing a node’s local center and local coordinate system.
The pivot point of a node is used for a number of purposes:
· As the center for rotation and scaling.
· Defines the transform origin for linked children.
· Defines the joint location for Inverse Kinematics.
The thing that most users think of as the pivot point -- graphically represented in 3ds max by the axis tripod that is displayed when a node is selected and the coordinate system is set to local -- is actually just a visual representation of the node's transformation matrix (NodeTM).
The node's transformation matrix is what is controlled by the transform controller that places the node in the scene. The transform controller controls the transformation relative to the node's parent.
How a Transform Controller Computes the NodeTM
This section provides an overview of how 3ds max gets data from a transform controller to create the node transformation matrix (NodeTM).
A controller can be thought of as operating internally within its own local coordinate system. For both keyframe and procedural controllers, the data generated is provided in the local space of the controller. It is not until the controller supplies its transformation to 3ds max does the coordinate system become relative to anything other than the controller itself. The way this works is as follows:
All controllers are derived from class Control (or StdControl, which is itself derived from Control). Every controller, regardless of type, provides data to 3ds max at a specified time via a method of the Control class called GetValue().
Prototype:
virtual void GetValue(TimeValue t, void *val, Interval &valid,
GetSetMethod method=CTRL_ABSOLUTE)=0;
One of the parameters passed to this method is void *val. Depending on the controller type, different data types are passed using this pointer (see Class Control::GetValue() for a complete discussion of all the data types). For example, a transform controller will be passed a Matrix3 data type. It is the job of this transform controller to update the matrix with its transformation. It does this by pre-multiplying its transformation with the matrix passed. The controller operates within its own space internally, but when it pre-multiplies its data with the matrix passed, it essentially means the controller is then working in the coordinate system of the matrix. Another way to think of this is that the Matrix3 passed is actually transforming the controllers data. Thus, what the final coordinate system ends up being depends upon the client of the controller and not on the controller itself -- it depends on the matrix passed to GetValue().
To better understand this consider the following examples. For a transform controller, the matrix passed to GetValue() is the transformation matrix of the node's parent. Consider a simplified keyframe transform controller that only provided position (no rotation or scale). This controller would just update the bottom row of the matrix (translation data) passed by pre-multiplying its value. If the node the controller was assigned to was not hierarchically linked, that is it had no parent other than the world, then the matrix passed to GetValue() would be the identity. In this case, when the controller pre-multiplied its value, it would simply be setting the translation row.
Consider a different case when the node the controller is assigned to is hierarchically linked. As before, the keys of the controller are stored in the local space of the controller. But in this case, when GetValue() is called, the matrix passed is not necessarily the identity. Rather it will be the transformation matrix of the parent of the node the controller is assigned to. If the parent is not located at the origin, or is scaled or rotated in some way, the parent matrix passed will provide an additional transformation to the one the controller itself provides.
For transform controllers, the matrix that is updated in GetValue()becomes the NodeTM and is used to position and orient the node in the scene. The next section discusses they way the transformations are applied to the matrix by the PRS controller to create the NodeTM.
Construction of the NodeTM for the PRS Transform Controller
This section describes how 3ds max constructs the NodeTM for the PRS controller. The PRS controller uses sub-controllers to create the final transformation. There are three sub-controllers -- one for Position, one for Rotation and one for Scale. The PRS controller is passed a matrix to its Control::GetValue() method. This matrix is the parent of the node. If the node has no parent, the matrix starts out as the identity matrix. GetValue() is first called on the position controller, then the rotation controller, then the scale controller. In the case of a node with no parent, when its position controller's GetValue() method is called, the identity is passed. First the position controller pre-multiplies its position. If the matrix passed is the identity, this is equivalent to setting the bottom row (the translation row) of the matrix. Next, GetValue() is called on the rotation controller and the matrix passed includes the position controller information. The rotation is pre-multiplied in. Next, GetValue() is called on the scale controller and the matrix passed includes the position and rotation controller information. The scale is pre-multiplied in.
The position is not affected by rotation or scale. The rotation is affected by the position. The scale is affected by the position and rotation. Said another way, the position is in the space of the parent. The rotation is in the space of the parent PLUS the position (the position has already applied itself to the matrix). Scale works in the coordinate system of the parent PLUS position PLUS rotation. The scaling is pre-multiplied however, so the scaling takes place about the local coordinate system, but then if the node is rotated, this scaling is rotated around. If the transformations were not applied in this way, the scaling would be skewed by the rotation.
Note that some position controllers can actually apply more than just position to the matrix. For example, the Path Controller when the Follow switch is active. In this case, the position controller actually applies some rotation to have the node remain tangent to the path it is following. Thus when the rotation controllers receives the matrix in GetValue(), the matrix already has some rotation applied.
The matrix passed to GetValue(), after the position, rotation and scale transformation have updated it, becomes the NodeTM 3ds max uses to position, rotate and scale nodes in the scene.
Methods
There is a method of the INode class that a developer may use to get the node transformation matrix:
virtual Matrix3 GetNodeTM(TimeValue t, Interval* valid=NULL)=0;
This method returns the world space transformation matrix of the node at the specified time. This matrix contains its parents transformation. The Node TM is inherited. The Node TM may be considered the world space transformation as far as kinematics is concerned.
There are two methods that a developer may use to set the node transformation matrix. From INode is:
virtual void SetNodeTM(TimeValue t, Matrix3& tm)=0;
This sets the node's world space transformation matrix. This will call SetValue() on the transform controller.
The following method if from Class Interface:
virtual void SetNodeTMRelConstPlane(INode *node, Matrix3& mat)=0;
This sets the node's transform relative to the current construction plane. This may be used during creating so you can set the position of the node in terms of the construction plane and not in world units.
The Object-Offset Transformation
The object offset transformation provides an offset of the geometry of an object from its node. This section describes the object offset transformation and its function in MAX.
One can see a node's pivot point graphically represented in 3ds max by selecting a node, going to the Hierarchy branch of the command panel, selecting the 'Pivot' button, and choosing either the 'Affect Pivot Only' or 'Affect Object Only' button. This displays a large axis tripod that shows the location and orientation of the node's pivot point (NodeTM). By choosing one of these buttons, and using the Move/Rotate/Scale toolbar controls, a user can manipulate the position of the geometry of the object independent of the pivot point. Or they may manipulate the pivot point independent of the geometry of the object position. One can think of the geometry of the object as the 'mesh' of the object.
The way the user is able to independently manipulate the pivot and the object is managed internally using the object-offset transformation. The object-offset transformation affects how the geometry of the object is related to the node. The object-offset transformation transforms the geometry of the object itself some amount from the node.
To understand how the object-offset is used, consider the following example from the 3ds max user interface. In the Hierarchy branch under 'Pivot', when the user has chosen the 'Affect Object Only' button they are free to move the geometry of the object around independent of the node. The pivot point does not move -- only how the geometry of the object is oriented relative to the pivot. What is happening internally is the object-offset transformation is being manipulated. This transformation is simply an additional offset that may be applied to the geometry of the object that is independent of the node. The object-offset transformation is not inherited by any child nodes.
As another example consider the use of the 'Affect Pivot Only' button. This mode lets the user move the pivot without affecting the position of the geometry of the object. When the system allows the user to move the pivot point, what is actually happening is the node's transformation is being altered (to re-orient the pivot point), then the object-offset transformation is adjusted to counter the node transformation. This lets the geometry of the object stay in the same place while the pivot point moves around. So again, when the user is moving the pivot point, 3ds max is actually adjusting the node transformation matrix and counter-adjusting the object-offset transformation.
Construction of the Object Offset Transformation
The object offset transformation consists of separate position, rotation and scale transformations. Like the NodeTM, these are applied by pre-multiplying position, then rotation, then scale. Thus the object offset transformation is:
Object Offset Transformation = Offset Scale * Offset Rotation * Offset Position
Unlike the NodeTM, the Object Offset Transformation is not inherited by children of the node.
Methods
The INode class has methods to get and set individual transformations of the object offset. These are:
virtual void SetObjOffsetPos(Point3 p)=0;
Sets the position portion of the object offset from the node.
virtual Point3 GetObjOffsetPos()=0;
Returns the position portion of the object-offset from the node as a Point3.
virtual void SetObjOffsetRot(Quat q)=0;
Sets the rotation portion of the object offset from the node.
virtual Quat GetObjOffsetRot()=0;
Returns the rotation portion of the object-offset from the node.
virtual void SetObjOffsetScale(ScaleValue sv)=0;
Sets the scale portion of the object offset from the node.
virtual ScaleValue GetObjOffsetScale()=0;
Returns the scale portion of the object-offset from the node.
Constructing an offset TM for a node is done with the following code:
Matrix3 tm(1);
Point3 pos = node->GetObjOffsetPos();
tm.PreTranslate(pos);
Quat quat = node->GetObjOffsetRot();
PreRotateMatrix(tm, quat);
ScaleValue scaleValue = node->GetObjOffsetScale();
ApplyScaling(tm, scaleValue);
The INode class has a set of methods that allow a developer to transform the node or geometry in the same way as the hierarchy Affect Pivot Only and Affect Object Only modes do. The entire node and object may be transformed, just the pivot, or just the geometry of the object. These methods are:
virtual void Move(TimeValue t, const Matrix3& tmAxis,
const Point3& val, BOOL localOrigin=FALSE,
BOOL affectKids=TRUE, int pivMode=PIV_NONE,
BOOL ignoreLocks=FALSE)=0;
This method may be called to move the node about the specified axis system. Either the pivot point, or the geometry of the object, or both the pivot and the object may be transformed. Optionally, any children of the node can be counter transformed so they don't move.
virtual void Rotate(TimeValue t, const Matrix3& tmAxis,
const AngAxis& val, BOOL localOrigin=FALSE,
BOOL affectKids=TRUE, int pivMode=PIV_NONE,
BOOL ignoreLocks=FALSE)=0;
This method may be called to rotate the node about the specified axis system. Either the pivot point, or the geometry of the object, or both the pivot and the object may be transformed. Optionally, any children of the node can be counter transformed so they don't rotate. There is also a Quat version of this method.
virtual void Scale(TimeValue t, const Matrix3& tmAxis,
const Point3& val, BOOL localOrigin=FALSE,
BOOL affectKids=TRUE, int pivMode=PIV_NONE,
BOOL ignoreLocks=FALSE)=0;
This method may be called to scale the node about the specified axis system. Either the pivot point, or the geometry of the object, or both the pivot and the object may be transformed. Optionally, any children of the node can be counter transformed so they don't scale.
The ObjectTM
The INode method GetObjectTM() returns the transformation matrix an object needs to be multiplied by to transform it into world space. This includes the parent transformation, the node transformation (NodeTM) and the Object Offset Transformation. Thus, the entire transformation used to transform the points of any object is:
ObjectTM = Offset Scale * Offset Rotation * Offset Position * Controller Scale * Controller Rotation * Controllers Position * Parent Transformation.
A developer can retrieve the entire transformation of the object-offset, node and parent using the INode method:
virtual Matrix3 GetObjectTM(TimeValue time, Interval* valid=NULL)=0;
This is the entire transformation of a node. When an object is actually translated into world space, this is the matrix used. This is the Object Offset Transformation, plus the NodeTM, plus the world space modifier effect.
This matrix could be used, for example, if you have a TriObject and wanted to get the world space coordinate of one of its vertices. You could do this by taking the vertex coordinate in object space and multiplying it by the matrix returned from this method.
The ObjectTM is not inherited. See the remarks on this method in the reference section documentation for more details.
A Sample Plug-In Using the Node and Object Offset Transformations
The following code is from the Reset Transform utility (\MAXSDK\SAMPLES\MODIFIERS\RESETTM.CPP). This utility is used to push object rotation and scale values onto the Modifier Stack and align object pivot points and bounding boxes with the world coordinate system. Reset Transform removes all rotation and scale values from selected objects and places those transforms in an XForm modifier. The code below demonstrates the use of the TMs discussed above.
As previously noted, there are two transforms that affect an object: the node transform which is the result of the transform controller plus inheritance, and the object offset transform which positions the geometry relative to the pivot point.
The Reset Transform utility takes the node transform (minus inheritance), and the offset transform, and combines them into a matrix which is given to an XForm modifier which is then placed at the top of the modifier stack.
The node and offset transforms can then be set back to the identity (actually, the position component of the node TM is left unchanged). Here is the code -- it operates on all the selected nodes.
for (int i=0; i<ip->GetSelNodeCount(); i++) {
INode *node = pi->GetSelNode(i);
Matrix3 ntm, ptm, rtm(1), piv(1), tm;
// Get Parent and Node TMs
ntm = node->GetNodeTM(ip->GetTime());
ptm = node->GetParentTM(ip->GetTime());
// Compute the relative TM
ntm = ntm * Inverse(ptm);
// The reset TM only inherits position
rtm.SetTrans(ntm.GetTrans());
// Set the node TM to the reset TM
tm = rtm*ptm;
node->SetNodeTM(ip->GetTime(), tm);
// Compute the pivot TM
piv.SetTrans(node->GetObjOffsetPos());
PreRotateMatrix(piv,node->GetObjOffsetRot());
ApplyScaling(piv,node->GetObjOffsetScale());
// Reset the offset to 0
node->SetObjOffsetPos(Point3(0,0,0));
node->SetObjOffsetRot(IdentQuat());
node->SetObjOffsetScale(ScaleValue(Point3(1,1,1)));
// Take the position out of the matrix since
// we don't reset position
ntm.NoTrans();
// Apply the offset to the TM
ntm = piv * ntm;
// Apply a derived object to the node's object
Object *obj = node->GetObjectRef();
IDerivedObject *dobj = CreateDerivedObject(obj);
// Create an XForm mod
SimpleMod *mod = (SimpleMod*)ip->CreateInstance(
OSM_CLASS_ID,
Class_ID(CLUSTOSM_CLASS_ID,0));
// Apply the transformation to the mod.
SetXFormPacket pckt(ntm);
mod->tmControl->SetValue(ip->GetTime(),&pckt);
// Add the modifier to the derived object.
ModContext* mc = new ModContext(new Matrix3(1), NULL, NULL);
dobj->AddModifier(mod, mc);
// Replace the node's object
node->SetObjectRef(dobj);
}
The 'Local' Transformation Matrix
To retrieve what is considered the local transformation (the transformation of a node relative to its parent) you must perform some matrix arithmetic on the nodes world space transformation (NodeTM). This is because there is not a local transformation matrix of a node stored by MAX.
What is often considered the local transformation is the transformation matrix of the node relative to its parent. However, the transformation of a node relative to its parent may be some function of the node's parent's transform. For example, a transform controller takes the parent's TM and modifies it. When a node evaluates itself it takes the parent's TM and passes it as an argument when it calls GetValue() on its transform controller. The task of a transform controller in its implementation of GetValue() is to modify this matrix. It applies its transformation to the parent transformation passed in.
In some cases this modification may be just a simple pre-multiplication by what is considered the local transformation matrix. But it may be some other more-complicated process. For example, if rotation or scale inheritance is turned off, the transform controller takes the parent's matrix and perhaps removes rotation from it, or removes scaling from it, and then applies itself. As another example, it may use the parent's position as a function to derive rotation -- as in a Look At controller. Thus, what is considered the local transformation is a function of the transform controller and is not stored by MAX.
What 3ds max does store is the node's world space transformation matrix (NodeTM). This matrix includes the parent's transformation. To understand how the local transformation can be extracted from the world space transformation consider the following:
Any transformation is made up by starting with the node, and then multiply it by its parent, and then by its parent, and so on, all the way through the ancestors. This is shown below:
NodeWorldTM = NodeLocalTM * ParentLocalTM * ParentLocalTM * ParentLocalTM, etc.
The parents world transformation is equal to the product of all its ancestors, so:
ParentWorldTM = ParentLocalTM * ParentsLocalTM * ParentsLocalTM, etc.
The nodes world transform can then be expressed as:
NodeWorldTM = NodesLocalTM * ParentWorldTM.
If we multiply both sides of this equation by the Inverse of the ParentsWorldTM and simplify we get:
NodeLocalTM = NodeWorldTM * Inverse(ParentWorldTM)
So, to retrieve what is considered the local transformation you must get the parent's transform and multiply the node's transform by the inverse of the parent. The way this is computed using the methods of 3ds max is shown in the following code fragment. This example assumes node is the INode* that we want the local TM from, and the result will get stored in localTM:
...
INode *parent;
Matrix3 parentTM, nodeTM, localTM;
nodeTM = node->GetNodeTM(0);
parent = node->GetParentNode();
parentTM = parent->GetNodeTM(0);
localTM = nodeTM*Inverse(parentTM);
...
Node Linking and Grouping
This section discusses the linking and grouping of nodes, and the effect this has on the transformations of the nodes.
In MAX, nodes in the scene may be linked together to form a hierarchy. A node that is linked to another node is referred to as a child node. A node that has children is referred to as a parent node. A node may have several children, but only a single parent. See Parent Child Hierarchy for more details.
Nodes may also be grouped. When nodes are grouped together, 3ds max effectively creates a new dummy node, and links the group nodes to the parent dummy node. 3ds max maintains other information such as if the group is open or closed, etc., but this just effects the user interface. Essentially grouping is just like linking all the nodes to a dummy.
The following discussion applies to both linking and grouping (since grouping is just a special case of linking).
When a parent-child link is created, the user will not want the nodes in the scene to move. To ensure that the nodes won't move, the transform controllers of the nodes need to be modified. This is because the reference coordinate system has changed. The TM controller is working in a coordinate system which is its parent's coordinates system. When a node is unlinked, its reference coordinate system is the world coordinate system. When a node is linked, its reference coordinates system becomes its parents. So, when nodes are linked the parents change, and 3ds max must adjust the transformation matrices to counteract the change in reference coordinates system. All the keys created by a keyframe transform controller must be updated to reflect the change. When a developer creates the link, the nodes and keys must be adjusted in a similar fashion. An option to the INode method AttachChild() that links a node as a child allows this to be done automatically.
virtual void AttachChild(INode* node, int keepPos=1)=0;
Makes the specified node a child of this node. If keepPos is nonzero, the child is adjusted as needed to keep it from moving. keepPos counters the change in the reference coordinate system.
Access to Node Controller Data
In addition to accessing the node and object offset transformations, developers may want to access the keyframes or data of a node's transform controller. See the Advanced Topics section Keyframe and Procedural Controller Data Access for information and sample code.
Summary
The node maintains two transformations. The node's transformation matrix is what is controlled by the transform controller that places the node in the scene. The object-offset transformation represents a separate position, rotation and scale orientation of the geometry of the object independent of the node. The primary methods available to work with the node and these matrices are part of the INode class.