wrote a Blender script to batch-export NLA strips as individual FBXs (exporting animations one by one is torture)

265 views 1 reply

The setup: character rig with ~35 animations in the NLA editor, engine team needs individual FBX files per animation. Every session I was doing the same manual dance: solo the strip, adjust the frame range, export FBX, unsolo, repeat. Took maybe 10–15 minutes per pass and I was exporting multiple times a day during polish.

Wrote a script that handles the whole thing:

import bpy
import os

def export_nla_strips_as_fbx(output_dir, objects=None):
    if objects is None:
        objects = [o for o in bpy.context.selected_objects if o.type == 'ARMATURE']

    os.makedirs(output_dir, exist_ok=True)
    scene = bpy.context.scene

    for arm in objects:
        if not arm.animation_data or not arm.animation_data.nla_tracks:
            continue

        tracks = arm.animation_data.nla_tracks

        for track in tracks:
            for strip in track.strips:
                # Mute everything, unmute just this track
                for t in tracks:
                    t.mute = True
                track.mute = False

                scene.frame_start = int(strip.frame_start)
                scene.frame_end = int(strip.frame_end)

                filepath = os.path.join(output_dir, strip.name + '.fbx')

                bpy.ops.export_scene.fbx(
                    filepath=filepath,
                    use_selection=True,
                    object_types={'ARMATURE', 'MESH'},
                    bake_anim=True,
                    bake_anim_use_all_actions=False,
                    bake_anim_use_nla_strips=True,
                    bake_anim_step=1.0,
                    bake_anim_simplify_factor=0.0,
                    add_leaf_bones=False,
                )
                print('Exported: ' + filepath)

        # Restore: unmute all tracks when done
        for t in tracks:
            t.mute = False

export_nla_strips_as_fbx('/path/to/output/')

A few caveats:

  • Exit tweakmode before running. Behavior gets weird if a strip is being edited when this fires.
  • Doesn't handle stacked strips on the same track (rare, but it happens), so those'll export together
  • bake_anim_simplify_factor=0.0 is intentional. I'd rather ship full curves and let the engine's import settings handle compression.

Saves me a lot of time. The part I'm least happy with is the track muting approach. It feels fragile, and writing to scene.frame_start/frame_end as a side effect is annoying if you're running this mid-session. Anyone found a cleaner way to isolate individual NLA strips for export without touching global scene state?

one thing that bit me with NLA batch exports: the strip's frame_start/frame_end in NLA space don't always match the action's actual keyframe range. if the strip's been scaled or offset in the NLA editor, you'll bake extra padding frames into the export and they show up as blank time at the start or end of the animation on the engine side.

worth deriving the range from the action's fcurves directly:

start = min(k.co.x for fc in action.fcurves for k in fc.keyframe_points)
end = max(k.co.x for fc in action.fcurves for k in fc.keyframe_points)
bpy.context.scene.frame_start = int(start)
bpy.context.scene.frame_end = int(end)

saved a lot of confusion when animations were consistently landing a few frames too long and nobody could figure out why.

Moonjump
Forum Search Shader Sandbox
Sign In Register