Animation
Awaken plays skeletal animation on imported characters and grows all the way up to a full locomotion system: crossfaded clips, a param-driven state graph, cross-rig retargeting, root motion that drives movement, foot IK, ragdolls, bone attachments, and cloth. It all runs on a CPU skinning pipeline that samples a clip, poses the skeleton, and re-uploads the deformed mesh each frame - and it all ships in the exported game unchanged.
The short version
For a hero or NPC you drop three things on the character: an Animator (playback), an AnimStateMachine (the locomotion graph), and a Script that drives it. The starter Third Person Controller wires all of this for you - see the tutorial.
The model
Awaken animation is skeletal (bone-based) and CPU-skinned. The moving parts:
- A skeleton - a bone hierarchy with bind poses and inverse-bind matrices.
- Animation clips - keyframed channels that drive bones over time.
- The Animator component - per-object playback state (which clip, how far into it, crossfade in flight, play window, root-motion output).
- The AnimStateMachine component (optional) - a param-driven graph that chooses which clip the Animator plays.
Skeletons and clips are shared assets addressed by id; they arrive through the import pipeline alongside the character mesh. The Animator is the small bit of state the animation system advances each frame - it serialises and shows in the Inspector like any other component.
The defining design choice: skinning runs on the CPU. Each frame the system deforms the mesh's vertex positions and uploads the result as an ordinary mesh. The renderer never sees bones, weights, or a special skinned vertex format - it draws a plain mesh that happens to change shape. This keeps the render pipeline simple and its instancing/static-batching paths untouched; a GPU skinning fast path could be layered on later without changing these types.
The Animator component
The Animator holds playback state. Most fields are runtime-only (hidden in the Inspector because they tick every frame); the ones you author are skeleton and speed, plus clip/loop when no state machine is driving it.
| Field | Type | Default | Meaning |
|---|---|---|---|
clip | asset ref | "" | Animation-clip id to play ("" = none). Hidden when an AnimStateMachine owns it. |
skeleton | asset ref | "" | Skeleton this object is skinned to ("" = none). |
speed | number | 1 | Playback rate; 1 = normal, negative plays in reverse. |
loop | bool | true | Wrap at the end vs. clamp and stop. Hidden when a state machine owns it. |
playing | bool | true | Advance the clock, or hold on the current frame. |
time | number | 0 | Seconds into the clip (runtime). |
fadeClip / fadeTime / fadeDuration / fadeElapsed | - | - | Crossfade state (runtime). While fadeClip != "" the system samples both clips and blends current → fade by fadeElapsed / fadeDuration, then commits. |
clipStart / clipEnd | number | 0 / 1 | Play window as normalized [0,1] fractions of the clip - playback loops/clamps within a slice instead of the whole clip. fadeStart/fadeEnd mirror them for the incoming clip. |
rootDx / rootDz | number | 0 | Root motion output (runtime) - the clip's horizontal displacement this frame in character-local space (read via api.rootMotion()). |
flatRootY | bool | false | Vertical hand-off (runtime) - when true the system flattens the clip's baked pelvis rise out of the pose and skips foot IK, so a physics jump owns the vertical. Flipped by api.setRootMotionVertical(false). |
Playback time is advanced by pure clock helpers (advanceTime, or advanceTimeRange when a play window trims the clip to a slice). Looping wraps within the window in either direction; non-looping clamps so you can detect the end. Because the system mutates time (and the other runtime fields) in place, stopping Play restores everything from the pre-play snapshot.
📸 Screenshot - save as img/media-animator-inspector.png
The Inspector showing an Animator and an AnimStateMachine on an imported character - the skeleton and speed fields, the graph picker, and the per-character clip bindings.
Crossfade blending
Direct playback is one call:
ts
api.playClip("run", { fade: 0.15 }); // crossfade from the current clip over 0.15 splayClip(clip, { fade, speed, loop, start, end }) sets up a crossfade the animation system then resolves: it samples both the outgoing and incoming clips and blends the poses with the core blendPoses function, ramping the weight over fade seconds before committing to the new clip. A fade of 0 (or no clip currently playing) switches instantly. The call is idempotent - re-requesting the clip already playing (or already fading to) is a no-op, so a state machine can call it every frame without stutter.
The skinning pipeline
Each frame, for every object with an enabled Animator that has a clip, skeleton, and skin data, the animation system (in @awaken/runtime) runs the pipeline below. The heavy lifting - sampling, kinematics, skinning, blending, foot IK (steps 2–6) - is pure, GPU-free math in @awaken/core, so it is fully unit-testable without a renderer; the clock is an equally pure runtime helper, and only the final upload touches the GPU.
- advance the clock - tick
timebydt · speed(only ifplaying), within the play window. samplePose- sample the clip attimeinto a per-bone local TRS pose. Bones a clip does not drive keep their bind pose. Translation/scale channels lerp; rotation channels use shortest-arc spherical interpolation. Sampling clamps at both ends of a channel's keys - no extrapolation. Interpolation islinearorstep; glTFcubicsplineis deferred and sampled as linear.blendPoses(only during a crossfade) - blend the outgoing and incoming local poses by the current fade weight.poseMatrices- forward kinematics up the hierarchy, turning local TRS into per-bone world matrices. Parents are resolved on demand with a cache, so any joint ordering works (glTF does not guarantee parents precede children) and cyclic skeletons are guarded.skinningMatrices- multiply each world matrix by the bone's inverse-bind matrix, producing the matrices linear-blend skinning uses.applyFootIK(if grounded and enabled) - 2-bone leg IK + pelvis adjust so planted feet meet the ground (see Foot IK).skinPositions- linear blend skinning on the CPU: for each vertex, blend up to four bone matrices (glTF's four influences) weighted by the vertex's skin weights. The rest-pose base positions are read-only; a fresh deformed array is returned.- upload - hand the deformed positions to the renderer, which rewrites that mesh's vertex buffer in place.
Where clips and skeletons come from
You do not author skeletons or clips inside Awaken - they come from imports. When you import a rigged character, its skeleton, skin binding (joints + weights + bind-pose positions), and any clips are registered as assets, and the character's Animator is pointed at them. Imported characters are posed from a bundled idle clip so they stand naturally rather than freezing at bind pose. See Colliders & Skeletons for the import side.
Cross-rig retarget (humanoid)
A clip authored for one rig can play on a different character. On import Awaken builds a humanoid avatar for each rig: it maps that skeleton's bones onto standard humanoid slots (hips, spine, upper/lower arms, legs, head…) via a rig profile, derives an auto-T-pose, and builds a geometric frame-map between the source and target skeletons. A retarget then replays the source clip's motion through the target's proportions - so one animation library dresses many characters, whatever tool authored the rigs.
Animation-pack import
You can import a whole animation pack - hundreds of FBX clips - before any character exists. The pack imports to rig-space clips (retarget-ready), and you bind them per character later. This decouples "collect a motion library" from "rig a specific hero", and lets the same pack feed every NPC. See the FBX / animation import notes.
The animation state graph
Anything past a clip or two runs through an AnimStateMachine - an authored graph of states and transitions, driven entirely by params. You never call playClip for locomotion; you set params and the graph picks the clip and crossfade.
- States name a logical clip (
"walk"), aloopflag, an optionalspeed, and an optional play window (start/end). - Transitions carry a
fromstate (or"*", the any-state wildcard), a target, a list of declarative conditions on params (==,!=,<,>,<=,trigger), and afadetime. The first firing transition wins each frame. - Triggers are one-shot params (
jump) that auto-reset when a transition consumes them.
Gameplay drives it from a script:
ts
onUpdate(api: ScriptApi) {
api.setAnimParam("speed", groundSpeed); // idle ⇄ walk ⇄ run by speed band
api.setAnimParam("grounded", grounded);
if (jumpPressed) api.setAnimTrigger("jump"); // one-shot
}setAnimParam/setAnimTrigger find the nearest AnimStateMachine on the entity or a child, so a controller on a parent capsule collider drives the machine that lives with the skinned child mesh. Each frame the graph system takes the first firing transition and crossfades the Animator to the target state's clip via playClip.
Per-character clip binding
The graph's states name clips logically ("walk", "run"); each character maps those names to its own baked clip ids through AnimStateMachine.clips. So one graph is authored once and reused across the hero and every NPC - each binding its own animation set. A logical name with no binding falls back to being treated as a direct clip id.
The Anim Graph editor tab
Graphs are edited visually in the editor's Anim Graph tab - lay out states, wire transitions, set conditions and fades. Graphs persist as .awakenanimgraph assets. There is also a built-in builtin:locomotion (LOCOMOTION_GRAPH) that is always available with no import, and a starter Third Person Locomotion graph (tps-locomotion) with idle/walk/run, three jump variants, short/long fall, soft/hard land, and get-up states wired up.
📸 Screenshot - save as img/media-anim-graph-tab.png
The Anim Graph tab showing the Third Person Locomotion graph - states laid out with any-state transitions into jump, fall, land, and the idle/walk/run speed bands.
Root motion
Many locomotion clips move the character forward in the animation (a walk cycle strides ahead). Root motion lets that authored motion drive the character, so the feet don't slide.
- Horizontal - the animation system writes the clip's per-frame character-local displacement into
rootDx/rootDz. A controller reads it withapi.rootMotion()→{ dx, dz }, rotates it by the character's facing, and feeds it intomoveAndSlide. The animation is authoritative over ground movement (no foot slide). An in-place clip returns{ 0, 0 }. - Vertical - the animation owns the character's vertical by default (a clip's pelvis bob and crouch are kept, feet planted). When your controller owns the vertical instead - a physics jump - call
api.setRootMotionVertical(false)for that frame. The system then flattens the clip's baked pelvis rise out of the pose and skips foot IK, so neither stacks on your jump. The typical pattern isapi.setRootMotionVertical(!airborne)each frame.
ts
onUpdate(api: ScriptApi, dt: number) {
const rm = api.rootMotion(); // clip's local horizontal delta
const [wx, wz] = rotateByFacing(rm.dx, rm.dz, facing);
api.setRootMotionVertical(!airborne); // physics owns vertical mid-air
const { grounded } = api.moveAndSlide(wx, vy * dt, wz);
}Preview arrow
When you preview a clip in the Inspector, a ground arrow under the character shows that clip's root-motion direction and magnitude, with its speed in m/s beside it. It's the quick way to tell whether a clip travels (and how fast) - an in-place clip shows no arrow; a big run-leap shows a long one.
📸 Screenshot - save as img/media-rootmotion-arrow.png
The inspector's clip preview on a character, with the cyan root-motion arrow on the ground pointing along the walk/run direction and the m/s readout beside it. Ideally two side by side: a walk (short arrow) and a run (long arrow).
Foot IK
When grounded, the animation system runs a shared foot-IK solver (@awaken/core): 2-bone leg IK re-plants each foot on the ground, a pelvis adjust keeps the hips consistent, a plant weight blends the correction in only for a foot that should be planted, and a foot-flatten step levels the sole. It grounds planted feet on uneven or authored surfaces without foot float. Foot IK is skipped whenever flatRootY is set (i.e. while airborne), since a tucked jump pose should not be dragged down to the ground plane. It runs in the editor's Play mode and in the shipped player identically.
Ragdoll
api.ragdoll(vx?, vy?, vz?) switches a skinned character from animation to physics. The system hands the current posed skeleton to physics, which spawns a dynamic capsule per major humanoid bone linked by ball joints, and then each frame reads those bodies back into the bone matrices - so the mesh skins to the physics pose. The character crumples, collides with the world, and flops. An optional seed velocity is inherited by every body, so a fall's momentum carries through.
The Ragdoll component's active flag persists (a downed character stays a ragdoll across a save). This feature is experimental - joint limits are generous. The starter third-person controller uses it for a fatal-fall crumple, then plays a get-up clip to recover.
Ragdoll needs physics
Ragdoll is only active when a physics backend is present (it is, in Play and in a shipped game). Without physics the call is a no-op. See Physics.
📸 Screenshot - save as img/media-ragdoll.png
A character mid-crumple after a fatal fall - limbs limp and folded on the ground from the physics ragdoll, distinct from any animated pose. A motion-blurred or mid-fall frame reads best.
Bone anchors & attachment
A BoneAnchor component makes an entity a live mirror of one named skeleton bone: each frame the animation system writes that bone's animated local pose into the entity's Transform. Anything parented under the anchor - a sword under "RightHand", a hat under "Head", a cape under "Neck" - then rides the animation through ordinary parent-transform composition, no special attach logic. The editor's Expand skeleton action spawns one anchor per bone mirroring the bone hierarchy; to attach a prop, drop it under the matching bone anchor in the Hierarchy. bone is the bone name (stable across re-rigs, unlike an index).
📸 Screenshot - save as img/media-bone-attachment.png
Split view: the Hierarchy with a character's skeleton expanded into bone anchors and a sword nested under the hand bone, and the Viewport showing that sword held in the character's hand as it animates.
Cloth
Cloth is a starter script, not a built-in engine subsystem - a Verlet / position-based simulation that runs on a mesh's own vertices. It is built entirely on the two script primitives api.meshData() (the entity's rest vertices + indices) and api.deformMesh() (replace this frame's positions; the engine recomputes normals and uploads). The script welds coincident vertices into particles, pins a top band to the object, and lets the rest fall and swing under gravity - so a cape parented under a character's "Neck" bone anchor hangs, swings, and trails as the animation moves.
Because it is just a script over the mesh-deform API, feel is tuned with plain knobs (gravity, damping, max speed, stiffness, solver iterations, pin band). Give a cloth entity its own mesh id - deforming is keyed by mesh id, so a shared id would warp every copy. See the Cloth starter script in starter-content/game/.
📸 Screenshot - save as img/media-cloth-cape.png
A caped character mid-stride, the cloak billowing and trailing behind from the Verlet cloth sim - a frame where the cloth is clearly deformed away from rest (turning or running) sells the motion.
Playing in the editor - and in the shipped game
The animation system, graph system, and foot IK advance Animators and re-upload deformed meshes while Play is running, so skinned characters animate live in the Game view. In edit mode they rest at their imported (idle-baked) pose. Enter Play to preview motion; playback state resets when you stop.
It all ships. Skeletons, baked clips, skins, and animation graphs ride the self-contained .awaken.json and the game.html container, and the exact same animationSystem + animGraphSystem
- foot IK run in the standalone player runtime. Animated characters animate in the export identically to editor Play - edit == play == ship.
Because skinning is per-vertex CPU work uploaded every frame, its cost scales with the number of animated characters on screen (it is not capped by visibility). To keep the frame fast, an animating character does not trigger a scene cache rebuild - its culling bound stays at the rest-pose radius, a fine conservative bound for a character. Animation is best used on a modest number of hero/character objects rather than thousands of props.
The Third Person Controller (starter)
The fastest way to a moving, animated character is the starter set:
- Third Person Controller (
.awakenscript) - camera-relative WASD, turn-to-face-movement, Shift to run, Space to jump, a spring-arm orbit camera, root-motion movement, and a fatal-fall ragdoll + get-up. - Third Person Locomotion (
.awakenanimgraph) - the locomotion graph the controller drives viaspeed/grounded/jump/fall/land/getUpparams. - Third Person Player (
.awakenprefab) - the assembled character (skinned mesh + Animator + AnimStateMachine + capsule Collider + the controller script).
Attach the controller to the character root, assign a root camera (not parented under the character), add an AnimStateMachine with the locomotion graph, and bind the character's clips. See the Third-Person Controller tutorial for the full walk-through.
Limitations (be honest)
- CPU skinning only. There is no GPU skinning path yet; the deformed mesh is re-uploaded each frame.
- Four bone influences per vertex (the glTF default).
cubicsplineinterpolation is deferred and sampled as linear.- No blend spaces / speed-matched locomotion yet. Locomotion crossfades between discrete states by speed band; there is no continuous velocity-blended blend space. (On the roadmap.)
- Ragdoll is experimental - joint limits are generous rather than anatomically tuned.
See also
- Physics: Character Controller -
moveAndSlide, the capsule mover root motion feeds. - Scripting: ScriptApi Reference -
playClip,setAnimParam,rootMotion,ragdoll,meshData/deformMesh. - Tutorial: Third-Person Controller - build a moving, animated character end to end.
- Import: Colliders & Skeletons - where skeletons, skins, clips, and animation packs come from.
- Inspector - editing the Animator and AnimStateMachine fields.
- The Player Runtime - the shipped runtime that animates your export.
- API: Core - the animation math (
samplePose,blendPoses,skinPositions,applyFootIK, …).