import os from datetime import datetime import bpy _FUTURE = bpy.app.version >= (2, 80, 0) from bpy.props import EnumProperty, StringProperty, BoolProperty, IntProperty from bpy.types import UIList, Operator, Panel, Context, Scene if _FUTURE: from bpy.types import Collection as Group else: from bpy.types import Group as Group def message_box(message: str, title: str = "Message", icon: str = 'INFO') -> None: """Shows modal window with a message :param message: Message text :param title: Window title :param icon: Window icon """ def draw(self, context): col = self.layout.column(align=True) for line in message.strip().split("\n"): col.label(text=line) bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) def name_as_path(name: str) -> str: """Converts and sanitizes an arbitrary string to a canonical path with OS-specific separator :param name: Some name :returns: Canonical path """ return os.path.join(*[bpy.path.clean_name(n) for n in name.replace("\\", "/").split("/") if n]) + ".fbx" def fbx_export(context: Context) -> None: """ Export all objects Groups to ASCII FBX """ print("[%s] Batch FBX export begin" % datetime.now()) settings_ascii = dict( version='ASCII6100', use_selection=True, global_scale=1.0, axis_forward='-Z', axis_up='Y', object_types=context.scene.fbx_object_types, use_mesh_modifiers=True, mesh_smooth_type='OFF', use_mesh_edges=False, use_tspace=False, use_armature_deform_only=False, add_leaf_bones=False, primary_bone_axis='Y', secondary_bone_axis='X', armature_nodetype='NULL', use_anim=context.scene.fbx_use_anim, use_default_take=False, path_mode='RELATIVE', batch_mode='OFF', use_batch_own_dir=False, ) settings_binary = dict( # Common version='BIN7400', axis_forward='-Z', axis_up='Y', apply_scale_options='FBX_SCALE_ALL', apply_unit_scale=True, bake_space_transform=True, global_scale=1.0, object_types=context.scene.fbx_object_types, use_custom_props=False, use_selection=True, # Mesh mesh_smooth_type='FACE', use_mesh_modifiers=False, use_mesh_edges=False, use_tspace=False, # Armature add_leaf_bones=False, primary_bone_axis='Y', secondary_bone_axis='X', use_armature_deform_only=False, armature_nodetype='NULL', # Animation bake_anim=context.scene.fbx_use_anim, bake_anim_use_all_bones=True, bake_anim_use_all_actions=True, bake_anim_use_nla_strips=True, bake_anim_force_startend_keying=True, bake_anim_step=1.0, bake_anim_simplify_factor=1.0, # File path_mode='RELATIVE', embed_textures=False, batch_mode='OFF', ) settings = settings_binary if _FUTURE or context.scene.fbx_force_binary else settings_ascii if _FUTURE: settings.pop('version') rootpath = os.path.normpath(bpy.path.abspath(context.scene.fbx_export_dir)) groups = bpy.data.collections if _FUTURE else bpy.data.groups override = context.copy() for group in [g for g in groups if g.fbx_exportable]: filepath = os.path.join(rootpath, name_as_path(group.name)) basepath = os.path.dirname(filepath) if not os.path.exists(basepath): os.makedirs(basepath) override.update( { "selected_objects": group.objects } ) settings.update( { "filepath": filepath } ) if bpy.app.version >= (4, 0, 0): with context.temp_override(**override): bpy.ops.export_scene.fbx(**settings) else: bpy.ops.export_scene.fbx(override, **settings) print("\n[%s] Batch FBX export end." % datetime.now()) class FBX_OT_ManualExport(Operator): bl_idname = "fbx.manual_export" bl_label = "Manual Export" bl_description = "Export FBX manualy" def execute(self, context): try: fbx_export(context) except Exception as e: self.report({'ERROR'}, str(e)) return {'CANCELLED'} return {'FINISHED'} class FBX_UL_ExportItems(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): row = layout.row(align=True) row.prop(item, "fbx_exportable", icon='CHECKBOX_HLT' if item.fbx_exportable else 'CHECKBOX_DEHLT', text="", emboss=False) row.prop(item, "name", text="", emboss=False, icon='GROUP') class SCENE_PT_fbx_autoexport(Panel): """Creates a Panel in the scene context of the properties editor""" bl_label = "FBX Auto-Export" bl_idname = "SCENE_PT_fbx_autoexport" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "scene" def draw(self, context): layout = self.layout scene = context.scene col = layout.column() split = col.split() sub = split.column() sub.label(text="Filter groups:") sub.template_list("FBX_UL_ExportItems", "", bpy.data, "collections" if _FUTURE else "groups", scene, "fbx_group_index", rows=5) sub = split.column() sub.label(text="Filter object types:") sub.prop(scene, "fbx_object_types") col.separator() if _FUTURE: col.use_property_split = True col.use_property_decorate = False col.prop(scene, "fbx_on_save") col.prop(scene, "fbx_use_anim") if not _FUTURE: col.prop(scene, "fbx_force_binary") col.separator() col.prop(scene, "fbx_export_dir") col.operator(FBX_OT_ManualExport.bl_idname) def on_save(*args, **kwargs) -> None: """ Runs when Blender file is being saved """ if not bpy.context.scene.fbx_on_save: return try: fbx_export(bpy.context) except Exception as e: message_box(str(e), "Export error", 'ERROR') classes = [ FBX_OT_ManualExport, FBX_UL_ExportItems, SCENE_PT_fbx_autoexport, ] def register() -> None: for cls in classes: bpy.utils.register_class(cls) Scene.fbx_export_dir = StringProperty( name="FBX Export Dir", description="FBX Export Directory", subtype="DIR_PATH", default="//../fbx/", ) Scene.fbx_on_save = BoolProperty( name="Export on save", description="Export FBX on Blender file save", default=False ) Scene.fbx_object_types = EnumProperty( name="Object Types", options={'ENUM_FLAG'}, items=( ('EMPTY', "Empty", ""), ('CAMERA', "Camera", ""), ('LAMP', "Lamp", ""), ('ARMATURE', "Armature", "WARNING: not supported in dupli/group instances"), ('MESH', "Mesh", ""), ('OTHER', "Other", "Other geometry types, like curve, metaball, etc. (converted to meshes)"), ), description="Which kind of object to export", default={'ARMATURE', 'EMPTY', 'MESH', 'OTHER'}, ) Scene.fbx_group_index = IntProperty(default=0) Scene.fbx_force_binary = BoolProperty( name="Binary Format", description="Force export FBX file as binary", default=False ) Scene.fbx_use_anim = BoolProperty( name="Include Animation", description="Export baked keyframe animation", default=False ) Group.fbx_exportable = BoolProperty( name="Export Group", description="Mark group as FBX exportable", default=False ) bpy.app.handlers.save_pre.append(on_save) def unregister() -> None: bpy.app.handlers.save_pre.remove(on_save) del Group.fbx_exportable del Scene.fbx_group_index del Scene.fbx_object_types del Scene.fbx_on_save del Scene.fbx_export_dir for cls in reversed(classes): bpy.utils.unregister_class(cls) register()