Skip to content
Snippets Groups Projects
export_fbx_bin.py 113 KiB
Newer Older
  • Learn to ignore specific revisions
  •                 if mat in data_materials:
                        data_materials[mat][1].append(obj)
                    else:
                        data_materials[mat] = (get_blenderID_key(mat), [obj])
    
        # Note FBX textures also hold their mapping info.
        # TODO: Support layers?
        data_textures = {}
        # FbxVideo also used to store static images...
        data_videos = {}
        # For now, do not use world textures, don't think they can be linked to anything FBX wise...
        for mat in data_materials.keys():
            for tex in mat.texture_slots:
                if tex is None:
                    continue
                # For now, only consider image textures.
                # Note FBX does has support for procedural, but this is not portable at all (opaque blob),
                # so not useful for us.
                # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
                #if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
                if tex.texture.type not in {'IMAGE'}:
                    continue
                img = tex.texture.image
                if img is None:
                    continue
                # Find out whether we can actually use this texture for this material, in FBX context.
                tex_fbx_props = fbx_mat_properties_from_texture(tex)
                if not tex_fbx_props:
                    continue
                if tex in data_textures:
                    data_textures[tex][1][mat] = tex_fbx_props
                else:
                    data_textures[tex] = (get_blenderID_key(tex), {mat: tex_fbx_props})
                if img in data_videos:
                    data_videos[img][1].append(tex)
                else:
                    data_videos[img] = (get_blenderID_key(img), [tex])
    
    
        # Animation...
        # From objects only for a start.
        tmp_scdata = FBXData(  # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
            None, None, None,
            settings, scene, objects, None,
            data_lamps, data_cameras, data_meshes, None,
            data_bones, data_deformers,
            data_world, data_materials, data_textures, data_videos,
        )
        animations = fbx_animations_objects(tmp_scdata)
    
    
        ##### Creation of templates...
    
        templates = OrderedDict()
        templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
    
        if data_lamps:
            templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps))
    
        if data_cameras:
            templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
    
        if data_bones:
            templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
    
        if data_meshes:
            templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(data_meshes))
    
        if objects:
            templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
    
        if arm_parents:
            # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
            templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
    
        if data_deformers:
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            nbr = len(data_deformers)
            nbr += sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values())
    
            templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
    
        # No world support in FBX...
        """
        if data_world:
            templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
        """
    
        if data_materials:
            templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
    
        if data_textures:
            templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
    
        if data_videos:
            templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
    
    
        if animations:
            # One stack!
            templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=1)
            # One layer per animated object.
            templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=len(animations[1]))
            # As much curve node as animated properties.
            nbr = sum(len(al) for _kal, al in animations[1].values())
            templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr)
            # And the number of curves themselves...
            nbr = sum(1 if ac else 0 for _kal, al in animations[1].values()
                                     for _kacn, acn in al.values()
                                     for _kac, _dv, ac in acn.values())
            templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr)
    
    
        templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
    
        ##### Creation of connections...
    
        connections = []
    
        # Objects (with classical parenting).
        for obj, obj_key in objects.items():
            # Bones are handled later.
    
            if isinstance(obj, Object):
    
                par = obj.parent
                par_key = 0  # Convention, "root" node (never explicitly written).
                if par and par in objects:
                    par_type = obj.parent_type
                    if par_type in {'OBJECT', 'BONE'}:
                        # Meshes parented to armature also have 'OBJECT' par_type, in FBX this is handled separately,
                        # we do not want an extra object parenting!
                        if (par, obj) not in arm_parents:
                            par_key = objects[par]
                    else:
                        print("Sorry, “{}” parenting type is not supported".format(par_type))
                connections.append((b"OO", get_fbxuid_from_key(obj_key), get_fbxuid_from_key(par_key), None))
    
        # Armature & Bone chains.
        for bo, (bo_key, _bo_data_key, arm) in data_bones.items():
            par = bo.parent
            if not par:  # Root bone.
                par = arm
            if par not in objects:
                continue
            connections.append((b"OO", get_fbxuid_from_key(bo_key), get_fbxuid_from_key(objects[par]), None))
    
        # Cameras
        for obj_cam, cam_key in data_cameras.items():
            cam_obj_key = objects[obj_cam]
            connections.append((b"OO", get_fbxuid_from_key(cam_key), get_fbxuid_from_key(cam_obj_key), None))
    
        # Object data.
        for obj, obj_key in objects.items():
    
            if isinstance(obj, Bone):
    
                _bo_key, bo_data_key, _arm = data_bones[obj]
                assert(_bo_key == obj_key)
                connections.append((b"OO", get_fbxuid_from_key(bo_data_key), get_fbxuid_from_key(obj_key), None))
            elif obj.type == 'LAMP':
                lamp_key = data_lamps[obj.data]
                connections.append((b"OO", get_fbxuid_from_key(lamp_key), get_fbxuid_from_key(obj_key), None))
            elif obj.type == 'MESH':
    
                mesh_key, _obj = data_meshes[obj.data]
    
                connections.append((b"OO", get_fbxuid_from_key(mesh_key), get_fbxuid_from_key(obj_key), None))
    
        # Deformers (armature-to-geometry, only for meshes currently)...
        for arm, deformed_meshes in data_deformers.items():
            for me, (skin_key, _obj, clusters) in deformed_meshes.items():
                # skin -> geometry
    
                mesh_key, _obj = data_meshes[me]
                connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(mesh_key), None))
    
                for bo, clstr_key in clusters.items():
                    # cluster -> skin
                    connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None))
                    # bone -> cluster
                    connections.append((b"OO", get_fbxuid_from_key(objects[bo]), get_fbxuid_from_key(clstr_key), None))
    
        # Materials
        mesh_mat_indices = {}
        _objs_indices = {}
        for mat, (mat_key, objs) in data_materials.items():
            for obj in objs:
                obj_key = objects[obj]
                connections.append((b"OO", get_fbxuid_from_key(mat_key), get_fbxuid_from_key(obj_key), None))
                # Get index of this mat for this object.
                # Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
                # Only mats for meshes currently...
                me = obj.data
                idx = _objs_indices[obj] = _objs_indices.get(obj, -1) + 1
                mesh_mat_indices.setdefault(me, {})[mat] = idx
        del _objs_indices
    
        # Textures
        for tex, (tex_key, mats) in data_textures.items():
            for mat, fbx_mat_props in mats.items():
                mat_key, _objs = data_materials[mat]
                for fbx_prop in fbx_mat_props:
                    # texture -> material properties
                    connections.append((b"OP", get_fbxuid_from_key(tex_key), get_fbxuid_from_key(mat_key), fbx_prop))
    
        # Images
        for vid, (vid_key, texs) in data_videos.items():
            for tex in texs:
                tex_key, _texs = data_textures[tex]
                connections.append((b"OO", get_fbxuid_from_key(vid_key), get_fbxuid_from_key(tex_key), None))
    
    
        #Animations
        if animations:
            # Animstack itself is linked nowhere!
            astack_id = get_fbxuid_from_key(animations[0])
            for obj, (alayer_key, acurvenodes) in animations[1].items():
                obj_id = get_fbxuid_from_key(objects[obj])
                # Animlayer -> animstack.
                alayer_id = get_fbxuid_from_key(alayer_key)
                connections.append((b"OO", alayer_id, astack_id, None))
                for fbx_prop, (acurvenode_key, acurves) in acurvenodes.items():
                    # Animcurvenode -> animalayer.
                    acurvenode_id = get_fbxuid_from_key(acurvenode_key)
                    connections.append((b"OO", acurvenode_id, alayer_id, None))
                    # Animcurvenode -> object property.
                    connections.append((b"OP", alayer_id, obj_id, fbx_prop.encode()))
                    for fbx_item, (acurve_key, dafault_value, acurve) in acurves.items():
                        if acurve:
                            # Animcurve -> Animcurvenode.
                            connections.append((b"OP", get_fbxuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
    
    
        ##### And pack all this!
    
        return FBXData(
            templates, templates_users, connections,
    
            settings, scene, objects, animations,
    
            data_lamps, data_cameras, data_meshes, mesh_mat_indices,
            data_bones, data_deformers,
            data_world, data_materials, data_textures, data_videos,
        )
    
    
    ##### Top-level FBX elements generators. #####
    
    def fbx_header_elements(root, scene_data, time=None):
        """
        Write boiling code of FBX root.
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        ##### Start of FBXHeaderExtension element.
        header_ext = elem_empty(root, b"FBXHeaderExtension")
    
        elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
    
        elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
    
        # No encryption!
        elem_data_single_int32(header_ext, b"EncryptionType", 0)
    
        if time is None:
            time = datetime.datetime.now()
        elem = elem_empty(header_ext, b"CreationTimeStamp")
        elem_data_single_int32(elem, b"Version", 1000)
        elem_data_single_int32(elem, b"Year", time.year)
        elem_data_single_int32(elem, b"Month", time.month)
        elem_data_single_int32(elem, b"Day", time.day)
        elem_data_single_int32(elem, b"Hour", time.hour)
        elem_data_single_int32(elem, b"Minute", time.minute)
        elem_data_single_int32(elem, b"Second", time.second)
        elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
    
        elem_data_single_string_unicode(header_ext, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        # 'SceneInfo' seems mandatory to get a valid FBX file...
        # TODO use real values!
        # XXX Should we use scene.name.encode() here?
        scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
        scene_info.add_string(b"UserData")
        elem_data_single_string(scene_info, b"Type", b"UserData")
        elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
        meta_data = elem_empty(scene_info, b"MetaData")
        elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
        elem_data_single_string(meta_data, b"Title", b"")
        elem_data_single_string(meta_data, b"Subject", b"")
        elem_data_single_string(meta_data, b"Author", b"")
        elem_data_single_string(meta_data, b"Keywords", b"")
        elem_data_single_string(meta_data, b"Revision", b"")
        elem_data_single_string(meta_data, b"Comment", b"")
    
        props = elem_properties(scene_info)
        elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
        elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
        original = elem_props_compound(props, b"Original")
        original("p_string", b"ApplicationVendor", "Blender Foundation")
        original("p_string", b"ApplicationName", "Blender")
        original("p_string", b"ApplicationVersion", "2.70")
        original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
        original("p_string", b"FileName", "/foobar.fbx")
        lastsaved = elem_props_compound(props, b"LastSaved")
        lastsaved("p_string", b"ApplicationVendor", "Blender Foundation")
        lastsaved("p_string", b"ApplicationName", "Blender")
        lastsaved("p_string", b"ApplicationVersion", "2.70")
        lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
    
        ##### End of FBXHeaderExtension element.
    
        # FileID is replaced by dummy value currently...
        elem_data_single_bytes(root, b"FileId", b"FooBar")
    
        # CreationTime is replaced by dummy value currently, but anyway...
        elem_data_single_string_unicode(root, b"CreationTime",
                                        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
                                        "".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
                                                  time.microsecond * 1000))
    
        elem_data_single_string_unicode(root, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        ##### Start of GlobalSettings element.
        global_settings = elem_empty(root, b"GlobalSettings")
    
        elem_data_single_int32(global_settings, b"Version", 1000)
    
        props = elem_properties(global_settings)
        up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
        elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
        elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
        elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
        elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
        elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
        elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
        elem_props_set(props, "p_number", b"UnitScaleFactor", 1.0)
        elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
        elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
        # XXX Those time stuff is taken from a file, have no (complete) idea what it means!
        elem_props_set(props, "p_enum", b"TimeMode", 11)
        elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
        elem_props_set(props, "p_timestamp", b"TimeSpanStop", 46186158000)  # XXX One second!
    
        ##### End of GlobalSettings element.
    
    
    def fbx_documents_elements(root, scene_data):
        """
        Write 'Document' part of FBX root.
        Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        name = scene_data.scene.name
    
        ##### Start of Documents element.
        docs = elem_empty(root, b"Documents")
    
        elem_data_single_int32(docs, b"Count", 1)
    
        doc_uid = get_fbxuid_from_key("__FBX_Document__" + name)
        doc = elem_data_single_int64(docs, b"Document", doc_uid)
        doc.add_string(b"")
        doc.add_string_unicode(name)
    
        props = elem_properties(doc)
        elem_props_set(props, "p_object", b"SourceObject")
        elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
    
        # XXX Some kind of ID? Offset?
        #     Anyway, as long as we have only one doc, probably not an issue.
        elem_data_single_int64(doc, b"RootNode", 0)
    
    
    def fbx_references_elements(root, scene_data):
        """
        Have no idea what references are in FBX currently... Just writing empty element.
        """
        docs = elem_empty(root, b"References")
    
    
    def fbx_definitions_elements(root, scene_data):
        """
        Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
        """
        definitions = elem_empty(root, b"Definitions")
    
        elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
        elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
    
        fbx_templates_generate(definitions, scene_data.templates)
    
    
    def fbx_objects_elements(root, scene_data):
        """
        Data (objects, geometry, material, textures, armatures, etc.
        """
        objects = elem_empty(root, b"Objects")
    
        for lamp in scene_data.data_lamps.keys():
            fbx_data_lamp_elements(objects, lamp, scene_data)
    
        for cam in scene_data.data_cameras.keys():
            fbx_data_camera_elements(objects, cam, scene_data)
    
    
        for mesh in scene_data.data_meshes.keys():
    
            fbx_data_mesh_elements(objects, mesh, scene_data)
    
        for obj in scene_data.objects.keys():
            fbx_data_object_elements(objects, obj, scene_data)
    
        for obj in scene_data.objects.keys():
    
            if not isinstance(obj, Object) or obj.type not in {'ARMATURE'}:
    
                continue
            fbx_data_armature_elements(objects, obj, scene_data)
    
        for mat in scene_data.data_materials.keys():
            fbx_data_material_elements(objects, mat, scene_data)
    
        for tex in scene_data.data_textures.keys():
            fbx_data_texture_file_elements(objects, tex, scene_data)
    
        for vid in scene_data.data_videos.keys():
            fbx_data_video_elements(objects, vid, scene_data)
    
    
        fbx_data_animation_elements(objects, scene_data)
    
    
    
    def fbx_connections_elements(root, scene_data):
        """
        Relations between Objects (which material uses which texture, and so on).
        """
        connections = elem_empty(root, b"Connections")
    
        for c in scene_data.connections:
            elem_connection(connections, *c)
    
    
    def fbx_takes_elements(root, scene_data):
        """
        Animations. Have yet to check how this work...
        """
    
        # XXX Are takes needed at all in new anim system?
    
        takes = elem_empty(root, b"Takes")
        elem_data_single_string(takes, b"Current", b"")
    
    
        animations = scene_data.animations
        if animations is None:
            return
        scene = scene_data.scene
        take_name = scene.name.encode()
        fps = scene.render.fps / scene.render.fps_base
        scene_start_ktime = int(units_convert(scene.frame_start / fps, "second", "ktime"))
        scene_end_ktime = int(units_convert(scene.frame_end / fps, "second", "ktime"))
    
        take = elem_data_single_string(takes, b"Take", take_name)
        elem_data_single_string(take, b"FileName", take_name + b".tak")
        take_loc_time = elem_data_single_int64(take, b"LocalTime", scene_start_ktime)
        take_loc_time.add_int64(scene_end_ktime)
        take_ref_time = elem_data_single_int64(take, b"ReferenceTime", scene_start_ktime)
        take_ref_time.add_int64(scene_end_ktime)
    
    
    
    ##### "Main" functions. #####
    FBXSettingsMedia = namedtuple("FBXSettingsMedia", (
        "path_mode", "base_src", "base_dst", "subdir",
        "embed_textures", "copy_set",
    ))
    FBXSettings = namedtuple("FBXSettings", (
    
        "to_axes", "global_matrix", "global_scale",
    
        "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
    
        "context_objects", "object_types", "use_mesh_modifiers",
    
        "mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only",
    
        "bake_anim", "bake_anim_step", "bake_anim_simplify_factor",
    
        "use_metadata", "media_settings", "use_custom_properties",
    ))
    
    
    # This func can be called with just the filepath
    def save_single(operator, scene, filepath="",
                    global_matrix=Matrix(),
                    axis_up="Z",
                    axis_forward="Y",
                    context_objects=None,
                    object_types=None,
                    use_mesh_modifiers=True,
                    mesh_smooth_type='FACE',
    
                    bake_anim=True,
                    bake_anim_step=1.0,
                    bake_anim_simplify_factor=1.0,
    
                    use_metadata=True,
                    path_mode='AUTO',
                    use_mesh_edges=True,
                    use_tspace=True,
                    embed_textures=False,
                    use_custom_properties=False,
    
                    **kwargs
                    ):
    
        if object_types is None:
            object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH'}
    
        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
    
        media_settings = FBXSettingsMedia(
            path_mode,
            os.path.dirname(bpy.data.filepath),  # base_src
            os.path.dirname(filepath),  # base_dst
            # Local dir where to put images (medias), using FBX conventions.
            os.path.splitext(os.path.basename(filepath))[0] + ".fbm",  # subdir
            embed_textures,
            set(),  # copy_set
        )
    
        settings = FBXSettings(
    
            (axis_up, axis_forward), global_matrix, global_scale,
    
            bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
    
            context_objects, object_types, use_mesh_modifiers,
    
            mesh_smooth_type, use_mesh_edges, use_tspace, False,
            bake_anim, bake_anim_step, bake_anim_simplify_factor,
            False, media_settings, use_custom_properties,
    
        )
    
        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, 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)
    
        # 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 {
            "global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
            "use_selection": False,
            "object_types": {'ARMATURE', 'EMPTY', 'MESH'},
            "use_mesh_modifiers": True,
    
            #"use_armature_deform_only": True,
            "bake_anim": True,
            #"use_anim_optimize": False,
            #"use_anim_action_all": True,
    
            "batch_mode": 'OFF',
    
            # 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,
    
        }
    
    
    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 groups) cases, when batch-exporting a whole
        .blend file.
        """
    
        ret = None
    
        org_mode = None
        if context.active_object and context.active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
            org_mode = context.active_object.mode
            bpy.ops.object.mode_set(mode='OBJECT')
    
        if batch_mode == 'OFF':
            kwargs_mod = kwargs.copy()
            if use_selection:
                kwargs_mod["context_objects"] = context.selected_objects
            else:
                kwargs_mod["context_objects"] = context.scene.objects
    
            ret = save_single(operator, context.scene, filepath, **kwargs_mod)
        else:
            fbxpath = filepath
    
            prefix = os.path.basename(fbxpath)
            if prefix:
                fbxpath = os.path.dirname(fbxpath)
    
            if batch_mode == 'GROUP':
                data_seq = bpy.data.groups
            else:
                data_seq = bpy.data.scenes
    
            # call this function within a loop with BATCH_ENABLE == False
            # no scene switching done at the moment.
            # orig_sce = context.scene
    
            new_fbxpath = fbxpath  # own dir option modifies, we need to keep an original
            for data in data_seq:  # scene or group
                newname = "_".join((prefix, bpy.path.clean_name(data.name)))
    
                if use_batch_own_dir:
                    new_fbxpath = os.path.join(fbxpath, newname)
                    # path may already exist
                    # TODO - might exist but be a file. unlikely but should probably account for it.
    
                    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 == 'GROUP':  # group
                    # group, so objects update properly, add a dummy scene.
                    scene = bpy.data.scenes.new(name="FBX_Temp")
                    scene.layers = [True] * 20
                    # bpy.data.scenes.active = scene # XXX, cant switch
                    for ob_base in data.objects:
                        scene.objects.link(ob_base)
    
                    scene.update()
                    # TODO - BUMMER! Armatures not in the group wont animate the mesh
                else:
                    scene = data
    
                kwargs_batch = kwargs.copy()
                kwargs_batch["context_objects"] = data.objects
    
                save_single(operator, scene, filepath, **kwargs_batch)
    
                if batch_mode == 'GROUP':
                    # remove temp group scene
                    bpy.data.scenes.remove(scene)
    
            # no active scene changing!
            # bpy.data.scenes.active = orig_sce
    
            ret = {'FINISHED'}  # so the script wont run after we have batch exported.
    
        if context.active_object and org_mode and bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set(mode=org_mode)
    
        return ret