Skip to content
Snippets Groups Projects
export_fbx_bin.py 153 KiB
Newer Older
  • Learn to ignore specific revisions
  •                 bake_anim_use_all_actions=True,
    
                    bake_anim_step=1.0,
                    bake_anim_simplify_factor=1.0,
    
                    add_leaf_bones=False,
                    primary_bone_axis='Y',
                    secondary_bone_axis='X',
    
                    use_metadata=True,
                    path_mode='AUTO',
                    use_mesh_edges=True,
                    use_tspace=True,
                    embed_textures=False,
    
                    use_custom_props=False,
    
        # Clear cached ObjectWrappers (just in case...).
        ObjectWrapper.cache_clear()
    
    
        if object_types is None:
    
            object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'OTHER'}
    
    
        if 'OTHER' in object_types:
            object_types |= BLENDER_OTHER_OBJECT_TYPES
    
        # Default Blender unit is equivalent to meter, while FBX one is centimeter...
        unit_scale = units_blender_to_fbx_factor(scene) if apply_unit_scale else 100.0
        if apply_scale_options == 'FBX_SCALE_NONE':
    
            global_matrix = Matrix.Scale(unit_scale * global_scale, 4) @ global_matrix
    
            unit_scale = 1.0
        elif apply_scale_options == 'FBX_SCALE_UNITS':
    
            global_matrix = Matrix.Scale(global_scale, 4) @ global_matrix
    
        elif apply_scale_options == 'FBX_SCALE_CUSTOM':
    
            global_matrix = Matrix.Scale(unit_scale, 4) @ global_matrix
    
            unit_scale = global_scale
        else: # if apply_scale_options == 'FBX_SCALE_ALL':
            unit_scale = global_scale * unit_scale
    
    
        global_scale = global_matrix.median_scale
    
        global_matrix_inv = global_matrix.inverted()
    
        # For transforming mesh normals.
        global_matrix_inv_transposed = global_matrix_inv.transposed()
    
    
        # Only embed textures in COPY mode!
        if embed_textures and path_mode != 'COPY':
            embed_textures = False
    
    
        # Calculate bone correction matrix
    
        bone_correction_matrix = None  # Default is None = no change
        bone_correction_matrix_inv = None
        if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
            from bpy_extras.io_utils import axis_conversion
            bone_correction_matrix = axis_conversion(from_forward=secondary_bone_axis,
                                                     from_up=primary_bone_axis,
                                                     to_forward='X',
                                                     to_up='Y',
                                                     ).to_4x4()
            bone_correction_matrix_inv = bone_correction_matrix.inverted()
    
    
    
            path_mode,
            os.path.dirname(bpy.data.filepath),  # base_src
            os.path.dirname(filepath),  # base_dst
    
            # Local dir where to put images (media), using FBX conventions.
    
            os.path.splitext(os.path.basename(filepath))[0] + ".fbm",  # subdir
            embed_textures,
            set(),  # copy_set
    
            operator.report, (axis_up, axis_forward), global_matrix, global_scale, apply_unit_scale, unit_scale,
    
            bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
    
            context_objects, object_types, use_mesh_modifiers, use_mesh_modifiers_render,
    
            mesh_smooth_type, use_subsurf, use_mesh_edges, use_tspace,
    
            armature_nodetype, use_armature_deform_only,
            add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
    
            bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions,
    
            bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying,
    
            False, media_settings, use_custom_props,
    
        )
    
        import bpy_extras.io_utils
    
        print('\nFBX export starting... %r' % filepath)
        start_time = time.process_time()
    
        # Generate some data about exported scene...
    
        scene_data = fbx_data_from_scene(scene, depsgraph, settings)
    
    
        root = elem_empty(None, b"")  # Root element has no id, as it is not saved per se!
    
        # Mostly FBXHeaderExtension and GlobalSettings.
        fbx_header_elements(root, scene_data)
    
        # Documents and References are pretty much void currently.
        fbx_documents_elements(root, scene_data)
        fbx_references_elements(root, scene_data)
    
        # Templates definitions.
        fbx_definitions_elements(root, scene_data)
    
        # Actual data.
        fbx_objects_elements(root, scene_data)
    
        # How data are inter-connected.
        fbx_connections_elements(root, scene_data)
    
        # Animation.
        fbx_takes_elements(root, scene_data)
    
    
        # And we are down, we can write the whole thing!
        encode_bin.write(filepath, root, FBX_VERSION)
    
    
        # Clear cached ObjectWrappers!
        ObjectWrapper.cache_clear()
    
    
        # copy all collected files, if we did not embed them.
        if not media_settings.embed_textures:
            bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
    
        print('export finished in %.4f sec.' % (time.process_time() - start_time))
        return {'FINISHED'}
    
    
    # defaults for applications, currently only unity but could add others.
    def defaults_unity3d():
        return {
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # These options seem to produce the same result as the old Ascii exporter in Unity3D:
            "axis_up": 'Y',
            "axis_forward": '-Z',
    
            "global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # Should really be True, but it can cause problems if a model is already in a scene or prefab
            # with the old transforms.
            "bake_space_transform": False,
    
    
            "use_selection": False,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    
            "object_types": {'ARMATURE', 'EMPTY', 'MESH', 'OTHER'},
    
            "use_mesh_modifiers": True,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            "use_mesh_edges": False,
            "mesh_smooth_type": 'FACE',
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            "use_tspace": False,  # XXX Why? Unity is expected to support tspace import...
    
            "use_armature_deform_only": True,
    
    
            "use_custom_props": True,
    
            "bake_anim": True,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            "bake_anim_simplify_factor": 1.0,
            "bake_anim_step": 1.0,
            "bake_anim_use_nla_strips": True,
            "bake_anim_use_all_actions": True,
    
            "add_leaf_bones": False,  # Avoid memory/performance cost for something only useful for modelling
            "primary_bone_axis": 'Y',  # Doesn't really matter for Unity, so leave unchanged
            "secondary_bone_axis": 'X',
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    
            "path_mode": 'AUTO',
            "embed_textures": False,
    
            "batch_mode": 'OFF',
        }
    
    
    def save(operator, context,
             filepath="",
             use_selection=False,
    
             batch_mode='OFF',
             use_batch_own_dir=False,
             **kwargs
             ):
        """
    
        This is a wrapper around save_single, which handles multi-scenes (or collections) cases, when batch-exporting
        a whole .blend file.
    
        active_object = context.view_layer.objects.active
    
        if active_object and active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
            org_mode = active_object.mode
    
            bpy.ops.object.mode_set(mode='OBJECT')
    
        if batch_mode == 'OFF':
            kwargs_mod = kwargs.copy()
    
            if use_active_collection:
                if use_selection:
                    ctx_objects = tuple(obj
                                        for obj in context.view_layer.active_layer_collection.collection.all_objects
                                        if obj.select_get())
                else:
                    ctx_objects = context.view_layer.active_layer_collection.collection.all_objects
    
                if use_selection:
                    ctx_objects = context.selected_objects
                else:
                    ctx_objects = context.view_layer.objects
            kwargs_mod["context_objects"] = ctx_objects
    
            depsgraph = context.evaluated_depsgraph_get()
            ret = save_single(operator, context.scene, depsgraph, filepath, **kwargs_mod)
    
            # XXX We need a way to generate a depsgraph for inactive view_layers first...
            # XXX Also, what to do in case of batch-exporting scenes, when there is more than one view layer?
            #     Scenes have no concept of 'active' view layer, that's on window level...
    
            fbxpath = filepath
    
            prefix = os.path.basename(fbxpath)
            if prefix:
                fbxpath = os.path.dirname(fbxpath)
    
    
            if batch_mode == 'COLLECTION':
                data_seq = tuple((coll, coll.name, 'objects') for coll in bpy.data.collections if coll.objects)
            elif batch_mode in {'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
                scenes = [context.scene] if batch_mode == 'ACTIVE_SCENE_COLLECTION' else bpy.data.scenes
                data_seq = []
                for scene in scenes:
                    if not scene.objects:
                        continue
    
                    # Needed to avoid having tens of 'Scene Collection' entries.
    
                    todo_collections = [(scene.collection, "_".join((scene.name, scene.collection.name)))]
                    while todo_collections:
                        coll, coll_name = todo_collections.pop()
                        todo_collections.extend(((c, c.name) for c in coll.children if c.all_objects))
                        data_seq.append((coll, coll_name, 'all_objects'))
    
                data_seq = tuple((scene, scene.name, 'objects') for scene in bpy.data.scenes if scene.objects)
    
    
            # call this function within a loop with BATCH_ENABLE == False
    
            new_fbxpath = fbxpath  # own dir option modifies, we need to keep an original
    
            for data, data_name, data_obj_propname in data_seq:  # scene or collection
                newname = "_".join((prefix, bpy.path.clean_name(data_name))) if prefix else bpy.path.clean_name(data_name)
    
    
                if use_batch_own_dir:
                    new_fbxpath = os.path.join(fbxpath, newname)
    
                    # path may already exist... and be a file.
                    while os.path.isfile(new_fbxpath):
                        new_fbxpath = "_".join((new_fbxpath, "dir"))
    
                    if not os.path.exists(new_fbxpath):
                        os.makedirs(new_fbxpath)
    
                filepath = os.path.join(new_fbxpath, newname + '.fbx')
    
                print('\nBatch exporting %s as...\n\t%r' % (data, filepath))
    
    
                if batch_mode in {'COLLECTION', 'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
                    # Collection, so that objects update properly, add a dummy scene.
    
                    scene = bpy.data.scenes.new(name="FBX_Temp")
    
                    src_scenes = {}  # Count how much each 'source' scenes are used.
    
                    for obj in getattr(data, data_obj_propname):
                        for src_sce in obj.users_scene:
                            src_scenes[src_sce] = src_scenes.setdefault(src_sce, 0) + 1
                        scene.collection.objects.link(obj)
    
                    # Find the 'most used' source scene, and use its unit settings. This is somewhat weak, but should work
                    # fine in most cases, and avoids stupid issues like T41931.
                    best_src_scene = None
    
                    for sce, nbr_users in src_scenes.items():
                        if (nbr_users) > best_src_scene_users:
                            best_src_scene_users = nbr_users
                            best_src_scene = sce
                    scene.unit_settings.system = best_src_scene.unit_settings.system
                    scene.unit_settings.system_rotation = best_src_scene.unit_settings.system_rotation
                    scene.unit_settings.scale_length = best_src_scene.unit_settings.scale_length
    
    
                    # new scene [only one viewlayer to update]
                    scene.view_layers[0].update()
    
                    # TODO - BUMMER! Armatures not in the group wont animate the mesh
                else:
                    scene = data
    
                kwargs_batch = kwargs.copy()
    
                kwargs_batch["context_objects"] = getattr(data, data_obj_propname)
    
                save_single(operator, scene, scene.view_layers[0].depsgraph, filepath, **kwargs_batch)
    
                if batch_mode in {'COLLECTION', 'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
                    # Remove temp collection scene.
    
                    bpy.data.scenes.remove(scene)
    
    
        if active_object and org_mode:
            context.view_layer.objects.active = active_object
            if bpy.ops.object.mode_set.poll():
                bpy.ops.object.mode_set(mode=org_mode)