Mixamo to game-ready: an end-to-end tutorial
Take one free Mixamo download through the whole pipeline: download, convert to glTF, inspect the rig, lint, fix what is mechanically fixable, and grow a project contract config from the results.
This is the bring-your-own-asset counterpart to the examples cookbook: the cookbook runs against small assets committed to this repo, while this tutorial runs against a real marketplace rig you download yourself. Each step links to the page that owns its why rather than re-explaining — what game-ready means for everything the checks judge, and the pipeline scenario guide for acquiring and handling the raw asset.
The complete key, default, and precedence reference for the contract used below is the configuration reference.
About the transcripts. The command output in this tutorial is
real, captured from a small generated rig that carries the same
mixamorig:* bone names, the same nine profile roles, and the same
mixamo.com take name as a real download (this repo does not ship
Mixamo's own files — see the
asset policy). Your download
will show many more bones — fingers, twist joints — and possibly more
findings; the roles, the commands, and the behavior are the same.
Excerpts marked real download were captured once (July 2026) from
Mixamo's default Walking; unlike the stand-in transcripts they are not
regenerated by this repo's tests, so treat their exact numbers as a
snapshot.
Build requirements. convert (step 2) ships in the default
install — it sits behind the fbx feature, which is enabled unless
you opted out. If you installed with --no-default-features, use the
default build for this one step (see
feature flags). Everything from step 3 on
works in any build.
1. Download a clip from Mixamo
Mixamo is free with an Adobe ID, and Adobe documents its animations as royalty-free for personal and commercial projects — see Adobe's Mixamo FAQ for the current terms. That covers using the assets in your project; it does not clearly permit redistributing the downloaded files, so keep them out of public repos and do not commit them here.
- Sign in and pick any character.
- Search the animations for Walking and select a walk cycle.
- Check the In Place option. Mixamo's default walks travel through the world; In Place keeps the hips over the origin, which is the shape most capsule-driven character controllers expect. (If your game uses root motion, download without In Place — step 6 shows how the contract differs.)
- Download as FBX Binary with skin. The skin is optional for
linting, but keeping it lets
reportrender the mesh later.
We recommend keeping the downloaded FBX as your immutable raw source and treating every animsmith output as a derived artifact — the pipeline scenario guide explains that raw-vs-generated split.
2. Convert to glTF
$ animsmith convert Walking.fbx -o walking.glb # real download
wrote walking.glb (70 node(s), 1 clip(s), 2 mesh(es) / 28374 position(s), 2 material(s)); dropped 1 clip(s) with no writable tracks
(Counts vary by character. This download contains a second take named
Take 001 with zero tracks and keys. Conversion omits that empty take,
writes the animated mixamo.com clip, and reports the omission. The
licensed download is not committed here.)
animsmith lints FBX directly, so conversion is not required to run
checks — but glTF/GLB is the native format for the rest of the
pipeline (fix, transform, and diff operate on glTF), and a
convert-once step gives later commands a stable baseline to compare
against. A conversion that succeeds proves the container is
well-formed, not that the motion is usable —
a valid file is not a usable clip
— which is what steps 4–6 judge.
3. Inspect the rig
$ animsmith inspect walking.glb
walking.glb
rig profile: mixamo (9 roles)
hips -> mixamorig:Hips (exact)
spine -> mixamorig:Spine (exact)
head -> mixamorig:Head (exact)
left_foot -> mixamorig:LeftFoot (exact)
right_foot -> mixamorig:RightFoot (exact)
left_toe -> mixamorig:LeftToeBase (exact)
right_toe -> mixamorig:RightToeBase (exact)
left_hand -> mixamorig:LeftHand (exact)
right_hand -> mixamorig:RightHand (exact)
skeleton: 9 bones
mixamorig:Hips
mixamorig:LeftFoot
mixamorig:LeftToeBase
mixamorig:RightFoot
mixamorig:RightToeBase
mixamorig:Spine
mixamorig:Head
mixamorig:LeftHand
mixamorig:RightHand
materials: 0
mesh instances: 0
clips: 1
mixamo.com: 1.000s, 2 tracks, 33 keys max
These are the same nine roles a real download resolves; your skeleton list will just be longer. From the real download (abridged):
$ animsmith inspect walking.glb # real download
walking.glb
rig profile: mixamo (9 roles)
...the same nine-role table as above...
skeleton: 70 bones
<fbx-root>
mixamorig:Hips [skinned]
mixamorig:Spine [skinned]
...
materials: 2
...the exact quoted source material names...
mesh instances: 2
node "<first mesh node name>"
...mesh, skin, and primitive/material context...
node "<second mesh node name>"
...mesh, skin, and primitive/material context...
clips: 1
mixamo.com: 1.033s, 156 tracks, 32 keys max
The extra bones — fingers, twist joints, end markers — carry no role,
and the two mesh nodes ride along for report playback.
Two things to read off this output:
- The
mixamoprofile resolved without any config. Mixamo is a built-in rig profile: it binds the semantic roles from Mixamo'smixamorig:*bone names, which is what arms the role-based checks in step 6. Without a resolved rig, those checks skip rather than guess (see the symptom index). - The clip is named
mixamo.com. Mixamo names every export's animation takemixamo.com, and animsmith uses the take name as the clip name. Your contract config must address the clip by this name.
The
mixamoprofile has no Root role. Mixamo rigs have no dedicated root bone —mixamorig:Hipsis the top of the hierarchy, and any root motion is baked into it. animsmith therefore judgesin-placeandroot-motion-speedon the Hips track for Mixamo rigs: travel measurements still work, but they describe hip movement, not a separate locomotion root. If your engine workflow retargets hip motion onto a dedicated root bone (a common Unreal step), that happens downstream in your DCC or importer — lint the export you actually ship to the engine.
4. Lint the mechanical checks
$ animsmith lint walking.glb
walking.glb:
coverage[bind-pose] first_frame_rest_delta 'mixamo.com': insufficient_rotation_evidence: ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s)
With no config, expectation-driven checks are not applicable. Mechanical
checks still evaluate, as does bind-pose; this analytic fixture has no
rotation tracks, so bind-pose records a nonblocking coverage gap rather than
claiming a clean evaluation.
Findings like constant-track notes are normal on a marketplace rig.
From the real download (abridged):
$ animsmith lint walking.glb # real download
walking.glb:
warning[scale-keys] clip 'mixamo.com' bone 'mixamorig:Hips': scale animation present ...
note[constant-track] clip 'mixamo.com' bone 'mixamorig:Spine': translation track has 2 keys but never moves — export bloat
...
0 error(s), 1 warning(s), 103 note(s), 1 coverage gap(s)
Of the clip's 156 tracks, 103 never move — pure export bloat — and the hips carry a scale track worth checking before retargeting. Nothing here is broken motion; the serious mechanical defects — flicker-inducing quaternion problems — tend to creep in later, after the asset has been through your DCC, a retargeter, or an exporter plugin.
5. Fix what is mechanically fixable
$ animsmith fix walking.glb --dry-run
0 key(s) would be fixed across 0 track(s) -> no output written
0 key(s) would be fixed across 0 track(s) -> no output written
(One summary line per default repair — quat-norm, then quat-flip.)
fix applies only repairs that are provably lossless: quat-norm and
quat-flip, the representation defects behind
the pose flickers, spins, or explodes.
That makes it safe to run unconditionally; on a clean export it is a
no-op, as here. When a re-export does pick up defects:
$ animsmith fix walking.glb -o walking-fixed.glb
$ animsmith lint walking-fixed.glb
$ animsmith diff walking.glb walking-fixed.glb
The re-lint proves the findings are gone; the diff proves the
repair moved no measured motion. The cookbook's
repair section shows the
full transcript of that loop on a committed dirty asset.
6. Grow the contract config
The mechanical checks passing does not make the clip game-ready — a
walk whose loop pops or whose feet skate is mechanically pristine (see
a valid file is not a usable clip).
The semantic checks need your expectations declared. Start from what
measure reports. The shortened JSON below is illustrative; capture and retain
the exact numbers from your own downloaded asset rather than treating these
sample values as vendor evidence:
$ animsmith measure --format json walking.glb
{
"schema_version": 19,
"schema": "urn:animsmith:schema:output:19",
"tool": { "name": "animsmith", "version": "0.14.0",
"source": { "revision": null, "dirty": null } },
"command": "measure",
"summary": { "files": 1 },
"files": [
{
"path": "walking.glb",
"input": {
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"bytes": 123456
},
"rig": { "profile": "mixamo", "resolution_outcome": "resolved",
"resolved_roles": { "hips": "mixamorig:Hips", "spine": "mixamorig:Spine",
"left_foot": "mixamorig:LeftFoot", "right_foot": "mixamorig:RightFoot" },
"resolved_role_policies": { "hips": "exact", "spine": "exact",
"left_foot": "exact", "right_foot": "exact" } },
"measurements": {
"schema_version": 18,
"schema": "urn:animsmith:schema:measurements:18",
"clips": { "mixamo.com": {
"duration_s": 1.0, "frame_count": 33,
"animated_bones": ["mixamorig:Hips", "mixamorig:LeftFoot", "mixamorig:RightFoot"],
"bone_channels": [
{ "bone_index": 0, "bone_name": "mixamorig:Hips",
"availability": "measured",
"properties": ["translation", "rotation"] },
{ "bone_index": 1, "bone_name": "mixamorig:LeftFoot",
"availability": "measured",
"properties": ["translation"] },
{ "bone_index": 2, "bone_name": "mixamorig:RightFoot",
"availability": "measured",
"properties": ["translation"] }
],
"bone_rotation_range_deg": { "mixamorig:Hips": 3.2 },
"loop_continuity": { "bones": [
{ "bone_index": 0, "bone_name": "mixamorig:Hips",
"position_delta_m": 0.0, "rotation_delta_deg": 0.0,
"seam_velocity_delta_mps": 0.0,
"seam_angular_velocity_delta_degps": 0.0 },
{ "bone_index": 1, "bone_name": "mixamorig:LeftFoot",
"position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
"seam_velocity_delta_mps": 0.0,
"seam_angular_velocity_delta_degps": 0.0 },
{ "bone_index": 2, "bone_name": "mixamorig:RightFoot",
"position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
"seam_velocity_delta_mps": 0.0,
"seam_angular_velocity_delta_degps": 0.0 }
] },
"loop_continuity_availability": "measured",
"loop_endpoint_mode_availability": "not_applicable",
"frame_grid_availability": "not_applicable",
"loop_seam_ratio": 1.2e-15,
"loop_seam_ratio_availability": "measured",
"gait": { "phase": 0.75, "phase_availability": "measured", "lr_amplitude_m": 0.2 },
"gait_availability": "measured",
"root_trajectory": {
"bone_index": 0, "bone_name": "mixamorig:Hips",
"source_role": "hips_fallback",
"translation": {
"horizontal_displacement_x_m": 0.0,
"horizontal_displacement_z_m": 0.0,
"horizontal_travel_m": 0.0,
"vertical_displacement_m": 0.0,
"vertical_min_displacement_m": 0.0,
"vertical_max_displacement_m": 0.0
},
"translation_availability": "measured",
"yaw": {
"heading_axis": "positive_z", "net_yaw_deg": 0.0,
"unwrapped_yaw_deg": 0.0, "yaw_travel_deg": 0.0
},
"yaw_availability": "measured"
},
"root_trajectory_availability": "measured",
"speed_mps": 0.0,
"speed_mps_availability": "measured"
} },
"mesh_definitions": [],
"node_instances": [],
"scenes": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"material_resource_coverage": "complete",
"material_definitions": [],
"textures": [],
"images": []
}
}
]
}
(Abridged — all nine roles resolve; head, toes, and hands are elided
here.) The numbers become the contract:
near-zero per-bone loop deltas and loop_seam_ratio say the cycle closes in
pose, velocity, and locomotion phase; speed_mps of zero confirms the In Place
download, and the gait numbers seed a
blend-ring group
once you have more than one direction. Declare what must stay true by
saving this as animsmith.toml next to your clip — animsmith
auto-loads it from the working directory:
[rig]
profile = "mixamo"
[clips."mixamo.com"]
loop = true
movement_owner_xz = "gameplay"
[checks.loop-seam]
max_ratio = 1.6
[checks.loop-closure]
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0
[checks.loop-seam-vel]
max_velocity_delta_mps = 0.1
[checks.loop-seam-rot]
max_angular_velocity_delta_degps = 5.0
The same contract is committed as
examples/mixamo.animsmith.toml
for reference (from a repo checkout, pass it with
--config examples/mixamo.animsmith.toml instead of saving a local
copy). It pins the profile rather than trusting auto-detection, declares
the clip a loop (arming the loop checks) and gives XZ movement to gameplay
(arming in-place,
judged on the Hips track per the callout above). Every key, glob
pattern, and severity override is documented in the
configuration reference.
Lint against the contract:
$ animsmith lint walking.glb
walking.glb:
coverage[bind-pose] first_frame_rest_delta 'mixamo.com': insufficient_rotation_evidence: ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s) # exits 0
And this is what a violation looks like — the same contract against a copy whose cycle was cut short, the classic popped loop:
$ animsmith lint walking-popped.glb
walking-popped.glb:
error[loop-closure] clip 'mixamo.com' bone 'mixamorig:RightFoot' @1.000s:
loop does not close in position (measured 0.1581, expected 0.0100)
error[loop-seam] clip 'mixamo.com' @1.000s: loop seam pops: wrap discontinuity
is 6.82× the neighbouring in-clip step (cap 1.60) — the clip does not
close its cycle (measured 6.8152, expected 1.6000)
error[loop-seam-vel] clip 'mixamo.com' bone 'mixamorig:RightFoot' @1.000s:
loop velocity changes at the seam (measured 0.7972, expected 0.1000)
3 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s) # exits 1
The contract also catches downloading the wrong variant. The real download above is Mixamo's default Walking — the traveling one — and under this In Place contract it fails exactly as it should:
$ animsmith lint walking.glb # real download
walking.glb:
error[in-place] clip 'mixamo.com': declared in-place but the root travels
at 1.79 m/s — the character will glide at runtime (measured 1.7857,
expected 0.0000)
...
For a root-motion download like that one, give XZ ownership to the animation
and add a speed pin, using the speed_mps that measure
reported as the starting value:
[clips."mixamo.com"]
loop = true
movement_owner_xz = "animation"
speed_mps = { value = 1.8, tolerance = 0.15 }
Older configs may spell XZ gameplay/animation ownership as in_place = true
or false. That alias remains supported, but one selector entry cannot declare
both in_place and movement_owner_xz.
Under that contract the real download passes the loop and speed pins and surfaces genuine content findings instead (abridged):
$ animsmith lint walking.glb # real download
walking.glb:
warning[foot-slide] clip 'mixamo.com' bone 'mixamorig:LeftFoot' @0.333s:
left foot skates during stance: speed deviates 0.52 m/s from the
expected 0.00 m/s (cap 0.30) — foot plants will slip at runtime ...
warning[foot-slide] clip 'mixamo.com' bone 'mixamorig:RightFoot' @0.833s: ...
0 error(s), 3 warning(s), 103 note(s), 0 coverage gap(s) # exits 0
Whether that stance drift is acceptable for your game is exactly the
kind of decision the contract records — tighten the cap, accept it
with --allow, or send the clip back for cleanup (see
feet slide within one clip).
From here the contract grows with your project: rename clips as you
import more animations, add [clips."run_*"] globs, speed pins, and
gait_groups blend rings as the locomotion set fills out — the
cookbook's
project contract section
and the worked
character.animsmith.toml
show that full shape. Commit animsmith.toml next to your assets and
every bare animsmith lint — local or
in CI — enforces it.
Where to go next
What game-ready means explains every failure mode these checks catch; everything else — the cookbook, the CLI reference, embedding — is routed from all pages.