Skip to content

Procedural animation

Motion worked out every frame instead of played back from a clip: feet that find the ground, animals with any number of legs that walk, trot and gallop without a single walk clip, heads that turn toward what they notice, and bodies that stagger when hit and catch themselves (or don't).

Status (2026-09-27): done: the engine side, the play-to-make blocks (Shove, Look at, Look away) and games/procedural_demo are on main and tested.

Everything lives in kke/ProceduralAnim.h (the reference) on top of what was already there:

Already in the engine Where
Clips, blend spaces, crossfading state machine kke/Animator.h
Two-bone IK, biped foot placement, retargeting kke/AnimRig.h
Ragdolls (humanoid, quadruped), joint limits kke/Ragdoll.h, RAGDOLLS.md
Secondary motion (tails, ears, soft bodies) kke/JigglePhysics.h, JIGGLE.md
Locomotion controller (what moves the capsule) kke/Locomotion.h, MOVEMENT.md

Nothing here is new science. The algorithms are the standard published ones: FABRIK for long chains (Aristidou & Lasenby 2011), the Raibert heuristic for where a foot lands (Raibert 1986), gait phase tables from biomechanics (Hildebrand; Alexander 1984, including the Froude number that tells every animal when to change gait), and motor-driven active ragdolls exactly the way Jolt's own JPH::Ragdoll::DriveToPoseUsingMotors does it (Jolt is already a dependency; no new library was needed).

The layers, in order

Every piece is a layer that takes a kke::Pose and changes it, with a weight so it can fade in and out:

Animator (clips)                      or the rest pose, for a creature with no clips
  -> blendPosesMasked / addPose       upper body waves while the legs walk; additive flinch
  -> ProceduralGait + applyGait       procedural legs          (or LegPlacer: clip legs on hills)
  -> LookAt                           head / spine toward a target
  -> solveTwoBone / solveFabrik       hands on things, tails, tentacles
  -> JigglePhysics                    secondary motion
  -> ActiveRagdoll                    physics follows all of the above; hits push it off

Legs for any animal: ProceduralGait

The controller (AI, player input, kke::Locomotion) moves the creature as a point with a heading. ProceduralGait plans the footsteps: each foot stays planted in the world until its turn in the gait, then swings in an arc to where it will be needed next, found by asking the ground. The body rises and settles with the terrain, pitches and rolls to its feet, bobs at push-off and leans into turns.

#include "kke/ProceduralAnim.h"

// A dog-sized quadruped: 0.8 m long, 0.3 m wide, hips 0.5 m up.
kke::ProceduralGait gait(kke::makeLegs(4, 0.8f, 0.3f, 0.5f));

auto ground = [&](const glm::vec3& from, glm::vec3& hit, glm::vec3& normal) {
    auto h = physics->physicsRaycast(from, {0, -1, 0}, 3.0f); // any IPhysicsWorld
    if (!h.hit) return false;
    hit = h.point; normal = h.normal; return true;
};

gait.reset(bodyWorld, ground);                                // once, and after teleports
// every frame, after the controller moved the body:
gait.update(bodyWorld, velocity, turnRateDegrees, ground, dt);
for (int i = 0; i < gait.legCount(); ++i) {
    const auto& f = gait.foot(i);                             // f.position, f.normal, f.planted
    if (f.landed) footstepAt(f.position);                     // sound and dust
}
glm::mat4 drawnBody = gait.bodyPose();                        // for the torso / pelvis

Legs are numbered front to back, left then right: a biped is L, R; a quadruped FL, FR, BL, BR (the same order as kke::QuadrupedBones); six legs L1, R1, L2, R2, L3, R3; eight likewise. makeLegs(count, length, width, hipHeight) lays out mirror pairs; a LegDesc per leg (hip, rest foot, upper and lower segment lengths, in body space: +Z forward, origin on the ground) describes anything else.

Gaits (kke::Gait): walk, trot, pace, canter, gallop, bound, pronk, tripod, wave, and Auto, which picks from speed with the Froude number v²/(g·hip height): quadrupeds walk below 0.5, trot to 2.5 and gallop above; bipeds walk then run; six and eight legs wave then run on tripods. Because the thresholds are relative to leg length, a pug and a horse both change gait at the right speed for their size. Gaits change mid-stride without feet teleporting: a leg finishes its swing before the new timing takes over.

Timing. A cycle is between cycleFast and cycleSlow pendulum periods of the leg (2π√(L/g)), and never so long that a foot would have to drag further than strideScale × leg length. Standing still, feet that ended up far from rest (resettle) step back under the body, one gait beat at a time.

On a skeleton

For a rigged animal (the Quaternius farm animals, anything with QuadrupedBones names) the same gait drives the bones:

auto chains = kke::quadrupedLegChains(model);                 // FL, FR, BL, BR
kke::ProceduralGait gait(kke::legsFromSkeleton(model, chains, modelToBody));
int hips = /* index of "Hips" */;
// per frame:
kke::Pose pose = animSet.restPose();                          // or an Animator pose (idle, eat...)
kke::applyGait(model, pose, chains, hips, gait, instanceTransform);

applyGait moves the hips by the gait's body sway and puts every foot on its planned spot with two-bone IK. Knees keep bending the way they bend at rest, so front knees and hind hocks fold opposite ways.

Clip legs on hills: LegPlacer

An animal with good walk and run clips doesn't need procedural legs, only feet that meet the ground. LegPlacer is FootPlacer for any number of legs: each foot keeps its animated height above the ground under it, the body drops as far as the lowest foot needs and pitches / rolls to the slope, and two-bone IK bends the legs.

kke::LegPlacer legs(kke::quadrupedLegChains(model), hips);
legs.apply(model, pose, groundInModelSpace, dt, onGround ? 1.0f : 0.0f);

Look-at and aim: LookAt

kke::LookAt look = kke::LookAt::quadruped(model);   // Neck, Head
// or kke::LookAt::humanoid(model)                  // spine_02, spine_03, neck_01, head
glm::vec3 target = worldToModel * glm::vec4(player, 1);
look.apply(model, pose, noticed ? &target : nullptr, dt);

The turn is shared along the chain (more in the head, less in the spine), limited (maxYaw, maxPitch) and smoothed (speed). A target further round than giveUpYaw is dropped and the head comes back to forward rather than snapping over the shoulder. For aiming, build a LookAt from any chain of Link{bone, share} and pass the weapon's direction as forward.

A person's arm: solveHumanArm

solveTwoBone bends a chain wherever its pole says and swings it the shortest way, so a person's arm can end up reaching through its own back, bending its elbow backwards or twisting its upper arm. For people, use solveHumanArm (kke/AnimRig.h): it places the elbow by its swivel round the shoulder-hand line (Tolani, Goswami & Badler 2000), starting from where an elbow hangs, and builds each bone from its direction and the elbow's hinge, so the elbow only bends forward. ArmLimits holds the joint ranges: the shoulder's (in the chest's frame, so a turned torso turns it), the elbow's bend, the forearm's twist, the wrist's bend and twist. A goal outside them is moved to the nearest pose the arm can make, and the result says where the hand went: attach what the hand holds to that.

kke::HumanArm right = kke::makeHumanArm(model, rightArm, leftArm); // once
kke::ArmGoal goal;
goal.hand = targetModelSpace;
goal.handRotation = gripRotation;  // optional: the forearm and wrist turn it as far as they go
goal.elbowToward = poleModelSpace; // optional: leans the elbow, within ArmLimits::swivel
kke::ArmResult held = kke::solveHumanArm(model, pose, right, goal);

Raised overhead, the range closes in (acrossOverhead, behindOverhead: the hand can't go as far across or behind the head as it can at shoulder height), and the shoulder girdle moves with the arm: when the rig has a clavicle above the upper arm (HumanArm::clavicle, found by name), the clavicle lifts about one degree for every two the arm rises past shoulder height (up to shoulderShrug, 30°) and swings forward as the hand reaches ahead (shoulderReach). That is the scapulohumeral rhythm every person has; an arm raised by the shoulder joint alone pinches the mesh at the shoulder and looks dislocated.

Tennis holds its racket this way (the racket follows the hand) and Climb Race puts its hands on holds with it. Both use the overload that also knows the body (kke/BodyShape.h: the arm goes round the torso, head and legs, never through them) and hold things by the palm with the fingers closed on them (kke/Equipment.h); EQUIPMENT.md explains both.

Clip, contacts, lean: CharacterIk

kke::CharacterIk (kke/CharacterIk.h) is the whole layer a person gets on top of their animation, in the order the big engines run it:

  1. The clip is the base. The Animator's pose carries the style and the timing of every move (walk, vault, climb, hang).
  2. Contacts put the hands and feet where the world says they are. Feet stand on the ground under them (FootPlacer, which asks under the heel, the ball and the toe tip of each foot and takes the highest, so a foot half over a step stands on it and never in it; a planted foot never sinks while the smoothing catches up). A foot can also be planted on a ledge, a wall or a box top (foot(side, point, normal)), and a hand put on an edge or a handle (hand(side, point, elbowToward)), solved as a human arm that keeps out of its own body.
  3. Procedural fine-tuning. The upper body leans into speeding up, slowing down and turning (1.6° per m/s², at most 9°).

Contacts are set every frame in world space; a limb without one fades back to the clip, so a hand reaching for a ledge or a foot leaving a wall never pops.

kke::CharacterIk ik(rig, &skinnedModel);                  // once: bones, arms, body shape
// every frame, after the Animator:
kke::Pose pose = animator.pose();
ik.feetOnGround(onGround);                                 // off in the air
if (hanging) {
    ik.hand(kke::CharacterIk::Left, edgeLeft, elbowHintLeft);
    ik.hand(kke::CharacterIk::Right, edgeRight, elbowHintRight);
    ik.foot(kke::CharacterIk::Left, wallPoint, wallNormal); // braced on the wall
}
ik.apply(rig, pose, modelToWorld, groundRay, velocity, dt);
kke::poseToLocals(pose, locals);

The walkable showcase (games/showcase, kke_demo) uses it for walking, vaulting, climbing, hanging and shimmying: hands on the edge, the balls of the feet braced on the wall while hanging, feet onto the top at the end of a climb. Tests: tests/test_character_ik.cpp.

Clips that walk away: makeClipsInPlace

Some clips have the travel baked into the root or hips bone (Synty's animals, many mocap walks). Played on a character the game moves itself, the model walks ahead of its body and snaps back every loop. loadModel now makes every clip play in place (ModelLoadOptions::clipsInPlace, on by default): the topmost bone whose own translation travels from the first frame to the last loses that steady drift and keeps its sway and bob. A turn in place isn't touched. Set clipsInPlace = false for a file meant as root motion (AnimationSet::extractRootMotion).

Long chains: solveFabrik

Two-bone IK (kke::solveTwoBone, or solveHumanArm for people's arms) is exact for arms and most legs. Tails, necks, tentacles and three-segment insect legs use FABRIK:

kke::IkChain tail = kke::findIkChain(model, "Tail1", "Tail4");
kke::solveFabrik(model, pose, tail, targetModelSpace, &pole);

fabrikPoints does the same on bare joint positions (creatures drawn from primitives), and kneePosition gives the knee of a two-segment limb.

When the chain must bend like a body does (a spine, a tentacle that can't fold back on itself), solveCcd (ENG-G19) keeps every joint within a bend limit, and a step limit per pass spreads the bend instead of curling the tip, which plain CCD does:

kke::CcdSettings ccd;
ccd.maxBendDegrees.assign(tail.bones.size() - 1, 25.0f); // per joint, root first
ccd.maxStepDegrees = 4.0f;                                // damping
kke::solveCcd(model, pose, tail, targetModelSpace, 1.0f, ccd);

Measured on 1000 random targets: FABRIK is faster (0.9 against 61 µs for 10 joints) and reaches more, but leaves 60 to 90% of its solves past those limits; CCD never does. Damping 4 degrees a pass brings the sharpest bend of a 10-joint reach from 57 to 38 degrees. ccdPoints works on bare points.

Layering clips: blendPosesMasked, addPose

kke::BoneMask upper = kke::boneMask(model, {"spine_01"});        // spine and everything above
kke::blendPosesMasked(walkPose, wavePose, upper, 1.0f, pose);    // walk + wave
kke::addPose(pose, flinchPose, flinchFirstFrame, {}, 0.7f, pose); // additive flinch on top

Active ragdoll: hit reactions and balance

A ragdoll whose joints have motors, driven toward the animation: at full strength it looks exactly like the clip; a hit weakens the muscles around the body that was hit (and, less, the joints next to them), knocks the balance, and both recover over time. Too far off balance (the torso leaning more than fallTilt from where the clip has it, or the pelvis sinking by fallDrop) and it goes limp, lies there for getUpDelay, then blends back to the clip.

Animated --hit()--> Active --steady for calmSeconds--> GettingUp --> Animated
                      \--off balance--> Fallen --getUpDelay--/
kke::ActiveRagdoll active(desc);                            // desc from buildHumanoidRagdoll / buildQuadrupedRagdoll
// when something hits the character:
if (!active.physical()) handle = ragdolls->createRagdoll(desc, velocity);
active.hit(body, push);
ragdolls->pushRagdollBody(handle, body, push);
// every frame:
active.setTargets(binding, animatedBoneWorld);              // where the clip has the bones
std::vector<glm::mat4> bodies;
ragdolls->ragdollBodyTransforms(handle, bodies);
active.update(dt, bodies);
if (active.physical()) ragdolls->driveRagdoll(handle, active.drive());
else if (handle) { ragdolls->destroyRagdoll(handle); handle = 0; }
// skin from poseFromRagdoll(...) while physical(); in GettingUp blend with
// blendPoses(ragdollPose, animatedPose, active.getUpBlend()).

IRagdollPhysics::driveRagdoll is implemented by the Jolt module (RigidBodyModule): swing-twist and hinge motors in position mode, torque limited by strength (torquePerKg × the heavier body's mass at full strength), plus an optional pull on a few bodies straight to their targets (assistBodies and assist: the pelvis, and the chest too on four legs, with gravity fed forward so it doesn't sag) that is what keeps a hit character on its feet and goes away when balance is lost. It is applied before every physics step, so it holds the same at 20 fps as at 144. FEMFX ragdolls have no motors: there driveRagdoll returns false and characters simply go limp.

On any model: ModelModule::setPoseModifier

A skinned model playing a clip can have a procedural layer on top: the modifier gets the clip's pose as model-space bone matrices each frame and changes them, and that is what is skinned and what boneWorld() returns. poseFromModel turns the matrices into the per-bone pose the layers here take (and poseToModel back):

models.setPoseModifier(instance, [&](std::vector<glm::mat4>& bones, float dt) {
    kke::Pose pose = kke::poseFromModel(model, bones);
    look.apply(model, pose, &targetInModelSpace, dt);
    bones = kke::poseToModel(model, pose);
});

It is skipped while a ragdoll (setBoneWorldOverride) drives the skeleton.

Driven by the AI

kke::ai::Agent (AI.md) already says what each animal wants, so a rig reads it rather than keeping its own state:

Agent field Drives
position, yaw, velocity ProceduralGait::update(body, velocity, turnRate, ground, dt): the body frame from position and yaw, the gait (walk, trot, gallop) from the speed
anim the layer on top: eat/drink/sniff lower the head (LookAt at the ground in front), rest lowers the body (GaitSettings), attack is a lunge, alert holds still and looks
hasLookAt, lookAt LookAt::apply (neck and head), nullptr when hasLookAt is false
enabled = false ragdolled: hand the body to ActiveRagdoll and give it back on Animated

Turn rate is the change in yaw over the frame. A hit on an animal is ActiveRagdoll::hit; while it is physical() set enabled = false so the AI doesn't steer a body it doesn't control.

Play-to-make

Following PLAY_TO_MAKE.md, the same blocks are open at the node and Lua levels, and the Simple level gets them through its recipes:

Node ("People") Lua What happens
"Shove" play.stagger(thing [, push]) They stagger and try to keep their feet (joint motors toward the pose they had); a big shove (about 4 m/s and up) still knocks them over, and they get up again by themselves where they landed. FellOver / StoodUp fire as usual. The bat still knocks them flat, even mid-stagger.
"Look at" play.lookAt(thing [, at]) They keep turning head and upper spine toward at: a thing (followed as it moves), a place (Vec, Lua), or you (the camera) when left out. Works on top of whatever clip they play.
"Look away" play.lookAway(thing) Back to looking ahead, smoothly.

A world without joint motors (a FEMFX-only build) makes Shove an ordinary knock-over, and Look at returns false; neither is an error, so a graph made in one build runs in any.

Trying it

games/procedural_demo (no Synty assets needed): a spider, a beetle, a dog and a person, built from generated skeletons with no clips at all, walk over hills and a flight of steps. Click the ground and everyone comes to the flag; click the dog or the person to hit them (Shift for a hard hit that knocks them down; they get up again); click a bug and it runs off. 1 / 2 / 3 make the dog walk, trot or gallop, 0 lets it pick by speed. See its README for the KKE_PROC_* switches.

The procedural demo

In the sandbox, play.stagger and play.lookAt work on placed people (from a node graph or a Lua script).

Tests

tests/test_procedural_anim.cpp: gait tables (diagonal pairs trot together, duty factors), Froude gait changes, feet staying planted while in stance, steps landing on raised ground, body pitch on a slope, look-at limits and give-up, FABRIK reach and bone lengths, masked and additive blending, the active ragdoll state machine, and the Jolt motor drive holding a limb on target, a ball joint reaching its target, and a motor-driven character staying up where a limp one falls (tests/test_jolt_ragdoll.cpp). The play blocks are covered in tests/test_node_graph.cpp (ShoveAndLookAtWorkWithAnyWorld).