bl_info = { "name": "Unity FBX format", "author": "Angel 'Edy' Garcia (@VehiclePhysics)", "version": (1, 4, 1), "blender": (2, 80, 0), "location": "File > Export > Unity FBX", "description": "FBX exporter compatible with Unity's coordinate and scaling system.", "warning": "", "wiki_url": "", "category": "Import-Export", } import bpy import mathutils import math # Multi-user datablocks are preserved here. Unique copies are made for applying the rotation. # Eventually multi-user datablocks become single-user and gets processed. # Therefore restoring the multi-user data assigns a shared but already processed datablock. shared_data = dict() # All objects and collections in this view layer must be visible while being processed. # apply_rotation and matrix changes don't have effect otherwise. # Visibility will be restored right before saving the FBX. hidden_collections = [] hidden_objects = [] disabled_collections = [] disabled_objects = [] def unhide_collections(col): global hidden_collections global disabled_collections # No need to unhide excluded collections. Their objects aren't included in current view layer. if col.exclude: return # Find hidden child collections and unhide them hidden = [item for item in col.children if not item.exclude and item.hide_viewport] for item in hidden: item.hide_viewport = False # Add them to the list so they could be restored later hidden_collections.extend(hidden) # Same with the disabled collections disabled = [item for item in col.children if not item.exclude and item.collection.hide_viewport] for item in disabled: item.collection.hide_viewport = False disabled_collections.extend(disabled) # Recursively unhide child collections for item in col.children: unhide_collections(item) def unhide_objects(): global hidden_objects global disabled_objects view_layer_objects = [ob for ob in bpy.data.objects if ob.name in bpy.context.view_layer.objects] for ob in view_layer_objects: if ob.hide_get(): hidden_objects.append(ob) ob.hide_set(False) if ob.hide_viewport: disabled_objects.append(ob) ob.hide_viewport = False def make_single_user_data(): global shared_data for ob in bpy.data.objects: if ob.data and ob.data.users > 1: # Figure out actual users of this datablock (not counting fake users) users = [user for user in bpy.data.objects if user.data == ob.data] if len(users) > 1: # Store shared mesh data (MESH objects only). # Other shared datablocks (CURVE, FONT, etc) are always exported as separate meshes # by the built-in FBX exporter. if ob.type == 'MESH': # Shared mesh data will be restored if users have no active modifiers modifiers = 0 for user in users: modifiers += len([mod for mod in user.modifiers if mod.show_viewport]) if modifiers == 0: shared_data[ob.name] = ob.data # Single-user data is mandatory in all object types, otherwise we can't apply the rotation. ob.data = ob.data.copy() def apply_object_modifiers(): # Select objects in current view layer not using an armature modifier bpy.ops.object.select_all(action='DESELECT') for ob in bpy.data.objects: if ob.name in bpy.context.view_layer.objects: bypass_modifiers = False for mod in ob.modifiers: if mod.type == 'ARMATURE': bypass_modifiers = True if not bypass_modifiers: ob.select_set(True) # Conversion to mesh may not be available depending on the remaining objects if bpy.ops.object.convert.poll(): print("Converting to meshes:", bpy.context.selected_objects) bpy.ops.object.convert(target='MESH') def reset_parent_inverse(ob): if (ob.parent): mat_world = ob.matrix_world.copy() ob.matrix_parent_inverse.identity() ob.matrix_basis = ob.parent.matrix_world.inverted() @ mat_world def apply_rotation(ob): bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.ops.object.transform_apply(location = False, rotation = True, scale = False) def fix_object(ob): # Only fix objects in current view layer if ob.name in bpy.context.view_layer.objects: # Reset parent's inverse so we can work with local transform directly reset_parent_inverse(ob) # Create a copy of the local matrix and set a pure X-90 matrix mat_original = ob.matrix_local.copy() ob.matrix_local = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X') # Apply the rotation to the object apply_rotation(ob) # Reapply the previous local transform with an X+90 rotation ob.matrix_local = mat_original @ mathutils.Matrix.Rotation(math.radians(90.0), 4, 'X') # Recursively fix child objects in current view layer. # Children may be in the current view layer even if their parent isn't. for child in ob.children: fix_object(child) def export_unity_fbx(context, filepath, active_collection, selected_objects, deform_bones, leaf_bones, primary_bone_axis, secondary_bone_axis, tangent_space, triangulate_faces): global shared_data global hidden_collections global hidden_objects global disabled_collections global disabled_objects print("Preparing 3D model for Unity...") # Root objects: Empty, Mesh, Curve, Surface, Font or Armature without parent root_objects = [item for item in bpy.data.objects if (item.type == "EMPTY" or item.type == "MESH" or item.type == "ARMATURE" or item.type == "FONT" or item.type == "CURVE" or item.type == "SURFACE") and not item.parent] # Preserve current scene # undo_push examples, including exporters' execute: # https://programtalk.com/python-examples/bpy.ops.ed.undo_push (Examples 4, 5 and 6) # https://sourcecodequery.com/example-method/bpy.ops.ed.undo (Examples 1 and 2) bpy.ops.ed.undo_push(message="Prepare Unity FBX") shared_data = dict() hidden_collections = [] hidden_objects = [] disabled_collections = [] disabled_objects = [] selection = bpy.context.selected_objects # Object mode if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode="OBJECT") # Ensure all the collections and objects in this view layer are visible unhide_collections(bpy.context.view_layer.layer_collection) unhide_objects() # Create a single copy in multi-user datablocks. Will be restored after fixing rotations. make_single_user_data() # Apply modifiers to objects (except those affected by an armature) apply_object_modifiers() try: # Fix rotations for ob in root_objects: print(ob.name, ob.type) fix_object(ob) # Restore multi-user meshes for item in shared_data: bpy.data.objects[item].data = shared_data[item] # Recompute the transforms out of the changed matrices bpy.context.view_layer.update() # Restore hidden and disabled objects for ob in hidden_objects: ob.hide_set(True) for ob in disabled_objects: ob.hide_viewport = True # Restore hidden and disabled collections for col in hidden_collections: col.hide_viewport = True for col in disabled_collections: col.collection.hide_viewport = True # Restore selection bpy.ops.object.select_all(action='DESELECT') for ob in selection: ob.select_set(True) # Export FBX file params = dict(filepath=filepath, apply_scale_options='FBX_SCALE_UNITS', object_types={'EMPTY', 'MESH', 'ARMATURE'}, use_active_collection=active_collection, use_selection=selected_objects, use_armature_deform_only=deform_bones, add_leaf_bones=leaf_bones, primary_bone_axis=primary_bone_axis, secondary_bone_axis=secondary_bone_axis, use_tspace=tangent_space, use_triangles=triangulate_faces) print("Invoking default FBX Exporter:", params) bpy.ops.export_scene.fbx(**params) except Exception as e: bpy.ops.ed.undo_push(message="") bpy.ops.ed.undo() bpy.ops.ed.undo_push(message="Export Unity FBX") print(e) print("File not saved.") # Always finish with 'FINISHED' so Undo is handled properly return {'FINISHED'} # Restore scene and finish bpy.ops.ed.undo_push(message="") bpy.ops.ed.undo() bpy.ops.ed.undo_push(message="Export Unity FBX") print("FBX file for Unity saved.") return {'FINISHED'} #--------------------------------------------------------------------------------------------------- # Exporter stuff (from the Operator File Export template) # ExportHelper is a helper class, defines filename and # invoke() function which calls the file selector. from bpy_extras.io_utils import ExportHelper from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import Operator class ExportUnityFbx(Operator, ExportHelper): """FBX exporter compatible with Unity's coordinate and scaling system""" bl_idname = "export_scene.unity_fbx" bl_label = "Export Unity FBX" bl_options = {'UNDO_GROUPED'} # ExportHelper mixin class uses this filename_ext = ".fbx" filter_glob: StringProperty( default="*.fbx", options={'HIDDEN'}, maxlen=255, # Max internal buffer length, longer would be clamped. ) # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. active_collection: BoolProperty( name="Active Collection Only", description="Export objects in the active collection only (and its children). May be combined with Selected Objects Only", default=False, ) selected_objects: BoolProperty( name="Selected Objects Only", description="Export selected objects only. May be combined with Active Collection Only", default=False, ) deform_bones: BoolProperty( name="Only Deform Bones", description="Only write deforming bones (and non-deforming ones when they have deforming children)", default=False, ) leaf_bones: BoolProperty( name="Add Leaf Bones", description="Append a final bone to the end of each chain to specify last bone length (use this when you intend to edit the armature from exported data)", default=False, ) primary_bone_axis: EnumProperty( name="Primary", items=(('X', "X Axis", ""), ('Y', "Y Axis", ""), ('Z', "Z Axis", ""), ('-X', "-X Axis", ""), ('-Y', "-Y Axis", ""), ('-Z', "-Z Axis", ""), ), default='Y', ) secondary_bone_axis: EnumProperty( name="Secondary", items=(('X', "X Axis", ""), ('Y', "Y Axis", ""), ('Z', "Z Axis", ""), ('-X', "-X Axis", ""), ('-Y', "-Y Axis", ""), ('-Z', "-Z Axis", ""), ), default='X', ) tangent_space: BoolProperty( name="Export tangents", description="Add binormal and tangent vectors, together with normal they form the tangent space (tris/quads only). Meshes with N-gons won't export tangents unless the option Triangulate Faces is enabled", default=False, ) triangulate_faces: BoolProperty( name="Triangulate Faces", description="Convert all faces to triangles. This is necessary for exporting tangents in meshes with N-gons. Otherwise Unity will show a warning when importing tangents in these meshes", default=False, ) # Custom draw method # https://blender.stackexchange.com/questions/55437/add-gui-elements-to-exporter-window # https://docs.blender.org/api/current/bpy.types.UILayout.html def draw(self, context): layout = self.layout layout.row().label(text = "Selection") layout.row().prop(self, "active_collection") layout.row().prop(self, "selected_objects") layout.separator() layout.row().label(text = "Meshes") layout.row().prop(self, "tangent_space") layout.row().prop(self, "triangulate_faces") layout.separator() layout.row().label(text = "Armatures") layout.row().prop(self, "deform_bones") layout.row().prop(self, "leaf_bones") layout.row().label(text = "Bone Axes") split = layout.split(factor=0.4) col = split.column() col.alignment = 'RIGHT' col.label(text = "Primary") split.column().prop(self, "primary_bone_axis", text="") split = layout.split(factor=0.4) col = split.column() col.alignment = 'RIGHT' col.label(text = "Secondary") split.column().prop(self, "secondary_bone_axis", text="") def execute(self, context): return export_unity_fbx(context, self.filepath, self.active_collection, self.selected_objects, self.deform_bones, self.leaf_bones, self.primary_bone_axis, self.secondary_bone_axis, self.tangent_space, self.triangulate_faces) # Only needed if you want to add into a dynamic menu def menu_func_export(self, context): self.layout.operator(ExportUnityFbx.bl_idname, text="Unity FBX (.fbx)") def register(): bpy.utils.register_class(ExportUnityFbx) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) def unregister(): bpy.utils.unregister_class(ExportUnityFbx) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) if __name__ == "__main__": register() # test call bpy.ops.export_scene.unity_fbx('INVOKE_DEFAULT')