Skip to content

[Feature] Add root motion extraction and application - #1135

Open
miogds wants to merge 1 commit into
untoldengine:developfrom
miolabs:feature/animation_root_motion_upstream
Open

[Feature] Add root motion extraction and application#1135
miogds wants to merge 1 commit into
untoldengine:developfrom
miolabs:feature/animation_root_motion_upstream

Conversation

@miogds

@miogds miogds commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Next link in the animation chain after inertialized transitions (#1125): root motion. A locomotion clip authored with a traveling root previously dragged the mesh away from the entity transform and snapped it back on every loop. With root motion enabled — opt-in per entity, default off, zero cost when disabled — the root joint's horizontal translation and yaw deltas are extracted from the raw sampled pose each frame and applied to the entity transform in character space (translateBy / rotateTo), while the pose root is grounded: horizontal travel zeroed, yaw removed via swing–twist decomposition. Vertical motion, pitch, and roll stay in the pose — a crouch still lowers the character, a stagger still leans it.

API

setRootMotionEnabled(entityId: zombie, enabled: true)                              // default root = first parentless joint
setRootMotionEnabled(entityId: zombie, enabled: true, rootJointPath: "root/hips")  // rig-specific override
isRootMotionEnabled(entityId: zombie)
  • No loop snap: the clip's per-loop root displacement and yaw are precomputed as CompiledAnimationClip root metadata, and the wrapped-time jump is corrected with them.
  • Anchored to the gameplay handle: hierarchical assets (setEntityMeshAsync) carry the AnimationComponent on a skinned child while the game steers the asset root. Deltas are applied to the entity the public API was called on, so nothing drifts inside the asset. Flat entities behave exactly as before.
  • Composes with transitions: root motion runs on the raw sampled pose before transition offsets, and the transition capture grounds the incoming clip's samples — inertialized transitions blend grounded poses and never teleport the character. changeAnimation re-baselines extraction, so a clip switch contributes no spurious delta.
  • Defends against LocalTransformComponent's zero-quaternion default rotation (which rotates every vector to zero) by treating it as identity.

How-to guide: docs/API/UsingRootMotion.md. Single commit cherry-picked onto current develop (5f61789).

Testing

AnimationRootMotionTests (11 tests, all passing):

  • Off by default; enable/disable round-trip
  • Entity accumulates clip displacement across loops (exact expected travel), no snap at loop wrap
  • Pose root grounded (horizontal zeroed, vertical kept); yaw accumulates on the entity and is stripped from the pose
  • Transition mid-walk blends grounded poses; clip switch does not teleport the entity
  • Hierarchical asset: APIs called on the parent root move the parent, child never drifts
  • Root joint path override

Inertialization, compiled-sampler, and policy suites still pass on this base. SwiftFormat lint clean.

Locomotion clips authored with a traveling root previously dragged the
mesh away from the entity transform and snapped back on loop. With
root motion enabled (opt-in per entity, default off), the root
joint's horizontal translation and yaw deltas are extracted from the
raw sampled pose each frame and applied to the entity transform in
character space via translateBy/rotateTo; the pose root is grounded
(horizontal zeroed, yaw removed via swing-twist decomposition).
Vertical motion, pitch, and roll stay in the pose.

- Loop wrap corrected with the clip's precomputed per-loop root
  displacement and yaw (CompiledAnimationClip root metadata) — no
  backward snap when the clip wraps
- setRootMotionEnabled(entityId:enabled:rootJointPath:) resolves
  hierarchical assets like the other animation APIs; the root defaults
  to the skeleton's first parentless joint, overridable by joint path
- Deltas anchor to the entity the public API was called on (the
  gameplay handle): hierarchical assets carry the AnimationComponent
  on a skinned child, and applying deltas there would drift the child
  inside the asset while the root the game steers stays put
- Runs on the raw sampled pose before transition offsets, and the
  transition capture grounds the incoming clip's samples, so
  inertialized transitions blend grounded poses and never teleport the
  character
- Defends against LocalTransformComponent's zero-quaternion default
  rotation, which rotates every vector to zero

Docs: docs/API/UsingRootMotion.md
@miogds
miogds requested a review from untoldengine as a code owner August 6, 2026 07:33
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