1
$\begingroup$

I'm new to writing addons for Blender and also to writing code in python, most of this is 'snippets' I found elsewhere, so I have little understanding of what this code is actually doing, beyond what it's supposed to be:

It opens a 'Save File' dialog (not useable in this case) which just results in 'access denied'
The purpose of the script is to find every object in every scene, and while preserving object hierarchies to export each as a separate FBX file.

The entire Traceback is this:

Python: Traceback (most recent call last):
  File "C:\Users\\\*\*\*\AppData\Roaming\Blender Foundation\Blender\3.6\scripts\addons\BatchFBXExportUnity.py", line 91, in execute
    batch_fbx_export_objects(self.directory_path + "/" if self.directory_path else "")
  File "C:\Users\\\*\*\*\AppData\Roaming\Blender Foundation\Blender\3.6\scripts\addons\BatchFBXExportUnity.py", line 48, in batch_fbx_export_objects
    bpy.ops.export_scene.fbx(
  File "C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\modules\bpy\ops.py", line 113, in \_\_call\_\_
    ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Error: Python: Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\addons\io_scene_fbx\__init__.py", line 661, in execute
    return export_fbx_bin.save(self, context, \*\*keywords)
  File "C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\addons\io_scene_fbx\export_fbx_bin.py", line 3514, in save
    ret = save_single(operator, context.scene, depsgraph, filepath, \*\*kwargs_mod)
  File "C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\addons\io_scene_fbx\export_fbx_bin.py", line 3409, in save_single
    encode_bin.write(filepath, root, FBX_VERSION)
  File "C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\addons\io_scene_fbx\encode_bin.py", line 314, in write
    with open(fn, 'wb') as f:
PermissionError: [Errno 13] Permission denied: '000000.fbx'
Location: C:\Program Files\WindowsApps\BlenderFoundation.Blender_3.6.1.0_x64__ppwjx1n5r4v9t\Blender\3.6\scripts\modules\bpy\ops.py:113
bpy.ops.object.batch_fbx_export(filepath="C:\\Users\\\*\*\*\\Documents\\Modules\\untitled.fbx")

And here is the code:

bl_info = {
    "name": "Batch FBX Exporter for Unity",
    "description": "Exports each object in the scene as an FBX file",
    "author": "Stellar Wolf Entertainment",
    "version": (0, 0, 1),
    "blender": (3, 6, 0),
    "location": "File > Export > Batch > Unity FBX",
    "warning": "This addon is still in development and may not behave as expected.",
    "category": "Import-Export",
}

import bpy
from bpy.props import StringProperty
from bpy_extras.io_utils import ExportHelper

def move_to_origin(obj):
    original_location = obj.location.copy()
    obj.location = (0, 0, 0)
    return original_location

def move_back_to_original_location(obj, original_location):
    # Move the object back to its original location
    obj.location = original_location

def get_object_hierarchy(obj):
    # Recursive function to get the entire hierarchy of an object
    hierarchy = [obj]

    if obj.children:
        for child in obj.children:
            hierarchy.extend(get_object_hierarchy(child))

    return hierarchy

def batch_fbx_export_objects(directory_path):
    for scene in bpy.data.scenes:
        bpy.context.window.scene = scene

        for obj in bpy.data.objects:
            if obj.type == "MESH":
                object_hierarchy = get_object_hierarchy(obj)

                for obj_in_hierarchy in object_hierarchy:
                    obj_in_hierarchy.select_set(True)
                original_locations = [move_to_origin(obj_in_hierarchy) for obj_in_hierarchy in object_hierarchy]

                file_path = bpy.path.abspath(directory_path + obj.name + ".fbx")
                bpy.ops.export_scene.fbx(
                    filepath=file_path,
                    use_selection=True,
                    axis_forward='-Z',
                    axis_up='Y',
                    global_scale=1.0,
                    apply_unit_scale=True,
                    apply_scale_options='FBX_SCALE_ALL',
                    bake_space_transform=True,
                    object_types={'MESH'},
                    use_mesh_modifiers=True,
                    mesh_smooth_type='FACE',
                    use_mesh_edges=False,
                    use_tspace=True,
                    use_custom_props=False,
                    use_armature_deform_only=False,
                    bake_anim=False,
                    bake_anim_use_all_actions=False,
                    bake_anim_use_nla_strips=False,
                    bake_anim_use_all_bones=False,
                    path_mode='AUTO'
                )

                for obj_in_hierarchy, original_location in zip(object_hierarchy, original_locations):
                    move_back_to_original_location(obj_in_hierarchy, original_location)

                for obj_in_hierarchy in object_hierarchy:
                    obj_in_hierarchy.select_set(False)

class BatchFBXExportOperator(bpy.types.Operator, ExportHelper):
    bl_idname = "object.batch_fbx_export"
    bl_label = "Batch FBX Export"
    bl_options = {'REGISTER', 'UNDO'}
    filename_ext = ".fbx"

    
    directory_path: StringProperty(
        name="Directory Path",
        description="Select the directory where the files will be saved",
        subtype='DIR_PATH'
    )

    def execute(self, context):
        batch_fbx_export_objects(self.directory_path + "/" if self.directory_path else "")
        return {'FINISHED'}

def menu_func(self, context):
    layout = self.layout
    layout.separator()
    layout.operator(BatchFBXExportOperator.bl_idname)

def register():
    bpy.utils.register_class(BatchFBXExportOperator)
    bpy.types.TOPBAR_MT_file_export.append(menu_func)

def unregister():
    bpy.utils.unregister_class(BatchFBXExportOperator)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func)

if __name__ == "__main__":
    register()
$\endgroup$
10
  • 1
    $\begingroup$ It's really not efficient for anyone to have your script on a different page and without the error message. This forces us to open your link, click multiple times to download it, install it in Blender and then try your addon... Whereas you could just include all the info we need to see what's going on. Please edit your question to include your script using the code insertion tool. Please also include the full error message from Blender, which you can copy from either the Info Editor or from the system console which you can open using the menu Window > Toggle System Console. $\endgroup$
    – Lauloque
    Commented Aug 1, 2023 at 21:13
  • 1
    $\begingroup$ Also please describe what your script is meant to do and how, so that we can check if your script actually does this correctly. $\endgroup$
    – Lauloque
    Commented Aug 1, 2023 at 21:14
  • $\begingroup$ @L0Lock the error message simply states 'Access Denied' on the first file, It doesn't show the file path, just the name and extension. The script is intended to 'crawl' each scene finding every object, and save the objects (hierarchies preserved) to separate files using the name of the object as the filename. $\endgroup$
    – RWolfe
    Commented Aug 1, 2023 at 21:22
  • 1
    $\begingroup$ Please edit your question to add information to it instead of commenting it, that will make things easier to follow. Where do you get that error? Is it a popup message, or you get it in the info editor or console? If in the footer bar, you should be able to see it in full if you open the info editor. $\endgroup$
    – Lauloque
    Commented Aug 1, 2023 at 21:26
  • 1
    $\begingroup$ Nice, i edited it to format the error as code instead of a quote. Could you add your script following the same formatting please? $\endgroup$
    – Lauloque
    Commented Aug 1, 2023 at 22:08

1 Answer 1

1
$\begingroup$

I tried your script on my side, and it seems to work without error. Since the error is about a denied access, maybe check if you have the rights permissions on that folder, or try another one.

That being said, your script have other issues:

  1. The FBX were exported in the parent directory of what I selected. This is partially due to the method you use to set the output path, which also leaded to bad user experience as the dialog window is for setting up a file output, not a folder output
  2. If I understand, you want to export objects with their children in one file? In that case, you need to make a list of objects, and when you export a parent and its children: remove them from the list so that you don't export the children again.

Getting the output path

Since ExportHelper is made specifically to create a file output path, I don't see the point of using it here.

You could use fileselect_add() instead with a few tweaks to make if work for a folder.

I basically used the code from this answer

Object walking

When you cycle through object, Blender doesn't "see" them them way you see them in the outliner by default.

To "see" like Blender, you can switch your outliner to Blender-File Data display type:

enter image description here

Now you might understand the issue with your method:

It will walk through each object alphabetically with no notion of relationships. So, when you have an object at the top of the list that is the children of another object, it will be exported at least twice; firstly as the parent, then as the children of the other object.

I couldn't think of an ideal way of doing this without having issues with other scenarios, so I decided to just assume you will only have mesh objects at the top of your hierarchy and went with this approach:

get a list of all mesh objects
    for those who have no parents:
        get their children
        add the result in a list

        change position & export operations

This would not work with rigged models, for example, as the meshes are children of the armature. But since you disabled animation in your export option, I assumed this isn't an issue.

But otherwise, I suggest you consider just getting the selected objects, programmatically select the hierarchy (there's a nice operator just for it) and then export.

Final code

I made a point to add lots of print() to help you see what happens in the console, I also added lots of comments to explain the code a bit.

bl_info = {
    "name": "Batch FBX Exporter for Unity",
    "description": "Exports each object in the scene as an FBX file",
    "author": "Stellar Wolf Entertainment",
    "version": (0, 0, 1),
    "blender": (3, 6, 0),
    "location": "File > Export > Batch > Unity FBX",
    "warning": "This addon is still in development and may not behave as expected.",
    "category": "Import-Export",
}

import bpy
from bpy.props import StringProperty, BoolProperty
from bpy.types import Operator

def move_to_origin(obj):
    original_location = obj.location.copy()
    obj.location = (0, 0, 0)
    return original_location

def move_back_to_original_location(obj, original_location):
    # Move the object back to its original location
    obj.location = original_location

def batch_fbx_export_objects(directory_path):
    for scene in bpy.data.scenes:
        bpy.context.window.scene = scene

        # Get a list of all mesh objects in the scene
        mesh_objects = [obj for obj in bpy.data.objects if obj.type == 'MESH']
        print("Found mesh objects:")
        for i in mesh_objects:
            print(f"  - '{i.name}'")

        for obj in mesh_objects:
            bpy.ops.object.select_all(action='DESELECT')
            # filter to only top parents
            if obj.parent == None:
                top_parent_name = obj.name
                print("Found object_hierarchy to export:")
                print(f"  - '{top_parent_name}'")
                
                object_hierarchy = [obj]
                
                # check for children and add them in the object_hierarchy list
                children = [child for child in obj.children if child.type == 'MESH']
                for child in children:
                    print(f"    - '{child.name}'")
                    object_hierarchy.append(child)

                # select each of object_hierarchy
                for obj_in_hierarchy in object_hierarchy:
                    obj_in_hierarchy.select_set(True)
                    
                original_locations = [move_to_origin(obj_in_hierarchy) for obj_in_hierarchy in object_hierarchy]


                file_path = bpy.path.abspath(f"{directory_path}{top_parent_name}.fbx")
                bpy.ops.export_scene.fbx(
                    filepath=file_path,
                    use_selection=True,
                    axis_forward='-Z',
                    axis_up='Y',
                    global_scale=1.0,
                    apply_unit_scale=True,
                    apply_scale_options='FBX_SCALE_ALL',
                    bake_space_transform=True,
                    object_types={'MESH'},
                    use_mesh_modifiers=True,
                    mesh_smooth_type='FACE',
                    use_mesh_edges=False,
                    use_tspace=True,
                    use_custom_props=False,
                    use_armature_deform_only=False,
                    bake_anim=False,
                    bake_anim_use_all_actions=False,
                    bake_anim_use_nla_strips=False,
                    bake_anim_use_all_bones=False,
                    path_mode='AUTO'
                )

                for obj_in_hierarchy, original_location in zip(object_hierarchy, original_locations):
                    move_back_to_original_location(obj_in_hierarchy, original_location)

                for obj_in_hierarchy in object_hierarchy:
                    obj_in_hierarchy.select_set(False)
            else:
                parent = obj.parent
                print(f"X Skipping '{obj.name}' as it has a parent: '{parent.name}'")

class BatchFBXExportOperator(bpy.types.Operator):
    # EDIT: this adds a tooltip to the operarot, visible when hovering it in the export menu.
    """Export the scene's meshes to FBX for Unity"""
    bl_idname = "object.batch_fbx_export"
    bl_label = "Batch FBX Export"
    # EDIT: you can t undo the export, so no need to flag this operator as undoable
    bl_options = {'REGISTER'}
    # EDIT: no need for extension if not using ExportHelper

    
    directory: StringProperty(
        name="Output Path",
        description="Where to export the FBX files"
        # subtype='DIR_PATH' is not needed to specify the selection mode.
        # But this will be anyway a directory path.
        )

    # Filters folders
    filter_folder: BoolProperty(
        default=True,
        options={"HIDDEN"}
        )

    def execute(self, context):
        print(f"Selected path '{self.directory}'")
        if self.directory:
            batch_fbx_export_objects(self.directory)
            # EDIT: letting the user know things are done is always good.
            self.report({'INFO'}, 'Export finished!')
        else:
            self.report({'WARNING'}, 'No valid path provided')
        return {'FINISHED'}

    def invoke(self, context, event):
        # Open browser, take reference to 'self' read the path to selected
        # file, put path in predetermined self fields.
        # See: https://docs.blender.org/api/current/bpy.types.WindowManager.html#bpy.types.WindowManager.fileselect_add
        context.window_manager.fileselect_add(self)
        # Tells Blender to hang on for the slow user input
        return {'RUNNING_MODAL'}

def menu_func(self, context):
    layout = self.layout
    layout.separator()
    layout.operator(BatchFBXExportOperator.bl_idname)

def register():
    bpy.utils.register_class(BatchFBXExportOperator)
    bpy.types.TOPBAR_MT_file_export.append(menu_func)

def unregister():
    bpy.utils.unregister_class(BatchFBXExportOperator)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func)


#### THIS IS FOR TESTING FROM THE EDITOR ONLY
#### REMOVE FROM FINAL ADDON
# force invoke register if started from editor
if __name__ == "__main__":
    register()

    # run the operator from the invoke function
    bpy.ops.object.batch_fbx_export("INVOKE_DEFAULT")
$\endgroup$
4
  • $\begingroup$ This is what I needed, I hadn't gotten around to fixing the issues I had already suspected with getting the hierarchy, and I will 'adapt' the existing solution to allow for animations etc later on, I was primarily trying to fix the 'file-select' problem, but thanks for both $\endgroup$
    – RWolfe
    Commented Aug 3, 2023 at 23:14
  • $\begingroup$ I hope you do know that the FBX exporter have batch modes and presets. If you don't do anything really specific that the exporter can't do, there's no reason to write your own exporter, especially if you do i in ways where adding features require rethinking half of it its procedures. $\endgroup$
    – Lauloque
    Commented Aug 4, 2023 at 1:11
  • $\begingroup$ I didn't know that the built-in exporter could export the scenes to multiple FBX files. This must be a relatively new feature $\endgroup$
    – RWolfe
    Commented Aug 5, 2023 at 20:40
  • $\begingroup$ Having now checked out the 'Batch' export modes for this, It's not what I needed, It will export each scene or collection as it's own file, which would require me to 'move' each object into it's own scene or collection. The script that I did create works on the object level exporting each object to it's own file. $\endgroup$
    – RWolfe
    Commented Aug 5, 2023 at 20:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .