Ben Traje
← Back to blender

How to Batch Export Object Hierarchies to Separate Files in Blender Python

14 Aug 26 (2mo ago)

Automating Object Separation in Blender

When managing modular assets, character rigs, or game-ready meshes, you often need to save different components of a master file into separate .blend files. Doing this manually is tedious and prone to error. Using Blender's Python API (bpy), you can create a "reload loop" that isolates specific hierarchies, purges unwanted data, formats the file names, and sets the UI for the end-user.

The Challenge: Context Errors

A common approach to batch saving is to loop through a list of objects, delete the ones you do not need, and save the file. However, scripts relying on bpy.ops (the Operators API) often break with AttributeError: 'Context' object has no attribute 'object' when reloading the master file.

This happens because the Operators API relies heavily on the user interface state (viewport modes, active selections). When bpy.ops.wm.open_mainfile() reloads the scene, the UI context is momentarily wiped out, causing the script to crash.

The Solution: The Data API

To build a bulletproof script, you must bypass the UI entirely and manipulate the raw data using the Data API (bpy.data).

  • Safe Deletion: Instead of selecting objects and triggering a delete operator, build a list of unwanted objects and remove them directly using bpy.data.objects.remove(obj, do_unlink=True). This bypasses context checks completely.
  • The Reload Loop: The script operates by capturing the master file path, opening it to ensure a clean slate, processing the data, saving the new .blend file, and then reopening the master file to start the next iteration.

Managing Object Visibility

Blender tracks object visibility across three different toggles. When isolating hierarchies, you must ensure the target objects are visible for the final export.

  • View Layer (Eye Icon): Controlled via obj.hide_set(False).
  • Global Viewport (Monitor Icon): Controlled via obj.hide_viewport = False.
  • Render (Camera Icon): Controlled via obj.hide_render = False.

The Complete Batch Separation Script

The following script iterates through a predefined list of collection groups, deletes the other unneeded groups, ensures visibility, strips naming suffixes for a cleaner file name, and sets the workspace to Layout before exporting.

Prerequisite: You must save your master Blender file to a dedicated directory before running this script, as it relies on bpy.data.filepath to know where to save the isolated files.

import bpy
import os

def export_specific_groups():
    # 1. Store the master file path
    master_filepath = bpy.data.filepath
    
    if not master_filepath:
        print("ERROR: Please save your master Blender file first!")
        return

    directory = os.path.dirname(master_filepath)
    
    # 2. Define the specific groups to cycle through
    target_groups = [
        "cut_01_grp", "cut_02_grp", "cut_03_grp", 
        "cut_04_grp", "cut_05_grp", "cut_06_grp", 
        "cut_07_grp", "heart_base_grp"
    ]

    for active_group in target_groups:
        print(f"--- Exporting: {active_group} ---")
        
        # Reload the master file to reset the scene state
        bpy.ops.wm.open_mainfile(filepath=master_filepath)
        
        # Helper function to get an object and ALL its children recursively
        def get_hierarchy(obj):
            hierarchy = [obj]
            for child in obj.children:
                hierarchy.extend(get_hierarchy(child))
            return hierarchy

        # 3. Gather all objects belonging to UNWANTED groups
        objects_to_delete = []
        for group_name in target_groups:
            if group_name != active_group:
                obj = bpy.data.objects.get(group_name)
                if obj:
                    objects_to_delete.extend(get_hierarchy(obj))
        
        # 4. Delete them directly via Data API (Immune to Context Errors)
        for obj in set(objects_to_delete):
            try:
                bpy.data.objects.remove(obj, do_unlink=True)
            except Exception as e:
                print(f"Skipped deleting {obj.name}: {e}")
        
        # 5. Enforce Visibility on the ACTIVE group
        obj_to_keep = bpy.data.objects.get(active_group)
        if obj_to_keep:
            for obj in get_hierarchy(obj_to_keep):
                obj.hide_set(False)       # View Layer (Eye icon)
                obj.hide_viewport = False # Global (Monitor icon)
                obj.hide_render = False   # Render (Camera icon)

        # 6. Set UI to the 'Layout' workspace before saving
        layout_ws = bpy.data.workspaces.get('Layout')
        if layout_ws and bpy.context.window:
            bpy.context.window.workspace = layout_ws

        # 7. Format the filename and strip suffixes
        base_name = active_group
        if base_name.endswith("_grp"):
            base_name = base_name[:-4]
            
        new_filename = f"blender_sciepro_heart_anim_{base_name}_v01.1.blend"
        new_filepath = os.path.join(directory, new_filename)
        
        # Save to the new isolated file
        bpy.ops.wm.save_as_mainfile(filepath=new_filepath)
        print(f"Saved: {new_filepath}")

    # 8. Restore the original master file
    bpy.ops.wm.open_mainfile(filepath=master_filepath)
    
    # Reset UI to the 'Scripting' workspace
    scripting_ws = bpy.data.workspaces.get('Scripting')
    if scripting_ws and bpy.context.window:
        bpy.context.window.workspace = scripting_ws
        
    print("Batch separation complete! Master file restored.")

# Execute the process
export_specific_groups()