Torque3D/Engine/source/ts/tsIKSolver.cpp
marauder2k7 9866908e99 Required changes for Inverse Kinematics
Added a * operator
compute from to -> adds safeties around shortestArc
conjugate -> reverses the xyz of the quaternion

IK Solver commit

Added: IKChain struct to tsshape
commands to tsshapeconstruct to create and setup ikchains
ik solvers -> ccd and fabrik, these are in their own file tsIKSolver

TODO: there needs to be some tooling added to the shape editor for this
2025-12-04 08:33:21 +00:00

292 lines
8.9 KiB
C++

#include "ts/tsShapeInstance.h"
/// All ik solving happens to the global transform then is converted to localspace
/// mNodeTransforms is the global transform vector
/// smNodeLocalTransform is the local transform vector
/// we do our solving after animateNodes so animations can change bone lengths
/// but our fabrik solver does not!
/// The book C++ Game Animation Programming 2nd Edition was used as reference.
Vector<F32> TSShapeInstance::smFabrikBoneLengths(__FILE__, __LINE__);
Vector<Point3F> TSShapeInstance::smFabrikPositions(__FILE__, __LINE__);
Vector<S32> TSShapeInstance::smIKChainNodes(__FILE__, __LINE__);
//-------------------------------------------------------------------
// FABRIK FUNCTIONS
//-------------------------------------------------------------------
void TSShapeInstance::updateChildWorldTransforms(S32 node)
{
for (S32 child = mShape->nodes[node].firstChild; child != -1; child = mShape->nodes[child].nextSibling)
{
// world[child] = world[parent] * local[child]
mNodeTransforms[child].mul(mNodeTransforms[node], smNodeLocalTransforms[child]);
// recurse
updateChildWorldTransforms(child);
}
}
bool TSShapeInstance::calculateFabrikBoneLengths()
{
const S32 count = smIKChainNodes.size();
// not enough for fabrik.
if (count < 2)
return false;
smFabrikBoneLengths.clear();
smFabrikBoneLengths.setSize(count - 1);
for (S32 i = 0; i < count - 1; i++)
{
const Point3F p0 = mNodeTransforms[smIKChainNodes[i]].getPosition();
const Point3F p1 = mNodeTransforms[smIKChainNodes[i + 1]].getPosition();
smFabrikBoneLengths[i] = (p1 - p0).magnitudeSafe();
}
return true;
}
void TSShapeInstance::applyFabrik(TSShape::IKChain& chain)
{
const U32 count = smFabrikPositions.size();
if (count < 2)
return;
for (S32 i = count - 1; i > 0; i--)
{
S32 nodeIdx = smIKChainNodes[i];
S32 nextNodeIdx = smIKChainNodes[i-1];
// Current joint global rotation & position
QuatF curWorldRot;
curWorldRot.set(mNodeTransforms[nodeIdx]);
const Point3F curPos = mNodeTransforms[nodeIdx].getPosition();
const Point3F nextPos = mNodeTransforms[nextNodeIdx].getPosition();
// Direction vectors
VectorF toNext = nextPos - curPos;
toNext.normalizeSafe();
VectorF toDesired = smFabrikPositions[i - 1] - smFabrikPositions[i];
toDesired.normalizeSafe();
// Rotation direction -> target direction
QuatF rotToTarget;
rotToTarget.computeRotationFromTo(toDesired, toNext);
QuatF parentWorldRot;
S32 parentIdx = mShape->nodes[nodeIdx].parentIndex;
if (parentIdx >= 0)
parentWorldRot.set(mNodeTransforms[parentIdx]);
else
parentWorldRot.identity();
QuatF curRotLocal = smNodeCurrentRotations[nodeIdx];
QuatF newRotLocal = parentWorldRot * rotToTarget * parentWorldRot.conjugate();
QuatF blendedRot;
TSTransform::interpolate(curRotLocal, curRotLocal * newRotLocal, chain.weight, &blendedRot);
// Update local rotation & matrix
smNodeCurrentRotations[nodeIdx] = blendedRot;
TSTransform::setMatrix(blendedRot, smNodeCurrentTranslations[nodeIdx], &smNodeLocalTransforms[nodeIdx]);
// Update world transform for this node
if (parentIdx >= 0)
mNodeTransforms[nodeIdx].mul(mNodeTransforms[parentIdx], smNodeLocalTransforms[nodeIdx]);
else
mNodeTransforms[nodeIdx] = smNodeLocalTransforms[nodeIdx];
// Propagate world transform to children
updateChildWorldTransforms(nodeIdx);
}
}
void TSShapeInstance::solveFabrikForward(const Point3F& target)
{
const U32 count = smFabrikPositions.size();
if (count < 2)
return;
smFabrikPositions[0] = target;
for (S32 i = 1; i < count; i++)
{
VectorF direction = (smFabrikPositions[i] - smFabrikPositions[i - 1]);
direction.normalizeSafe();
Point3F offset = direction * smFabrikBoneLengths[i - 1];
smFabrikPositions[i] = smFabrikPositions[i - 1] + offset;
}
}
void TSShapeInstance::solveFabrikBackward(const Point3F& root)
{
const U32 count = smFabrikPositions.size();
if (count < 2)
return;
smFabrikPositions[count - 1] = root;
for (S32 i = count-2; i > 0; i--)
{
VectorF direction = (smFabrikPositions[i] - smFabrikPositions[i + 1]);
direction.normalizeSafe();
Point3F offset = direction * smFabrikBoneLengths[i];
smFabrikPositions[i] = smFabrikPositions[i + 1] + offset;
}
}
//-------------------------------------------------------------------
// FABRIK FUNCTIONS END
//-------------------------------------------------------------------
//-------------------------------------------------------------------
// MAIN SOLVER ENTRY FUNCTIONS
//-------------------------------------------------------------------
bool TSShapeInstance::solveCCD(TSShape::IKChain& chain)
{
PROFILE_SCOPE(TSShapeInstance_solveCCD);
if (!chain.enabled)
return false;
// safety.
smIKChainNodes.clear();
smIKChainNodes = chain.nodes;
const U32 count = smIKChainNodes.size();
if (count == 0) return false;
const S32 endNode = smIKChainNodes.first();
const F32 threshold = chain.threshold;
const U32 maxIterations = chain.maxIterations;
const Point3F targetPos = mNodeTransforms[chain.targetIndex].getPosition();
for (U32 iter = 0; iter < maxIterations; iter++)
{
Point3F endPos = mNodeTransforms[endNode].getPosition();
// Early exit if the end effector is close enough
if ((targetPos - endPos).magnitudeSafe() < threshold)
return true;
// Iterate joints from the one before the end effector -> root
// (exclude the end effector itself)
for (S32 i = 1; i < count; i++)
{
S32 nodeIdx = smIKChainNodes[i];
// Current joint global rotation & position
QuatF curWorldRot;
curWorldRot.set(mNodeTransforms[nodeIdx]);
const Point3F curPos = mNodeTransforms[nodeIdx].getPosition();
// Direction vectors
VectorF toEnd = endPos - curPos;
VectorF toTarget = targetPos - curPos;
toEnd.normalizeSafe();
toTarget.normalizeSafe();
// Rotation direction -> target direction
QuatF rotToTarget;
rotToTarget.computeRotationFromTo(toTarget, toEnd);
QuatF parentWorldRot;
S32 parentIdx = mShape->nodes[nodeIdx].parentIndex;
if (parentIdx >= 0)
parentWorldRot.set(mNodeTransforms[parentIdx]);
else
parentWorldRot.identity();
QuatF curRotLocal = smNodeCurrentRotations[nodeIdx];
QuatF newRotLocal = parentWorldRot * rotToTarget * parentWorldRot.conjugate();
QuatF blendedRot;
TSTransform::interpolate(curRotLocal, curRotLocal * newRotLocal, chain.weight, &blendedRot);
// Update local rotation & matrix
smNodeCurrentRotations[nodeIdx] = blendedRot;
TSTransform::setMatrix(blendedRot, smNodeCurrentTranslations[nodeIdx], &smNodeLocalTransforms[nodeIdx]);
// Update world transform for this node
if (parentIdx >= 0)
mNodeTransforms[nodeIdx].mul(mNodeTransforms[parentIdx], smNodeLocalTransforms[nodeIdx]);
else
mNodeTransforms[nodeIdx] = smNodeLocalTransforms[nodeIdx];
// Propagate world transform to children
updateChildWorldTransforms(nodeIdx);
// Check updated end effector
endPos = mNodeTransforms[endNode].getPosition();
if ((targetPos - endPos).magnitudeSafe() < threshold)
return true;
}
}
return false;
}
bool TSShapeInstance::solveFrabrik(TSShape::IKChain& chain)
{
PROFILE_SCOPE(TSShapeInstance_solveFabrik);
if (!chain.enabled)
return false;
// safety.
smIKChainNodes.clear();
smIKChainNodes = chain.nodes;
const U32 count = smIKChainNodes.size();
// not enough for fabrik.
if (count < 2)
return false;
// Resize working position buffer
smFabrikPositions.setSize(count);
// Fill initial global positions
for (U32 i = 0; i < count; i++)
smFabrikPositions[i] = mNodeTransforms[smIKChainNodes[i]].getPosition();
if (!calculateFabrikBoneLengths())
return false;
const S32 endNode = smIKChainNodes.first();
const Point3F rootPos = smFabrikPositions[count-1];
const Point3F targetPos = mNodeTransforms[chain.targetIndex].getPosition();
const F32 threshold = chain.threshold;
const U32 maxIterations = chain.maxIterations;
for (U32 i = 0; i < maxIterations; i++)
{
Point3F endPos = smFabrikPositions[0];
// Early exit if the end effector is close enough
if ((targetPos - endPos).magnitudeSafe() < threshold)
{
applyFabrik(chain);
return true;
}
solveFabrikForward(targetPos);
solveFabrikBackward(rootPos);
}
applyFabrik(chain);
Point3F endPos = mNodeTransforms[endNode].getPosition();
if ((targetPos - endPos).magnitudeSafe() < threshold)
return true;
return false;
}