Inverse Kinematics
See Also: Class IKChainActions, Class IKSolver, Class IIKChainControl, Class IIKControl, Class ZeroPlaneMap, Class RootLink, Class Link, Class LinkChain, Class Animatable
Introduction to Inverse Kinematics
The Inverse Kinematics (IK for short) API’s are divided over three files: IIKSys.h, IKSolver.h, and IKHierarchy.h. IIKSys.h contains the interface definition of the two types of controllers, IIKControl and IIKChainControl. They are both TM controllers. The former is used in joints (nodes on which IK will be applied), and the latter is used in the IK goals (nodes that serve as IK goals).
The IKControl and IKChainControl are knit together as the IK system. When an IK solution is called for, the IK system will invoke the IK solver, which is a mathematical algorithm that, given the goal position/orientation, and incidental parameters, produces a solution in terms of joint angles (rotation values of respective nodes). While the controllers involved in IK are not expandable via plug-in, the IK solver is actually plug-able. The IK system will recognize a plug-in solver by putting it in the solver list wherever it appears. The API of the IK solver, defined in IKSolver.h and IKHierarchy.h, contains the data structure of transformation hierarchy used in IKSolver.h.
An interface pointer to the IKControl class can be obtained by using Animatable::GetInterface(I_IKCONTROL).
An example on using the IK API can be found in the 3ds max SDK, \MAXSDK\SAMPLES\IKSOLVERS\IKLIMB.
Concepts
Degrees of Freedom
A node transform contains six degrees of freedom. Three are translational (sliding) and three are rotational. In the API, they have enum names, IKSys::TransX to IKSys::TransZ and IKSys::RotX to IKSys::RotZ. When the context implies rotational or translational, IKSys::DofX to IKSys::DofZ are used.
A degree of freedom can be active or inactive. An inactive degree of freedom is not to be used by IK. In other words, only active degrees of freedom are degrees of freedom as far as IK is concerned. The methods DofActive(), ActiveTrans(), ActiveRot(), and ActiveDofs() are provided to determine which degrees of freedom are active and inactive. The first query answers whether a specific (translational / rotational) axis is active. The following three return a set of DOFs, which is defined in the same file. The set returned by ActiveTrans() and ActiveRot() is to be tested by IKSys::DofX to IKSys::DofZ, and the set returned by ActiveDofs() is to be tested by IKSys::TransX to IKSys::RotZ.
Relationship to IK Chains
A node transformation can be conceptually viewed as consisting of two joints, sliding joint (translational degrees of freedom) and rotational joint. The enum type, IKSys::JointType, contains two enums: IKSys::SlidingJoint and IKSys::RotationalJoint. An IK chain starts at the rotational joint of the Start Joint and ends at the sliding joint of the End Joint. Two IK chains covering the same joint, are considered overlapping IK chains.
Let’s call a node an IK chain node if its TM controller supports interface IIKChainControl, i.e., node->GetTMController()->GetInterface(I_IKChainControl) != NULL. The following query returns a list of IK chain nodes; InodeTab IKChains(JointType) const
The parameter that decides whether an individual degree of freedom is (IK) active is not animatable. There is an animatable variable of IK chain that decides whether the goal defined in the IK chain actually affects the joints it covers at a specific time, this can be queried using IKBound().
Joint Limits
A degree of freedom can be bound to a limited range. The lower bound and upper bound can be set separately. At each end, there are a boolean parameter, Limited, and a float number that sets the limit. The limit is effective only if the boolean parameter, Limited, is true. Following queries concern the joint limits. When a Point2 is returned, it is ordered as the lower limit followed by the upper limit. When a Point3 is returned, the order is X, Y, and Z. For example, TransLowerLimits() returns lower limits of TransX, TransY, and TransZ, respectively.
Preferred Angles
Some solver may start off the solution process with joint angles being set to special values, these are called preferred angles. PrefPosition() and PrefRotation() return preferred angles of the translation and rotation joints, respectively. The APIs allows them to be animatable. However, in 3ds max 4, they are constant with regard to animation time. The methods SetPrefTrans(), SetPrefRot(), and SetPrefTR() set their values at a specific time.
Joint Angles
TransValue() and RotValue() will query joint angles of sliding and rotational joints, respectively, at a specific time. If the user supplies non-null pointer to a validity interval, the validity interval will be updated.
Their values can be assigned using AssignTrans() and AssignRot(). The methods AssignActiveTrans() and AssignActiveRot() will skip those degrees of freedom that are not active. These same two methods allow active DOFs to be given as the first argument of type DofSet, while having the new values supplied as float array whose size should be the same as the DofSet [DofSet::Count()]. The order in the array is: DofZ if the DofSet includes DofZ, DofY if the DofSet includes it, and followed by DofX if it is included in the DofSet. IK controllers are designed to be controlled by the IK goals (through the internal IK system). Therefore, their values are not to be set as independent variables are. These assignment methods just assign new values to the respective variables and that’s all. They do not adjust the validity interval, this is done through the methods SetTransValid(), SetRotValid() and SetTRValid().The last method sets the validity intervals for translation and rotation values to the same interval.
FK Sub-controller
The Forward Kinematics sub-controller and the pointer to the node that holds this controller as its TM controller can be obtained using Control* FKSubController() and INode* GetNode(). Note that the IK controller is not designed to be instanced. It is expected to have a unique node.
Interface Class IIKChainControl
Reference Indices
The following enum constants are indices to the reference targets made by the IIKChainControl. For example, GetReference(IIKChainControl::kPBlockRef) will return a pointer to IParamBlock2. These reference indices are:
kPBlockRef
The IparamBlock2 parameter block.
kGoalTMRef
A Matrix3 controller representing the sub-controller that defines the transformation matrix of this node
kEndJointRefINode
The INode reference for the End Joint.
kEnableRef
Bool (float) controller. This switch indicates when the IK chain is enabled or effective.
kStartJointRef
The INode reference for the Start Joint.
kLastRef
Place holder.
Parameter Block Indices
There is just one parameter block. Its index to Animatable::GetParamBlock() is given by enum constant kParamBlock.
Parameter Indices
The following enum constants represent the indices of the parameters in the parameter block:
kStartJoint INode, referenced by kStartJointRef
kEndJoint INode, referenced by kEndJointRef
kSolverName String, the solver name.
kAutoEnable BOOL, auto-enable.
kSwivel Angle, swivel angle.
kPosThresh Float, pos threshold.
kRotThresh Float, rot threshold.
kIteration Integer, iterations.
kEEDisplay BOOL, EE display.
kEESize Float, EE size.
kGoalDisplay BOOL, display goals.
kGoalSize Float, goal size.
kVHDisplay BOOL, VH display.
kVHSize Float, VH size.
kVHLength Float, VH length.
kSolverDisplay BOOL, display solvers.
kAutoSnap BOOL, auto snap.
kVHUseTarget BOOL, VH use target.
kVHTarget INode, VH target.
kLastParam Place holder.
Most parameters have separate querying methods. For example;
GetParamBlock(IIKChainControl::kParamBlock)>GetINode(IIKChainControl::kStartJoint)
will return turns the same pointer as does StartJoint(). The kStartJoing and kEndJoint parameters that define the IK Chain Delimiters can be obtained by using the INode* StartJoint() and INode* EndJoint() methods.
Swivel Angles
Most of the time, joints are drawn on a plane (planar IK chains). A desirable quality of an IK solver is not to disturb this plane. Those solvers would use only the Start Joint to rotate the plane. When the End Joint is adhered to the goal, the rotation about the axis from the Start Joint to the End Joint is called Swivel. Swivel angle is the amount of this rotation.
In order for the swivel angle to make sense, however, we need a reference point. When a chain is put at the preferred pose, meaning that all the joint angles are set to preferred angles, the plane expanded by the pivots of all the joints on the chain is called Initial Plane, and the axis from the Start Joint to End Joint is called Initial End Effector Axis (EEAxis). Since Preferred Angles are animation variable, so is the Initial Plane and EEAxis. The InitPlane() and InitEEAxis() methods return the normal to the Initial Plane and the unit vector of the Initial EEAxis. These are represented in the parent space of the Start Joint. To obtain the normal in object space, use the method ChainNormal(). The relationship between ChainNormal() and InitPlane() is (in pseudo code):
InitPlane() = ChainNormal() * StartJoint()->PrefRotation().
When the joints do not lie on a plane, the closest plane, in certain sense, can be used. Let’s call this plane the solver plane.
The class ZeroPlaneMap is defined in IKHierarchy.h. This provides the functionality that, given a unit axis, which is to be substituted for by EE Axis, produces a unit vector, which will be interpreted as the normal to a plane. This plane will be taken as the zero plane to which the swivel angle is relative: the plane corresponds to swivel angle being zero. The amount of rotation that will bring the Zero Plane to the current solver plane is the Swivel Angle, which corresponds to the parameter of index kSwivel and can be obtained using the SwivelAngle() method. The following relationship holds (in pseudo code):
ChainNormal() * StartJoint()->Rotation() = ZeroPlaneMap(EEAxis) * <Rotation about EEAxis by SwivelAngle()>
A plugin solver may have its own ZeroPlaneMap. In case it does not have a preference, it can use the default one provided by the IK system as IKSys::ZeroPlaneMap* DefaultZeroPlaneMap().
The solver plane can be controlled by an extra node and this is often desired by the user. In this case, they want the solver plane to align with the plane defined by the Start Joint, the End Joint, and the extra node, called the swivel angle target. Since the target and the swivel angle are meant to control the same thing, only one of them can be used. The parameter of index kVHUseTarget determines which will be used. It is not animatable. If it is true, the swivel angle target will be used. The target node can be obtained from the parameter of index kVHTarget. When the target node is NULL, the swivel angle will be used.
Solver Properties
The method IKSolver* Solver() returns the IK solver whose responsibility it is to solve the IK problem. Its class name is stored as the parameter of index kSolverName. As mentioned, there is an animatable variable that determines, at a specific time, whether IK is enabled. Whether the solver is enabled or disabled can be checked using the SolverEnabled() method.
This is just an interface to the sub-controller with reference index kEnableRef. Since its value is used to determine whether joints in an IK chain depend on the IK goal when reference message is received, developers should not replace it, using the 3ds max Reference APIs, with an arbitrary controller whose dependency structure is general. This would create a time variable dependency structure and is not desirable. The built in controller is a key-frame controller.
Sometimes, we still want to invoke IK by moving the goal even if SolverEnabled() is false. In such cases the purpose of IK is to set keys on the FK sub-controllers of the joints. The feature is called "Use IK to FK", which is only meaningful during an interactive session. For this purpose CanAutoEnabled() corresponds to the parameter of index kAutoEnable and indicates whether the feature of "Use IK to FK" is desired. The AutoEnableSet() method provides the transient state of the IK chain: whether IK is currently enabled due to this feature. It is only set, if it is ever set, during the next update cycle, with regard to when the goal is moved, or its TM is set to new value.
Valid IK Chains
A valid IK chain is one that has a good Solver, Start and End joints, where the Start Joint is an ancestor of the End Joint in the scene hierarchy. The Valid() and INode* GetNode() methods are available for this. The latter returns the node that holds it as its TM controller. The IK Chain Controller is not designed to be instanced. It is expected to have a unique node.
Class IKSolver and Solver plugins
Being a plugin itself, the IK system allows the solver to be a separate plugin. This class defines the base class that plugin solver should derive from. The IK solver is a pure mathematical function: it does not hold state and just solves a given, self-contained, mathematical problem. In other words, the plugin solver does not have influence on when IK is invoked and what an IK problem is (what is the goal and what are the joints, etc.), but contributes to IK by answering how to solve. Structurally, it is independent of the SDK and, hence, can be built independently, except for some theoretically independent math library.
Class Identity
Plugin solvers are recognized by the IK system by the Super Class ID. A unique enum, IK_SOLVER_CLASS_ID, defined in animtbl.h, is given to it. The SClass_ID SuperClassID() method should not to be overridden. Since it is a pure mathematical function that does not hold state, an individual plugin solver is identified by its class name which can be obtained using the GetClassName() method. When class name clashes, a suffix may be appended. The class name will appear in the solver list from that users can pick for or assign to IK chains.
Solver Traits
Following methods are meant to be overridden by the plugin solver queried by the IK system.
IsInteractive(). IK can be used as a controller or as an interactive manipulation tool. In the former, the relationship between the goal and the joints are permanent: joints are completely controlled by the goal. In the latter, the relationship is transient, existing only during interactive manipulation. In the end, IK solutions are registered at each joint, mostly likely as key-frames, and it no longer matters how joints got their joint angles. Only non-interactive, or controller, IK solvers ares used.
IsHistoryDependent(). At a specific animation time, the history dependent solver will reach solutions not only based the state of goal at the time, but also its previous states (hence they are history dependent). On the contrary, the history independent solver does its job based on the state of the goal just at the time. The procedural implication is that, when the goal is changed at time t, the IK system would have to invalidate joints, at time t for the history independent solver, and at all times that are greater or equal to t for the history dependent solver. Only history dependent solvers are used by the IK system.
The methods UseSlidingJoint() and UseSwivelAngle() are used to tell whether the plugin solver intends to use the sliding joint (translational degrees of freedom) or the swivel angle parameter of the IK chain.
When two IK chains overlap, i.e., if there’s a joint belonging to both IK chains, some solvers are able to negotiate between the possibly contending goals and some are not. The method DoesOneChainOnly() is used to determine this. For those that can only solve one chain at a time, the IK system will pass to the solvers one chain at a time in a definitive order. Only solvers that "do one chain only" are used.
The method DoesRootJointLimits() and DoesJointLimitsButRoot() deal with the concern of joint limits. If the solver supports joint limits, the IK system will trust it. Otherwise, the IK system will, after calling the solver, clamp the results according the joint limit constraints. The root joint in DoesRootJointLimits() refers to the Start Joint. It is treated differently from the rest of joints.
The method SolveEERotation() tells whether the rotation part of the goal node will be used. If it returns false, only the position of the goal node is taken as the IK goal and rotation threshold will be irrelevant.
Solvers can reach solutions with closed formula, analytically, or going through iterations. For an analytic solver, thresholds and maximum iteration numbers are not relevant. Checking whether or not a solver is analytical can be done using the IsAnalytic() method.
Solution Parameters
The methods GetPosThreshold(), GetRotThreshold(), GetMaxIteration(), SetPosThreshold(), SetRotThreshold(), SetMaxIteration() are used to get and set the Position Threshold, Rotation Threshold, and the Maximum number allowed for iterations and are not relevant for all solvers. For an analytic solver, for example, these are not used at all. The IK system, however, may set values to them.
As mentioned earlier, plugin solvers may have their own Zero Plane Map. If so, they must override the IKSys::ZeroPlaneMap* GetZeroPlaneMap() method. The IK system will need it to perform IK snapping: setting the swivel angle based on the current pose so that the pose is consistent with the swivel angle.
Solve
Solve() is the method which the IK system will call when it’s time to update the joints according to the IK goal and other parameters. The IK system will compile an IK problem represented in the 3ds max scene data structure, nodes, controllers, etc., into a pure mathematical representation, IKSys::LinkChain. This data structure only represents one IK chain. Overlapping IK chains are not dealt with. The ReturnCondition, returned by the Solve() method is a bit-set. Its definition is copied from IKHierarchy.h:
typedef unsigned ReturnCondition;
enum ConditionBit {
bLimitReached = 0x00000001,
bLimitClamped = 0x00000002,
bMaxIterationReached = 0x00000004,
// The first eight bits are reserved for mild condition.
// They are still considered successful.
bGoalTooCloseToEE = 0x00000100,
bInvalidArgument = 0x00000200,
bInvalidInitialValue = 0x00000400
};
The data structure passed to the solver is transient, meaning that it will be discarded once the solution is copied back to the joints. If the return condition indicates failure, (return_condition > 0xff) then the result will not be copied back to the joint nodes in the 3ds max scene database.
A LinkChain is a hierarchy of transformations. At the top is the RootLink and it is followed by a number of Link’s . Each link, be it a RootLink or Link, evaluates to a matrix, called the link matrix (LinkMatrix). This matrix consists of two parts, one is a variable part (degrees of freedom) and the other is a relatively constant part, called rigid extend (RigidExtend). It can be graphically represented as:
The link variable is a rotation, rotXYZ. The link matrix is defined as
RigidExtend * Rx (rotXYZ.x) * Ry (rotXYZ.y) * Rz (rotXYZ.z)
where Rx (.) stands for rotation about the x-axis, etc.
The variable part of a Link is of one degree of freedom. It can be translational or rotational. A typical 3 degrees of freedom (xyz) rotational joint can be decomposed into 3 Links, with the first two being null links:
A LinkChain consists of a RootLink and a number of Link’s. The reason that the LinkChain can start with rotation, the variable part of the RootLink, is that the IK system will collapse all the node transformations from the root of the scene to the parent matrix of the Start Joint and the translation part of the Start Joint, called the parent matrix, and transform the node hierarchy of the IK chain into this space. The author of the plugin solver does not have to be concerned about parentMatrix. They work on the problem as it is rooted at the origin of the world. The data structure of LinkChain should be sufficient for those solvers that DoesOneChainOnly() and UseSwivelAngle().
IK System
The IK system services two interfaces, one through the CORE, and the other through the IK Chain Controller class.
Interface Class IKCmdOps
The interface ID is defined in IIKSys.h as IK_FP_INTERFACE_ID and is published through the CORE, i.e., to get a pointer to this interface, call GetCOREInterface(IK_FP_INTERFACE_ID).
To create a new IK chain, use the method CreateIKChain(). Given two pointers, start and end, that are intended for the Start and End joints respectively, and the solver’s name, solver, this method will create a node that has an IKChainControl as the TM controller. This method will return false and fail if: 1) start joint is not a proper ancestor of end joint in the scene graph; or 2) there is a node along the path from the start joint to the end joint whose TM controller is not an IK Control and is not replaceable, or 3) no IK solver is found with the given name.
Interface Class IKChainActions
The interface ID is defined in IIKsys.h as IKCHAIN_FP_INTERFACE_ID and is published through the class IKChainControl, i.e., to get a pointer to this interface, call;
GET_IKCHAIN_CD->GetInterface(IKCHAIN_FP_INTERFACE_ID)
where GET_IKCHAIN_CD is a macro defined in the same file that produces a pointer to the class descriptor of class IKChainControl.
This interface also publishes a number of actions. An action acts on the currently selected IK chain node. The following method returns TRUE if there is exactly one valid IK chain node selected: IsSnapEnabled(). The following methods execute snapping on the currently selected IK chain node: SnapAction() , IKSnapAction(), and FKSnapAction(). The first one does IK snapping if its Enabled is false and FK snapping if its Enabled is true. The next two will do IK snapping and FK snapping, respectively.