Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,292 words · 1 segments analyzed
30/07/2026 One of my most requested blog posts has always been something on inverse kinematics and foot locking. That isn't surprising - foot sliding is something that affects almost all animation systems, and proper foot locking and inverse kinematics is something that is tricky to get right, yet can have a huge positive impact on the visual quality of the resulting animation. But speak to any two animation programmers and they will likely give you two completely different answers as to the best way to solve foot-sliding. Undoubtedly this topic is much more of an art than a science - which might explain why it isn't always easy to find resources on it - and why it gets less attention in the academic sphere. With that being said, I thought I would share a selection of little recipes related to solving foot-sliding. And while I'm certain these are not the proven-best solutions to this problem (and there are likely many games which do it better), they have served me well over the years, and so should make a good starting point for anyone getting into the topic. For digestibility I've split the topic into a few parts. First I'll cover my method for solving a leg chain to position the toe at a desired location. Second, I'll cover my solution for locking the position of the toe during contacts at runtime using inertialization. Third, a technique for automatically annotating contacts in animation data. Fourth, an offline method for foot sliding removal for when you have access to the whole animation and cycles to spare. And finally I'll close with a few philosophical thoughts about the whole thing. While I provide code through the article, the full source code for this article can be found here. Solving a Leg Chain Foot Locking Contact Times Offline Foot Locking Conclusion Solving a Leg Chain Overview Here is the problem set-up. We have a chain of joints for the leg of a character, posed in a particular configuration, and we want to modify the local rotations such that the existing pose is preserved as much as possible, but where the toe ends up at some desired location: To do this we're going to perform the following steps: Compute the target heel location from the target toe location Solve the two-joint IK problem to position the heel at its target Rotate the heel joint to orient the toe towards the toe target Optionally rotate the toe-end to resolve any ground collisions Find the Heel Target Computing the desired heel location is easy - we simply compute the vector from the toe to the heel in the existing input pose and add this to the toe target. *targetHeel = Vector3Add(*targetToe, Vector3Subtract( globalTransforms[heelBoneIndex].translation, globalTransforms[toeBoneIndex].translation)); Which looks like this: 2. Solve for the Heel Target Once we have the heel target we can solve for the local rotations of the hip and knee joints such that it positions the leg chain correctly. To do this I'm going to use a slightly modified version of the two bone inverse kinematics code I shared a long time ago (see this page for the derivation of the QuaternionFromScaledAngleAxis function). 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) { // Softly clamp the target based on the distance given by maxExtension Vector3 targetClamp = targetHeel; float targetLength = Vector3Distance(targetHeel, globalHip.translation); if (targetLength > maxExtension - softening) { // Smoothly clamp when it gets within softening distance of the maxExtension 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)); } // Compute the rotation axis based on vector perpendicular to the plane // rotation which is closed to the provided knee side vector 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; // Compute the change in rotation angle required using the cosine rule 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)); // Compute the three world-space rotations needed to solve the two-bone ik problem 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))); // Update the local space rotations for the hip and the knee *localHip = QuaternionMultiply(QuaternionMultiply(QuaternionMultiply( QuaternionInvert(globalPelvis.rotation), r2), r0), globalHip.rotation); *localKnee = QuaternionMultiply(QuaternionMultiply( QuaternionInvert(globalHip.rotation), r1), globalKnee.rotation); } The two additions here are simple. First, I let the user provide a max extension length maxExtension which I softly clamp the target towards based on the softening distance. This means that the limb only ever exponentially approaches the provided maxExtension, which helps prevent hyper-extension: Vector3 targetClamp = targetHeel; float targetLength = Vector3Distance(targetHeel, globalHip.translation); if (targetLength > maxExtension - softening) { // Smoothly clamp when it gets within softening distance of the maxExtension 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)); } Second, I use the side vector of the knee joint to find a stable axis to rotate around. This prevents the need for a pole-vector or anything like that. Vector3 axisDwn = Vector3Normalize( Vector3Subtract(globalHeel.translation, globalHip.translation)); Vector3 axisFwd = Vector3Normalize(Vector3CrossProduct(axisDwn, sideVector)); Vector3 axisRot = Vector3Normalize(Vector3CrossProduct(axisDwn, axisFwd)); Assuming you have two buffers localTransforms and globalTransforms filled with bone local and global transforms, you can call the function like this: // Solve Two-Bone Inverse Kinematics to place heel 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; Where, for the geno character, kneeSideVector is just (Vector3){ 1.0f, 0.0f, 0.0f } and a typical value for softening might be 0.005f (meters). Once completed this positions the heel at the target, but the local rotation we've applied means that the toe joint is no longer rotated toward its target. 3. Solve for the Toe Target To solve for the toe target we can just perform a basic look-at using QuaternionBetween (For the curious, I wrote a bit about the derivation of this function here): 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); } Which can be used as follows: ForwardKinematics(globalTransforms, localTransforms, model); localTransforms[heelBoneIndex].rotation = BoneOrientTowards( globalTransforms[kneeBoneIndex],