Skip to content
←Back to Blog
Inverse Kinematics and Foot Locking: From Two-Bone IK to Offline Slide Removal
Inverse KinematicsFoot LockingAnimation

Inverse Kinematics and Foot Locking: From Two-Bone IK to Offline Slide Removal

A practical recipe for foot sliding: solve the leg with soft-clamped two-bone IK and toe orientation, lock the toe target during contacts with inertialization, annotate contacts automatically, then use a position-based dynamics-style offline pass to distribute corrections. The key insight is velocity preservation, not sticking to the ground, and the toe—not the heel—is what should be locked.

Daniel HoldenJuly 30, 202613 min read713 views
ZH

Foot sliding affects almost every animation system. Proper foot locking and inverse kinematics are hard to get right, but when they work, the improvement in animation quality is immediate. In this article I'm collecting the recipes I have used over the years into one place.

Two animation programmers will often give you two different answers to the same foot-sliding problem. This is more art than science. The goal here is not a canonical solution, but a practical foundation: solve the leg cleanly, lock the toe during contact, label contacts automatically, and use an offline constraint solver when you have the whole clip available.

Solving a Leg Chain

The basic setup is a posed leg chain. We want to change only the local rotations so that the original pose is preserved as much as possible, while the toe arrives at a desired location.

Leg chain IK setup
The leg chain, its current pose, and the target for the toe.

The pipeline is:

  1. Compute the target heel location from the target toe location.
  2. Solve the two-joint IK problem to place the heel at its target.
  3. Rotate the heel joint to orient the toe toward its target.
  4. Optionally rotate the toe end to avoid ground collisions.

Find the Heel Target

Given the current pose, the offset from toe to heel is known. Add that offset to the desired toe target:

*targetHeel = Vector3Add(*targetToe, Vector3Subtract(
    globalTransforms[heelBoneIndex].translation,
    globalTransforms[toeBoneIndex].translation));
Heel target computed from the toe target
The heel target is the toe target plus the current toe-to-heel offset.

Solve for the Heel Target

Next, solve the hip and knee rotations so the heel reaches its target. I use a modified version of my earlier two-bone IK code. The important additions are a soft extension clamp and a stable rotation axis derived from the knee side vector.

static inline Quaternion QuaternionExp(Vector3 v)
{
    float halfangle = sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);

    if (halfangle < 1e-4f)
    {
        return QuaternionNormalize((Quaternion){ v.x, v.y, v.z, 1.0f });
    }
    else
    {
        float c = cosf(halfangle);
        float s = sinf(halfangle) / halfangle;
        return (Quaternion){ s * v.x, s * v.y, s * v.z, c };
    }
}

static inline Quaternion QuaternionFromScaledAngleAxis(Vector3 v)
{
    return QuaternionExp(Vector3Scale(v, 0.5f));
}

static inline void TwoBoneInverseKinematics(
    Quaternion *localHip,
    Quaternion *localKnee,
    Transform globalPelvis,
    Transform globalHip,
    Transform globalKnee,
    Transform globalHeel,
    Vector3 targetHeel,
    Vector3 sideVector,
    float maxExtension,
    float softening)
{
    Vector3 targetClamp = targetHeel;
    float targetLength = Vector3Distance(targetHeel, globalHip.translation);

    if (targetLength > maxExtension - softening)
    {
        float saturation = 1.0f - expf(
            -Max(targetLength - maxExtension + softening, 0.0f) / softening);

        targetClamp = Vector3Add(
            globalHip.translation,
            Vector3Scale(Vector3Subtract(targetHeel, globalHip.translation),
                (maxExtension - softening + softening * saturation) / targetLength));
    }

    Vector3 axisDwn = Vector3Normalize(
        Vector3Subtract(globalHeel.translation, globalHip.translation));
    Vector3 axisFwd = Vector3Normalize(Vector3CrossProduct(axisDwn, sideVector));
    Vector3 axisRot = Vector3Normalize(Vector3CrossProduct(axisDwn, axisFwd));

    Vector3 a = globalHip.translation;
    Vector3 b = globalKnee.translation;
    Vector3 c = globalHeel.translation;
    Vector3 t = targetClamp;

    float lab = Vector3Distance(b, a);
    float lcb = Vector3Distance(b, c);
    float lat = Vector3Distance(t, a);
    float lca = Vector3Distance(a, c);

    float acab0 = acosf(Clamp(Vector3DotProduct(
        Vector3Scale(Vector3Subtract(c, a), 1.0f / lca),
        Vector3Scale(Vector3Subtract(b, a), 1.0f / lab)), -1.0f, +1.0f));

    float babc0 = acosf(Clamp(Vector3DotProduct(
        Vector3Scale(Vector3Subtract(a, b), 1.0f / lab),
        Vector3Scale(Vector3Subtract(c, b), 1.0f / lcb)), -1.0f, +1.0f));

    float acab1 = acosf(Clamp(
        (lab * lab + lat * lat - lcb * lcb) / (2.0 * lab * lat),
        -1.0f, +1.0f));

    float babc1 = acosf(Clamp(
        (lab * lab + lcb * lcb - lat * lat) / (2.0 * lab * lcb),
        -1.0f, +1.0f));

    Quaternion r0 = QuaternionFromScaledAngleAxis(
        Vector3Scale(axisRot, acab1 - acab0));
    Quaternion r1 = QuaternionFromScaledAngleAxis(
        Vector3Scale(axisRot, babc1 - babc0));
    Quaternion r2 = QuaternionNormalize(QuaternionBetween(
        Vector3Subtract(globalHeel.translation, globalHip.translation),
        Vector3Subtract(targetClamp, globalHip.translation)));

    *localHip = QuaternionMultiply(QuaternionMultiply(QuaternionMultiply(
        QuaternionInvert(globalPelvis.rotation), r2), r0),
        globalHip.rotation);

    *localKnee = QuaternionMultiply(QuaternionMultiply(
        QuaternionInvert(globalHip.rotation), r1),
        globalKnee.rotation);
}

The soft clamp matters. Instead of snapping to a maximum limb length, the target approaches maxExtension exponentially. This prevents hyper-extension without destroying the animation.

The side vector gives a stable axis around which to rotate the knee. That removes the need for a separate pole vector.

Two-bone IK placing the heel while preserving the source pose.

Solve for the Toe Target

Once the heel is solved, orient the heel joint so the toe points toward its target:

static inline Quaternion QuaternionBetween(Vector3 p, Vector3 q)
{
    Vector3 c = Vector3CrossProduct(p, q);
    Quaternion o = {
        c.x, c.y, c.z,
        sqrtf(Vector3DotProduct(p, p) * Vector3DotProduct(q, q))
            + Vector3DotProduct(p, q)
    };

    return QuaternionLength(o) < 1e-8f
        ? QuaternionFromAxisAngle((Vector3){ 1.0f, 0.0f, 0.0f }, PI)
        : QuaternionNormalize(o);
}

static inline Quaternion BoneOrientTowards(
    Transform boneParentTransform,
    Transform boneTransform,
    Transform boneChildTransform,
    Vector3 target)
{
    Quaternion desiredRotation = QuaternionMultiply(
        QuaternionNormalize(QuaternionBetween(
            Vector3Subtract(boneChildTransform.translation,
                boneTransform.translation),
            Vector3Subtract(target, boneTransform.translation))),
        boneTransform.rotation);

    return QuaternionMultiply(
        QuaternionInvert(boneParentTransform.rotation),
        desiredRotation);
}
ForwardKinematics(globalTransforms, localTransforms, model);
localTransforms[heelBoneIndex].rotation = BoneOrientTowards(
    globalTransforms[kneeBoneIndex],
    globalTransforms[heelBoneIndex],
    globalTransforms[toeBoneIndex],
    *targetToe);
The toe orientation is solved after the heel placement.

Handle Ground-Plane Collisions

Targets can also be constrained against the ground. First record heel and toe heights in the bind pose:

float heelMinHeight = globalTransforms[leftHeelBoneIndex].translation.y;
float toeMinHeight = globalTransforms[leftToeBoneIndex].translation.y;
float toeEndMinHeight = globalTransforms[leftToeEndBoneIndex].translation.y;
targetToe->y = Max(targetToe->y, toeMinHeight);
targetHeel->y = Max(targetHeel->y, heelMinHeight);

After clamping, re-orient the toe end so its tip does not penetrate the floor:

ForwardKinematics(globalTransforms, localTransforms, model);
*targetToeEnd = globalTransforms[toeEndBoneIndex].translation;

if (enableHeightClamp)
{
    targetToeEnd->y = Max(targetToeEnd->y, toeEndMinHeight);
}

localTransforms[toeBoneIndex].rotation = BoneOrientTowards(
    globalTransforms[heelBoneIndex],
    globalTransforms[toeBoneIndex],
    globalTransforms[toeEndBoneIndex],
    *targetToeEnd);
Ground clamping with the toe end corrected after the height clamp.

Complete Leg Solver

static inline void SolveLegChain(
    Model model,
    Transform *localTransforms,
    Transform *globalTransforms,
    Vector3 target,
    Vector3 *targetHeel,
    Vector3 *targetToe,
    Vector3 *targetToeEnd,
    int pelvisBoneIndex,
    int hipBoneIndex,
    int kneeBoneIndex,
    int heelBoneIndex,
    int toeBoneIndex,
    int toeEndBoneIndex,
    bool enableHeightClamp,
    bool enableHeelLookAt,
    bool enableToeLookAt,
    float heelMinHeight,
    float toeMinHeight,
    float toeEndMinHeight,
    float softening,
    Vector3 kneeSideVector)
{
    *targetToe = target;

    if (enableHeightClamp)
    {
        targetToe->y = Max(targetToe->y, toeMinHeight);
    }

    *targetHeel = Vector3Add(*targetToe, Vector3Subtract(
        globalTransforms[heelBoneIndex].translation,
        globalTransforms[toeBoneIndex].translation));

    if (enableHeightClamp)
    {
        targetHeel->y = Max(targetHeel->y, heelMinHeight);
    }

    Vector3 sideVector = Vector3RotateByQuaternion(
        kneeSideVector,
        globalTransforms[kneeBoneIndex].rotation);

    float maxExtension = Vector3Distance(
        globalTransforms[hipBoneIndex].translation,
        globalTransforms[heelBoneIndex].translation);

    Quaternion modifiedHip, modifiedKnee;
    TwoBoneInverseKinematics(
        &modifiedHip,
        &modifiedKnee,
        globalTransforms[pelvisBoneIndex],
        globalTransforms[hipBoneIndex],
        globalTransforms[kneeBoneIndex],
        globalTransforms[heelBoneIndex],
        *targetHeel,
        sideVector,
        maxExtension,
        softening);

    localTransforms[hipBoneIndex].rotation = modifiedHip;
    localTransforms[kneeBoneIndex].rotation = modifiedKnee;

    if (enableHeelLookAt)
    {
        ForwardKinematics(globalTransforms, localTransforms, model);
        localTransforms[heelBoneIndex].rotation = BoneOrientTowards(
            globalTransforms[kneeBoneIndex],
            globalTransforms[heelBoneIndex],
            globalTransforms[toeBoneIndex],
            *targetToe);
    }

    if (enableToeLookAt)
    {
        ForwardKinematics(globalTransforms, localTransforms, model);
        *targetToeEnd = globalTransforms[toeEndBoneIndex].translation;

        if (enableHeightClamp)
        {
            targetToeEnd->y = Max(targetToeEnd->y, toeEndMinHeight);
        }

        localTransforms[toeBoneIndex].rotation = BoneOrientTowards(
            globalTransforms[heelBoneIndex],
            globalTransforms[toeBoneIndex],
            globalTransforms[toeEndBoneIndex],
            *targetToeEnd);
    }

    ForwardKinematics(globalTransforms, localTransforms, model);
}

This deliberately recomputes global transforms more often than necessary. In production, you only need to update the bones consumed by the next stage. The solver also assumes a flat ground plane at zero height; on terrain, use a raycast.

The important result is a stable black box: given a pose and a toe target, it returns a plausible leg pose. Foot locking then becomes the problem of providing a good toe target.

Runtime Foot Locking

During a contact, follow a fixed floor point. Without a contact, follow the toe position from the source animation. Transition between the two sources with inertialization.

typedef struct FootLockingState
{
    Vector3 position;
    Vector3 velocity;
    Vector3 inputPosition;
    Vector3 inputVelocity;
    Vector3 offsetPosition;
    Vector3 offsetVelocity;
    float time;
    Vector3 contact;
    bool locked;
} FootLockingState;
void UpdateFootLockingState(
    FootLockingState *state,
    Vector3 inputPosition,
    bool inputContact,
    float contactHeight,
    float deltaTime,
    float unlockDistance,
    float lockDistance,
    float blendTime)
{
    state->inputVelocity = Vector3Scale(
        Vector3Subtract(inputPosition, state->inputPosition),
        1.0f / Max(deltaTime, 1e-8f));
    state->inputPosition = inputPosition;

    InertializeCubicUpdate(
        &state->position,
        &state->velocity,
        &state->time,
        state->locked ? state->contact : state->inputPosition,
        state->locked ? Vector3Zero() : state->inputVelocity,
        state->offsetPosition,
        state->offsetVelocity,
        deltaTime,
        blendTime);

    float inputDistance = Vector3Distance(state->position, state->inputPosition);

    if (!state->locked && inputContact && inputDistance < lockDistance)
    {
        state->locked = true;
        state->contact = state->inputPosition;
        state->contact.y = contactHeight;

        InertializeCubicTransition(
            &state->offsetPosition,
            &state->offsetVelocity,
            &state->time,
            state->inputPosition,
            state->inputVelocity,
            state->contact,
            Vector3Zero(),
            blendTime);
    }
    else if (state->locked && (!inputContact || inputDistance > unlockDistance))
    {
        state->locked = false;

        InertializeCubicTransition(
            &state->offsetPosition,
            &state->offsetVelocity,
            &state->time,
            state->contact,
            Vector3Zero(),
            state->inputPosition,
            state->inputVelocity,
            blendTime);
    }
}
void InertializeCubicUpdate(
    Vector3 *position,
    Vector3 *velocity,
    float *time,
    Vector3 inputPosition,
    Vector3 inputVelocity,
    Vector3 offsetPosition,
    Vector3 offsetVelocity,
    float deltaTime,
    float blendTime)
{
    float t = Clamp((*time + deltaTime) / Max(blendTime, 1e-8f), 0.0f, 1.0f);
    float w0 = 2.0f * t * t * t - 3.0f * t * t + 1.0f;
    float w1 = (t * t * t - 2.0f * t * t + t) * blendTime;
    float w2 = (6.0f * t * t - 6.0f * t) / Max(blendTime, 1e-8f);
    float w3 = 3.0f * t * t - 4.0f * t + 1.0f;

    *position = Vector3Add(inputPosition, Vector3Add(
        Vector3Scale(offsetPosition, w0),
        Vector3Scale(offsetVelocity, w1)));
    *velocity = Vector3Add(inputVelocity, Vector3Add(
        Vector3Scale(offsetPosition, w2),
        Vector3Scale(offsetVelocity, w3)));
    *time = *time + deltaTime;
}

void InertializeCubicTransition(
    Vector3 *offsetPosition,
    Vector3 *offsetVelocity,
    float *time,
    Vector3 sourcePosition,
    Vector3 sourceVelocity,
    Vector3 destinationPosition,
    Vector3 destinationVelocity,
    float blendTime)
{
    float t = Clamp(*time / Max(blendTime, 1e-8f), 0.0f, 1.0f);
    float w0 = 2.0f * t * t * t - 3.0f * t * t + 1.0f;
    float w1 = (t * t * t - 2.0f * t * t + t) * blendTime;
    float w2 = (6.0f * t * t - 6.0f * t) / Max(blendTime, 1e-8f);
    float w3 = 3.0f * t * t - 4.0f * t + 1.0f;

    *offsetPosition = Vector3Add(
        Vector3Scale(Vector3Subtract(sourcePosition, destinationPosition), w0),
        Vector3Scale(Vector3Subtract(sourceVelocity, destinationVelocity), w1));
    *offsetVelocity = Vector3Add(
        Vector3Scale(Vector3Subtract(sourcePosition, destinationPosition), w2),
        Vector3Scale(Vector3Subtract(sourceVelocity, destinationVelocity), w3));
    *time = 0.0f;
}
The toe target toggles smoothly between tracked and locked states.

Then feed that target into the leg solver:

UpdateFootLockingState(
    &leftLockState,
    globalTransforms[leftToeBoneIndex].translation,
    testContacts.leftContacts[animationFrame] > contactThreshold,
    toeMinHeight,
    deltaTime,
    unlockDistance,
    lockDistance,
    lockBlendTime);

leftTarget = leftLockState.position;

if (enableInverseKinematics)
{
    SolveLegChain(
        genoModel,
        modifyTransforms,
        globalTransforms,
        leftTarget,
        &leftTargetHeel,
        &leftTargetToe,
        &leftTargetToeEnd,
        pelvisBoneIndex,
        leftHipBoneIndex,
        leftKneeBoneIndex,
        leftHeelBoneIndex,
        leftToeBoneIndex,
        leftToeEndBoneIndex,
        enableHeightClamp,
        enableHeelLookAt,
        enableToeLookAt,
        heelMinHeight,
        toeMinHeight,
        toeEndMinHeight,
        softening,
        (Vector3){ 1.0f, 0.0f, 0.0f });
}
The final runtime foot pinning result.

Contact Times

To lock the foot, you need contact labels. Manual labeling is the gold standard, but velocity and height heuristics get you most of the way.

Start with the global velocity magnitude of the toe:

Toe velocity signal
Low toe velocity is a strong contact signal.
Thresholded toe velocity
The same signal after thresholding.

Toe height is less reliable than velocity, but it is a useful sanity check to reject stationary-but-airborne poses:

Toe height signal
Toe height alone is noisier and harder to threshold.

In good locomotion data, a velocity threshold between roughly 0.1 and 0.5 m/s and a height threshold around 0.1 m are sensible starting points.

I usually apply a majority-vote filter next. It removes single-frame activations and deactivations:

Majority vote contact filtering
A five-frame majority filter is a good default at 60 Hz.

Then optionally smooth the binary signal into a continuous one:

Gaussian smoothed contact signal
Smoothing lets the runtime tune contact sensitivity and timing.

This is not foolproof. Running contacts can last only one or two frames at 30 Hz. Keeping animation data at 60 Hz, and using cubic interpolation when upsampling, makes automatic labeling much easier.

Offline Foot Locking

If the whole animation is available, formulate foot sliding as constraints and solve them with a position-based-dynamics-like iteration.

Treat pelvis and toe positions as particles. Preserve their source-animation relations with soft springs, but add hard constraints that bring in-contact toe particles across adjacent frames together.

Vector3* pelvisLocations = RL_CALLOC(animation.frameCount, sizeof(Vector3));
Vector3* leftToeLocations = RL_CALLOC(animation.frameCount, sizeof(Vector3));
Vector3* rightToeLocations = RL_CALLOC(animation.frameCount, sizeof(Vector3));

for (int i = 0; i < animation.frameCount; i++)
{
    pelvisLocations[i] =
        animation.framePoses[i][pelvisBoneIndex].translation;
    leftToeLocations[i] =
        animation.framePoses[i][leftToeBoneIndex].translation;
    rightToeLocations[i] =
        animation.framePoses[i][rightToeBoneIndex].translation;
}
float softFactor = 0.05f;
float hardFactor = 0.9f;
int iterations = 25000;

for (int iteration = 0; iteration < iterations; iteration++)
{
    for (int i = 0; i < animation.frameCount; i++)
    {
        // Enforce inter-frame and in-frame constraints here.
    }
}

For the left leg, inter-frame constraints look like this:

if (i > 0)
{
    Vector3 restPrevToe =
        animation.framePoses[i - 1][leftToeBoneIndex].translation;
    Vector3 restCurrToe =
        animation.framePoses[i - 0][leftToeBoneIndex].translation;
    Vector3 restPrevHip =
        animation.framePoses[i - 1][pelvisBoneIndex].translation;
    Vector3 restCurrHip =
        animation.framePoses[i - 0][pelvisBoneIndex].translation;

    Vector3 consPrevToe = leftToeLocations[i - 1];
    Vector3 consCurrToe = leftToeLocations[i - 0];
    Vector3 consPrevHip = pelvisLocations[i - 1];
    Vector3 consCurrHip = pelvisLocations[i - 0];

    if (contacts.leftContacts[i - 1] > contactThreshold &&
        contacts.leftContacts[i - 0] > contactThreshold)
    {
        Vector3 toeTarget = Vector3Lerp(
            consPrevToe, consCurrToe, 0.5f);
        toeTarget.y = toeMinHeight;

        leftToeLocations[i - 1] = Vector3Lerp(
            consPrevToe, toeTarget, hardFactor);
        leftToeLocations[i - 0] = Vector3Lerp(
            consCurrToe, toeTarget, hardFactor);
    }
    else
    {
        Vector3 prevToeTarget = Vector3Add(
            consCurrToe, Vector3Subtract(restPrevToe, restCurrToe));
        Vector3 currToeTarget = Vector3Add(
            consPrevToe, Vector3Subtract(restCurrToe, restPrevToe));

        prevToeTarget.y = Max(prevToeTarget.y, toeMinHeight);
        currToeTarget.y = Max(currToeTarget.y, toeMinHeight);

        leftToeLocations[i - 1] = Vector3Lerp(
            consPrevToe, prevToeTarget, softFactor);
        leftToeLocations[i - 0] = Vector3Lerp(
            consCurrToe, currToeTarget, softFactor);
    }

    pelvisLocations[i - 1] = Vector3Lerp(
        consPrevHip,
        Vector3Add(consCurrHip,
            Vector3Subtract(restPrevHip, restCurrHip)),
        softFactor);

    pelvisLocations[i - 0] = Vector3Lerp(
        consCurrHip,
        Vector3Add(consPrevHip,
            Vector3Subtract(restCurrHip, restPrevHip)),
        softFactor);
}

Then add an in-frame constraint that preserves the hip-to-toe length:

Vector3 restHip =
    animation.framePoses[i][pelvisBoneIndex].translation;
Vector3 restToe =
    animation.framePoses[i][leftToeBoneIndex].translation;
float restLength = Vector3Distance(restHip, restToe);

Vector3 currHip = pelvisLocations[i];
Vector3 currToe = leftToeLocations[i];
Vector3 currDirection = Vector3Normalize(
    Vector3Subtract(currHip, currToe));

pelvisLocations[i] = Vector3Lerp(
    currHip,
    Vector3Add(currToe,
        Vector3Scale(currDirection, +restLength)),
    softFactor);

leftToeLocations[i] = Vector3Lerp(
    currToe,
    Vector3Add(currHip,
        Vector3Scale(currDirection, -restLength)),
    softFactor);

After all iterations, feed the corrected pelvis and toe targets back into SolveLegChain for every frame:

for (int i = 0; i < animation.frameCount; i++)
{
    BackwardKinematics(localTransforms, animation.framePoses[i], model);
    localTransforms[pelvisBoneIndex].translation = pelvisLocations[i];
    ForwardKinematics(globalTransforms, localTransforms, model);

    SolveLegChain(
        model,
        localTransforms,
        globalTransforms,
        leftToeLocations[i],
        &leftTargetHeel,
        &leftTargetToe,
        &leftTargetToeEnd,
        pelvisBoneIndex,
        leftHipBoneIndex,
        leftKneeBoneIndex,
        leftHeelBoneIndex,
        leftToeBoneIndex,
        leftToeEndBoneIndex,
        enableHeightClamp,
        enableHeelLookAt,
        enableToeLookAt,
        heelMinHeight,
        toeMinHeight,
        toeEndMinHeight,
        softening,
        leftKneeSideVector);

    SolveLegChain(
        model,
        localTransforms,
        globalTransforms,
        rightToeLocations[i],
        &rightTargetHeel,
        &rightTargetToe,
        &rightTargetToeEnd,
        pelvisBoneIndex,
        rightHipBoneIndex,
        rightKneeBoneIndex,
        rightHeelBoneIndex,
        rightToeBoneIndex,
        rightToeEndBoneIndex,
        enableHeightClamp,
        enableHeelLookAt,
        enableToeLookAt,
        heelMinHeight,
        toeMinHeight,
        toeEndMinHeight,
        softening,
        rightKneeSideVector);

    ForwardKinematics(animation.framePoses[i], localTransforms, model);
}

Compare root motion scaled up by 1.25:

No correction.
Runtime inertialization.
Offline particle-based correction.

And root motion scaled down by 0.75:

No correction.
Runtime inertialization.
Offline particle-based correction.

Because the offline pass sees the entire clip, it distributes the correction instead of reacting to contacts frame by frame.

Philosophy and Pitfalls

Foot Sliding Is About Velocity

Foot sliding is a velocity mismatch between the source animation and the runtime character. It can come from the root moving at a different speed than the animation, or from blending and modifying local joint rotations.

Do not think of it primarily as friction or physics. That framing leads people to label contacts by height and then over-constrain the whole foot. The more useful model is velocity preservation, especially at contacts because that is where the error is easiest to see.

Lock the Toe, Not the Heel

Toe contact without heel contact
Toe contact is far more common and more stable than heel contact.

In locomotion, the toe area is in contact most of the time. Heel-only contact is short and often unstable. Toe-only contact is common, and characters pivot around the toe.

Do not lock the heel just because it penetrates the ground in a bad pose. Give the heel freedom and constrain the toe.

IK Is a Modification, Not a Replacement

Procedural IK replacement style
Classic rigging IK replaces the pose; runtime foot IK should modify it.

Two-joint IK is often imagined as a procedural replacement for three joints. That is useful in rigging, but for foot locking it destroys too much source detail. Treat IK as a minimal modification of an existing pose.

Avoid the Dinosaur

Pulling the hips down to satisfy IK produces bent, dinosaur-like knees.
Leg close to hyper-extension
Real locomotion often sits close to hyper-extension.

Real legs often run very close to hyper-extension. Pulling hips down removes the IK failure but ruins the motion. Prefer a little sliding to a broken pose. Preserve the input motion velocity, and let the solver make only the minimum necessary change.

Source Code

The full source code accompanying the original article is available in the GenoView-InverseKinematics repository.


Source: Inverse Kinematics and Foot Locking, Orange Duck.

As an Amazon Associate, we earn from qualifying purchases.