Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

  1. Sign in and pick any character.
  2. Search the animations for Walking and select a walk cycle.
  3. 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.)
  4. Download as FBX Binary with skin. The skin is optional for linting, but keeping it lets report render 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 mixamo profile resolved without any config. Mixamo is a built-in rig profile: it binds the semantic roles from Mixamo's mixamorig:* 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 take mixamo.com, and animsmith uses the take name as the clip name. Your contract config must address the clip by this name.

The mixamo profile has no Root role. Mixamo rigs have no dedicated root bone — mixamorig:Hips is the top of the hierarchy, and any root motion is baked into it. animsmith therefore judges in-place and root-motion-speed on 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.