Skip to content

[Feature] Motion matching - #12

Merged
miogds merged 3 commits into
developfrom
feature/animation_motion_matching
Jul 27, 2026
Merged

[Feature] Motion matching#12
miogds merged 3 commits into
developfrom
feature/animation_motion_matching

Conversation

@miogds

@miogds miogds commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

M3 of the animation roadmap (docs/Architecture/animationPoseLayer.md): characters animate with no animation state machine. Gameplay states a goal — desired velocity and facing, typically straight from SteeringSystem — and every search interval (default 0.1 s) the controller finds the database frame best matching the current pose and the predicted future trajectory, then jumps there through the existing inertialized-transition machinery. Root motion moves the entity: clips are the vocabulary, the search is the state machine.

setRootMotionEnabled(entityId: zombie, enabled: true)
setMotionMatching(entityId: zombie, descriptor: MotionMatchingDescriptor(
    leftFootPath: "root/hips/.../foot_l", rightFootPath: "root/hips/.../foot_r"))
setMotionMatchingEnabled(entityId: zombie, enabled: true)
// per frame, from AI:
setMotionMatchingGoal(entityId: zombie, desiredVelocity: steeringOutput)

Design points

  • MotionDatabase — clips resampled at 30 Hz into 27-dim character-space feature vectors: foot positions/velocities, hip velocity, and future root trajectory positions + facings at 0.33/0.66/1.0 s, loop-wrapped with the clip's per-loop root displacement so trajectories never snap at clip ends. Per-dimension std-dev normalization, per-group weights (MotionMatchingWeights). Built lazily at runtime from the entity's loaded clips — no new file format or exporter changes; a persisted database is deferred until Learned Motion Matching needs one.
  • Search — brute force with per-dimension early-out (a few thousand frames ≈ hundreds of microseconds), seeded with the currently playing frame's cost discounted 10% as hysteresis: only meaningfully better frames cause a jump, and winners near the natural playback position are skipped. This kills the pointless phase-jump churn a naive argmin produces.
  • Frame-of-reference subtlety (bug found during development): the database measures foot velocities in character space including root travel; the query must therefore use world-space finite differences rotated into the character frame. Measuring character-relative deltas instead zeroes out travel and biases every query toward idle — test-covered.
  • Trajectory prediction — first-order lag of a simulated velocity toward the goal (predictionHalflife), integrated in closed form; facing eases toward the desired facing (or the velocity direction).
  • beginAnimationTransition generalized to arbitrary target times, so jumps land mid-clip through the standard transition path with root motion re-baselined.

Stacked on #10 (feature/animation_foot_ik) — merge #10 into develop first via its own PR page, then this retargets/merges cleanly.

Testing

AnimationMotionMatchingTests (10 tests, all passing):

  • Database: frame count/layout exact; walk frames encode hip velocity 1 m/s and trajectory 0.33/0.66/1.0 m ahead; idle frames encode stillness; trajectory wraps across the clip loop without snapping
  • Search: querying a stored frame's own features returns it (or an identical twin); walk-like vs idle-like queries land in the right clips
  • End to end: a forward goal selects the walk clip and root motion moves the entity toward the goal; zeroing the goal settles on idle and stops the entity; playback stays monotonic under a constant matched goal (hysteresis); disabled by default

All six animation suites (sampler, policy, inertialization, root motion, foot IK, motion matching) pass together.

Javier Segura added 3 commits July 25, 2026 23:27
Animates characters with no animation state machine: gameplay states a
goal (desired velocity/facing, typically the steering output) and the
controller periodically searches a motion database for the frame that
best matches the current pose and the predicted future trajectory,
then jumps there through an inertialized transition. Root motion moves
the entity; clips are the vocabulary, the search is the state machine.

- MotionDatabase: clips resampled at a fixed rate (default 30 Hz) into
  27-dim character-space feature vectors — foot positions/velocities,
  hip velocity, future root trajectory positions and facings at
  0.33/0.66/1.0 s (loop-wrapped with the clip's per-loop root
  displacement). Per-dimension normalization, per-group weights.
  Built lazily at runtime from the entity's loaded clips; a persisted
  format is deferred until Learned Motion Matching needs it.
- Brute-force nearest-neighbour search with early-out, seeded with the
  currently playing frame's cost discounted 10% — hysteresis so only
  meaningfully better frames cause a jump. Winners near the natural
  playback position are skipped entirely.
- Query velocities are world-space finite differences rotated into the
  character frame, matching how the database measures them from clip
  root motion (a character-relative difference would zero out travel
  and bias every query toward idle).
- Trajectory prediction: first-order lag of the simulated velocity
  toward the goal (predictionHalflife), integrated analytically.
- beginAnimationTransition generalized to arbitrary target times so
  jumps land mid-clip through the standard transition path, with root
  motion re-baselined.
- Public API: setMotionMatching(entityId:descriptor:),
  setMotionMatchingEnabled, isMotionMatchingEnabled,
  setMotionMatchingGoal(entityId:desiredVelocity:desiredFacing:).

Docs: docs/API/UsingMotionMatching.md
Hierarchical assets loaded via setEntityMeshAsync carry their
AnimationComponent on a skinned scenegraph child while the game holds
and steers the asset root. Root motion applied its transform deltas to
the component's entity, and motion matching read heading and world
position from it — so the child drifted inside the asset along its
never-rotated local frame while the root the game turned stayed put:
characters marched off in a fixed world direction ignoring their goal,
and the goal-to-character-space conversion churned the clip selection.

Both systems now anchor to the entity the public API was called on
(setRootMotionEnabled / setMotionMatching), which is the gameplay
handle by the engine's own hierarchical-resolution convention. Flat
entities behave exactly as before (anchor == component entity).

Regression test drives a parent root with the components on a child.
@miogds
miogds changed the base branch from feature/animation_foot_ik to develop July 27, 2026 21:02
@miogds
miogds merged commit 1edf2c1 into develop Jul 27, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant