Slice multi-take / Unity-style FBX files into individual animation clips, optionally bundle them as multi-take FBX files. Built for s&box / Source 2 ModelDoc, but the output works just as well in Unreal, Unity, or any FBX-aware tool.
Unity asset packs usually ship one long FBX timeline per file plus a .meta sidecar that defines clip ranges via firstFrame / lastFrame. Blender, and most engines, see this as one giant animation. This script reads the sidecar, slices the master action, and writes each clip out as a clean, standalone FBX or as a take inside a bundled FBX.
- Blender 4.0+
- Python (bundled with Blender — no
pip installneeded)
Auto-selected per input file:
- Unity-meta mode: if
<input>.metaexists with aclipAnimations:block, each clip'sfirstFrame/lastFrameis sliced out of the master action. - Action mode: if no usable meta sidecar is found, every action already in the FBX is exported as-is.
Force action mode with --no-meta. Override the sidecar path with --meta PATH (single-input only).
blender --background --python extract_animations.py -- [args]
Everything after -- is forwarded to the script.
| Flag | Type | What it does |
|---|---|---|
--input PATH [PATH ...] |
required | One or more source FBX files. |
--output DIR |
required | Output root directory. |
--prefix STR |
optional | String prefix for per-clip filenames. |
--armature-only |
flag | Drop meshes from exports — skeleton + animation only. |
--meta PATH |
optional | Explicit .meta sidecar path (single-input only). |
--no-meta |
flag | Ignore .meta sidecars; use action mode. |
--combined |
flag | Per source: also write <source>-Clips.fbx (every clip as a take). |
--combined-only |
flag | Skip per-clip files; only write the per-source bundle. |
--all-combined |
flag | Across all inputs: also write All-Clips.fbx (every clip as a take). |
--all-combined-only |
flag | Skip the per-source pass entirely; only write All-Clips.fbx. |
--strip-root-motion [AXES] |
flag/value | Zero out root-motion location keys on the armature object and the root bone. Optional axes string (e.g. XY, XYZ, Z). With no value, defaults to XY (strip horizontal, keep vertical for jump/bob). |
Input
Animations/
├── Defenses.fbx (one master action with 26 takes)
└── Defenses.fbx.meta (Unity sidecar with 26 clip ranges)
Command
blender --background --python extract_animations.py -- \
--input Animations/Defenses.fbx \
--output extracted
Output
extracted/
└── Defenses/
├── High-Guard.fbx
├── Low-Guard.fbx
├── Slip-Backward-Idle.fbx
└── ... (23 more)
Add --combined:
blender --background --python extract_animations.py -- \
--input Animations/Defenses.fbx \
--output extracted \
--combined
Output
extracted/
└── Defenses/
├── High-Guard.fbx
├── ... (26 individual clips)
└── Defenses-Clips.fbx (all 26 takes in one FBX)
Use --combined-only to skip the per-clip files:
blender --background --python extract_animations.py -- \
--input Animations/Defenses.fbx \
--output extracted \
--combined-only
Output
extracted/
└── Defenses/
└── Defenses-Clips.fbx (only the bundle; no individual clips)
Input
Animations/
├── Defenses.fbx + Defenses.fbx.meta (26 clips)
├── Hits.fbx + Hits.fbx.meta (6 clips)
├── Locomotion.fbx + Locomotion.fbx.meta (8 clips)
├── Punches.fbx + Punches.fbx.meta (90 clips)
├── Stance.fbx + Stance.fbx.meta (2 clips)
└── Steps.fbx + Steps.fbx.meta (8 clips)
Command (note: --input accepts multiple paths)
blender --background --python extract_animations.py -- \
--input Animations/Defenses.fbx Animations/Hits.fbx Animations/Locomotion.fbx \
Animations/Punches.fbx Animations/Stance.fbx Animations/Steps.fbx \
--output extracted \
--all-combined-only
Output (one file, 140 takes inside)
extracted/
└── All-Clips.fbx
Drop All-Clips.fbx into ModelDoc / Unity / Unreal and every clip shows up as its own animation.
Combine --combined + --all-combined for per-clip files, per-source bundles, and a mega-bundle in one run:
blender --background --python extract_animations.py -- \
--input Animations/*.fbx \
--output extracted \
--combined --all-combined
Output
extracted/
├── All-Clips.fbx (140 takes from every source)
├── Defenses/
│ ├── High-Guard.fbx
│ ├── ... (26 per-clip files)
│ └── Defenses-Clips.fbx (26-take bundle)
├── Hits/
│ ├── Hit-Head-Front.fbx
│ ├── ...
│ └── Hits-Clips.fbx
└── ... (Locomotion, Punches, Stance, Steps — same shape)
$blender = "C:\Program Files\Blender Foundation\Blender 4.4\blender.exe"
$script = "C:\path\to\fbx-clip-extractor\extract_animations.py"
$src = "C:\path\to\Animations"
$dst = "C:\path\to\extracted"
& $blender --background --python $script -- `
--input (Get-ChildItem "$src\*.fbx" | ForEach-Object FullName) `
--output $dst `
--combined --all-combinedConstants near the top of extract_animations.py control the FBX export:
| Constant | Default | Notes |
|---|---|---|
EXPORT_AXIS_FORWARD / EXPORT_AXIS_UP |
-Z / Y |
Standard FBX axes. |
EXPORT_PRIMARY_BONE_AXIS / EXPORT_SECONDARY_BONE_AXIS |
X / Z |
Required by s&box ModelDoc. |
EXPORT_ADD_LEAF_BONES |
False |
Required by s&box ModelDoc. |
EXPORT_FRAME_RATE |
30 |
Set None to keep the imported scene's fps. |
EXPORT_BAKE_STEP |
1.0 |
Sample every frame. |
EXPORT_SIMPLIFY_FACTOR |
0.0 |
0 = no key reduction (preserves mocap fidelity). |
- Clip names like
1.High-Guardfrom Unity get their leadingN.stripped automatically (becomesHigh-Guard). - The
.metasidecar is 0-indexed; Blender's import is usually 1-indexed. The script auto-detects and applies the offset. - In
--all-combinedmode, name collisions across sources get a<source>_<clip>prefix. Clean clip names always win when unique. - All inputs share the first source's armature for the bundled export; safe as long as bone names match across files (they do for any pack built on a single Mecanim humanoid rig).
Use --strip-root-motion when you want in-place clips (no character displacement). Common with engines that drive locomotion programmatically, or when blending root-motion clips causes flickering / position jumps.
# Default: strip horizontal motion only (X and Y in Blender's Z-up world)
--strip-root-motion
# Explicit axes (any combination of X, Y, Z)
--strip-root-motion XY # horizontal only (default)
--strip-root-motion XYZ # full in-place — no displacement at all
--strip-root-motion Z # only vertical (rare)
What it actually zeroes per clip:
- The armature object's
locationchannels for the chosen axes. - The root bone's
pose.bones["..."].locationchannels for the chosen axes (root = any bone with no parent).
Important nuance. Sometimes you want to keep Z (vertical) location for jumping or vertical bobbing. For walking animations you usually want to delete X and Y (horizontal motion) but might leave Z (slight up/down bob). For pure idle/attack/defense clips on a flat floor, strip all three. Test in your engine and see what looks right — the right answer is rig- and clip-dependent.
Axis names refer to Blender's Z-up world. Most game engines that consume the resulting FBX (s&box / Source 2, Unreal) treat the kept axis as their vertical, which matches what you'd intuitively expect. Unity Y-up users may need to swap which axes they strip — re-read the exported clip and check.
Verifying the strip with a re-import. If you re-import the exported FBX into Blender to check the result, the data you stripped may appear on a different array_index than expected. That's FBX's Y-up vs Blender's Z-up axis swap on round-trip: bone-local axes get re-oriented, so what was Blender Z (vertical, kept) ends up showing under array_index 1 ("Y") in the re-imported diagnostic, not array_index 2 ("Z"). The strip itself is correct — verify by inspecting the in-memory action right after slicing, or by trusting the result inside your target engine instead of round-tripping through Blender.