Skip to content
Snippets Groups Projects
export_fbx_bin.py 153 KiB
Newer Older
  • Learn to ignore specific revisions
  •             loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
                p_rots[ob_obj] = rot
    
                anim_loc.add_keyframe(real_currframe, loc)
                anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
                anim_scale.add_keyframe(real_currframe, scale)
            for anim_shape, me, shape in animdata_shapes.values():
                anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
    
            for anim_camera, camera in animdata_cameras.values():
                anim_camera.add_keyframe(real_currframe, (camera.lens,))
    
            currframe += bake_step
    
    
        scene.frame_set(back_currframe, subframe=0.0)
    
    
        # And now, produce final data (usable by FBX export code)
        # Objects-like loc/rot/scale...
        for ob_obj, anims in animdata_ob.items():
            for anim in anims:
    
                anim.simplify(simplify_fac, bake_step, force_keep)
    
                if not anim:
                    continue
                for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
    
                    anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
    
                    anim_data[1][fbx_group] = (group_key, group, fbx_gname)
    
    
        # And meshes' shape keys.
        for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
    
            anim_shape.simplify(simplify_fac, bake_step, force_keep)
    
            if not anim_shape:
                continue
            for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
    
                anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
                anim_data[1][fbx_group] = (group_key, group, fbx_gname)
    
    
        # And cameras' lens keys.
        for cam_key, (anim_camera, camera) in animdata_cameras.items():
    
            anim_camera.simplify(simplify_fac, bake_step, force_keep)
            if not anim_camera:
                continue
            for elem_key, group_key, group, fbx_group, fbx_gname in anim_camera.get_final_data(scene, ref_id, force_keep):
    
                anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
                anim_data[1][fbx_group] = (group_key, group, fbx_gname)
    
    
        astack_key = get_blender_anim_stack_key(scene, ref_id)
        alayer_key = get_blender_anim_layer_key(scene, ref_id)
    
        name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
    
        if start_zero:
            f_end -= f_start
            f_start = 0.0
    
    
        return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
    
    
    
        """
        Generate global animation data from objects.
        """
        scene = scene_data.scene
        animations = []
    
        frame_start = 1e100
        frame_end = -1e100
    
            nonlocal frame_start, frame_end
            if anim is not None:
                animations.append(anim)
                f_start, f_end = anim[4:6]
                if f_start < frame_start:
                    frame_start = f_start
                if f_end > frame_end:
                    frame_end = f_end
    
    
                _astack_key, astack, _alayer_key, _name, _fstart, _fend = anim
                for elem_key, (alayer_key, acurvenodes) in astack.items():
                    for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
                        animated.add((elem_key, fbx_prop))
    
    
        # Per-NLA strip animstacks.
        if scene_data.settings.bake_anim_use_nla_strips:
            strips = []
    
                ob = ob_obj.bdata  # Back to real Blender Object.
                if not ob.animation_data:
                    continue
    
    
                # Some actions are read-only, one cause is being in NLA tweakmode
                restore_use_tweak_mode = ob.animation_data.use_tweak_mode
                if ob.animation_data.is_property_readonly('action'):
                  ob.animation_data.use_tweak_mode = False
    
    
                # We have to remove active action from objects, it overwrites strips actions otherwise...
    
                ob_actions.append((ob, ob.animation_data.action, restore_use_tweak_mode))
    
                for track in ob.animation_data.nla_tracks:
    
                    if track.mute:
                        continue
                    for strip in track.strips:
                        if strip.mute:
                            continue
                        strips.append(strip)
                        strip.mute = True
    
            for strip in strips:
                strip.mute = False
    
                         fbx_animations_do(scene_data, strip, strip.frame_start, strip.frame_end, True, force_keep=True))
    
                scene.frame_set(scene.frame_current, subframe=0.0)
    
            for ob, ob_act, restore_use_tweak_mode in ob_actions:
    
                ob.animation_data.use_tweak_mode = restore_use_tweak_mode
    
        # All actions.
        if scene_data.settings.bake_anim_use_all_actions:
            def validate_actions(act, path_resolve):
                for fc in act.fcurves:
                    data_path = fc.data_path
                    if fc.array_index:
                        data_path = data_path + "[%d]" % fc.array_index
                    try:
                        path_resolve(data_path)
                    except ValueError:
                        return False  # Invalid.
                return True  # Valid.
    
    
                # Restore org state of object (ugh :/ ).
                props = (
                    'location', 'rotation_quaternion', 'rotation_axis_angle', 'rotation_euler', 'rotation_mode', 'scale',
                    'delta_location', 'delta_rotation_euler', 'delta_rotation_quaternion', 'delta_scale',
                    'lock_location', 'lock_rotation', 'lock_rotation_w', 'lock_rotations_4d', 'lock_scale',
    
                    'tag', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
    
                    'matrix_parent_inverse', 'empty_display_type', 'empty_display_size', 'empty_image_offset', 'pass_index',
    
                    'color', 'hide_viewport', 'hide_select', 'hide_render', 'instance_type',
    
                    'use_instance_vertices_rotation', 'use_instance_faces_scale', 'instance_faces_scale',
    
                    'display_type', 'show_bounds', 'display_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
                    'show_wire', 'show_all_edges', 'show_transparent', 'show_in_front',
    
                    'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
                )
                for p in props:
    
                    if not ob_to.is_property_readonly(p):
                        setattr(ob_to, p, getattr(ob_from, p))
    
                # Actions only for objects, not bones!
    
                ob = ob_obj.bdata  # Back to real Blender Object.
    
    
                if not ob.animation_data:
                    continue  # Do not export animations for objects that are absolutely not animated, see T44386.
    
    
                if ob.animation_data.is_property_readonly('action'):
                    continue  # Cannot re-assign 'active action' to this object (usually related to NLA usage, see T48089).
    
    
                # We can't play with animdata and actions and get back to org state easily.
                # So we have to add a temp copy of the object to the scene, animate it, and remove it... :/
    
                # Great, have to handle bones as well if needed...
                pbones_matrices = [pbo.matrix_basis.copy() for pbo in ob.pose.bones] if ob.type == 'ARMATURE' else ...
    
    
                for act in bpy.data.actions:
                    # For now, *all* paths in the action must be valid for the object, to validate the action.
                    # Unless that action was already assigned to the object!
                    if act != org_act and not validate_actions(act, path_resolve):
                        continue
    
                    frame_start, frame_end = act.frame_range  # sic!
    
                             fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True,
                                               objects={ob_obj}, force_keep=True))
    
                    if pbones_matrices is not ...:
                        for pbo, mat in zip(ob.pose.bones, pbones_matrices):
                            pbo.matrix_basis = mat.copy()
    
                    scene.frame_set(scene.frame_current, subframe=0.0)
    
                if pbones_matrices is not ...:
                    for pbo, mat in zip(ob.pose.bones, pbones_matrices):
                        pbo.matrix_basis = mat.copy()
    
                scene.frame_set(scene.frame_current, subframe=0.0)
    
        # Global (containing everything) animstack, only if not exporting NLA strips and/or all actions.
        if not scene_data.settings.bake_anim_use_nla_strips and not scene_data.settings.bake_anim_use_all_actions:
    
            add_anim(animations, animated, fbx_animations_do(scene_data, None, scene.frame_start, scene.frame_end, False))
    
        # Be sure to update all matrices back to org state!
    
        scene.frame_set(scene.frame_current, subframe=0.0)
    
        return animations, animated, frame_start, frame_end
    
    def fbx_data_from_scene(scene, depsgraph, settings):
    
        """
        Do some pre-processing over scene's data...
        """
        objtypes = settings.object_types
    
        dp_objtypes = objtypes - {'ARMATURE'}  # Armatures are not supported as dupli instances currently...
    
        # ##### Gathering data...
    
        # This is rather simple for now, maybe we could end generating templates with most-used values
        # instead of default ones?
    
        objects = {}  # Because we do not have any ordered set...
    
        for ob in settings.context_objects:
            if ob.type not in objtypes:
                continue
            ob_obj = ObjectWrapper(ob)
            objects[ob_obj] = None
            # Duplis...
    
            for dp_obj in ob_obj.dupli_list_gen(depsgraph):
    
        perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...")
    
    
        data_lights = {ob_obj.bdata.data: get_blenderID_key(ob_obj.bdata.data)
                       for ob_obj in objects if ob_obj.type == 'LIGHT'}
    
        # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
    
        data_cameras = {ob_obj: get_blenderID_key(ob_obj.bdata.data)
                        for ob_obj in objects if ob_obj.type == 'CAMERA'}
    
        # Yep! Contains nothing, but needed!
    
        data_empties = {ob_obj: get_blender_empty_key(ob_obj.bdata)
                        for ob_obj in objects if ob_obj.type == 'EMPTY'}
    
        for ob_obj in objects:
            if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
                continue
            ob = ob_obj.bdata
    
            use_org_data = True
    
            org_ob_obj = None
    
            # Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those.
            if ob_obj.is_dupli:
                org_ob_obj = ObjectWrapper(ob)  # We get the "real" object wrapper from that dupli instance.
                if org_ob_obj in data_meshes:
                    data_meshes[ob_obj] = data_meshes[org_ob_obj]
                    continue
    
    
            is_ob_material = any(ms.link == 'OBJECT' for ms in ob.material_slots)
    
            if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material:
                # We cannot use default mesh in that case, or material would not be the right ones...
    
                use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES)
    
                backup_pose_positions = []
    
                    # No need to create a new mesh in this case, if no modifier is active!
    
                        # For meshes, when armature export is enabled, disable Armature modifiers here!
    
                        # XXX Temp hacks here since currently we only have access to a viewport depsgraph...
    
                        #
                        # NOTE: We put armature to the rest pose instead of disabling it so we still
                        # have vertex groups in the evaluated mesh.
    
                        if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
    
                            object = mod.object
                            if object and object.type == 'ARMATURE':
                                armature = object.data
    
                                # If armature is already in REST position, there's nothing to back-up
                                # This cuts down on export time dramatically, if all armatures are already in REST position
                                # by not triggering dependency graph update
                                if armature.pose_position != 'REST':
                                    backup_pose_positions.append((armature, armature.pose_position))
                                    armature.pose_position = 'REST'
    
                        elif mod.show_render or mod.show_viewport:
    
                            # If exporting with subsurf collect the last Catmull-Clark subsurf modifier
                            # and disable it. We can use the original data as long as this is the first
                            # found applicable subsurf modifier.
                            if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK':
                                if last_subsurf:
                                    use_org_data = False
                                last_subsurf = mod
                            else:
                                use_org_data = False
                    if settings.use_subsurf and last_subsurf:
                        # XXX: When exporting with subsurf information temporarily disable
                        # the last subsurf modifier.
                        tmp_mods.append((last_subsurf, last_subsurf.show_render, last_subsurf.show_viewport))
                        last_subsurf.show_render = False
                        last_subsurf.show_viewport = False
    
                if not use_org_data:
    
                    # If modifiers has been altered need to update dependency graph.
    
                    if backup_pose_positions or tmp_mods:
    
                        depsgraph.update()
                    ob_to_convert = ob.evaluated_get(depsgraph) if settings.use_mesh_modifiers else ob
    
                    # NOTE: The dependency graph might be re-evaluating multiple times, which could
                    # potentially free the mesh created early on. So we put those meshes to bmain and
                    # free them afterwards. Not ideal but ensures correct ownerwhip.
    
                    tmp_me = bpy.data.meshes.new_from_object(
                                ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph)
    
                    data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
    
                # Change armatures back.
                for armature, pose_position in backup_pose_positions:
                    print((armature, pose_position))
                    armature.pose_position = pose_position
                    # Update now, so we don't leave modified state after last object was exported.
    
                # Re-enable temporary disabled modifiers.
                for mod, show_render, show_viewport in tmp_mods:
                    mod.show_render = show_render
                    mod.show_viewport = show_viewport
                if backup_pose_positions or tmp_mods:
    
                data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
    
    
            # In case "real" source object of that dupli did not yet still existed in data_meshes, create it now!
            if org_ob_obj is not None:
                data_meshes[org_ob_obj] = data_meshes[ob_obj]
    
    
        perfmon.step("FBX export prepare: Wrapping ShapeKeys...")
    
    
        geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
    
        for me_key, me, _free in data_meshes.values():
    
            if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1):  # We do not want basis-only relative skeys...
    
            if me in data_deformers_shape:
                continue
    
            shapes_key = get_blender_mesh_shape_key(me)
    
            # We gather all vcos first, since some skeys may be based on others...
    
            _cos = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
            me.vertices.foreach_get("co", _cos)
            v_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
    
            sk_cos = {}
            for shape in me.shape_keys.key_blocks[1:]:
                shape.data.foreach_get("co", _cos)
                sk_cos[shape] = tuple(vcos_transformed_gen(_cos, geom_mat_co))
            sk_base = me.shape_keys.key_blocks[0]
    
            for shape in me.shape_keys.key_blocks[1:]:
    
                # Only write vertices really different from org coordinates!
                shape_verts_co = []
                shape_verts_idx = []
    
                sv_cos = sk_cos[shape]
                ref_cos = v_cos if shape.relative_key == sk_base else sk_cos[shape.relative_key]
                for idx, (sv_co, ref_co) in enumerate(zip(sv_cos, ref_cos)):
                    if similar_values_iter(sv_co, ref_co):
    
                        # Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not
                        #       have this at all... Anyway, this should cover most common cases imho.
                        continue
    
                    shape_verts_co.extend(Vector(sv_co) - Vector(ref_co))
    
                    shape_verts_idx.append(idx)
    
    
                # FBX does not like empty shapes (makes Unity crash e.g.).
                # To prevent this, we add a vertex that does nothing, but it keeps the shape key intact
    
                if not shape_verts_co:
    
                    shape_verts_co.extend((0, 0, 0))
                    shape_verts_idx.append(0)
    
    
                channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
                data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
    
                data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data
    
        perfmon.step("FBX export prepare: Wrapping Armatures...")
    
    
        data_deformers_skin = {}
        data_bones = {}
    
        arm_parents = set()
    
        for ob_obj in tuple(objects):
            if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
    
            fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
    
                                       data_bones, data_deformers_skin, data_empties, arm_parents)
    
        if settings.add_leaf_bones:
            data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones)
    
    
        # Some world settings are embedded in FBX materials...
        if scene.world:
    
            data_world = {scene.world: get_blenderID_key(scene.world)}
    
        perfmon.step("FBX export prepare: Wrapping Materials...")
    
    
        # TODO: Check all the material stuff works even when they are linked to Objects
    
        #       (we can then have the same mesh used with different materials...).
        #       *Should* work, as FBX always links its materials to Models (i.e. objects).
        #       XXX However, material indices would probably break...
    
        for ob_obj in objects:
            # If obj is not a valid object for materials, wrapper will just return an empty tuple...
    
            for ma_s in ob_obj.material_slots:
                ma = ma_s.material
                if ma is None:
    
                    continue  # Empty slots!
    
                # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
                # However, I doubt anything else than Lambert/Phong is really portable!
    
                # Note we want to keep a 'dummy' empty material even when we can't really support it, see T41396.
    
                ma_data = data_materials.setdefault(ma, (get_blenderID_key(ma), []))
                ma_data[1].append(ob_obj)
    
        perfmon.step("FBX export prepare: Wrapping Textures...")
    
    
        # Note FBX textures also hold their mapping info.
        # TODO: Support layers?
    
        # FbxVideo also used to store static images...
    
        # For now, do not use world textures, don't think they can be linked to anything FBX wise...
    
        for ma in data_materials.keys():
            # Note: with nodal shaders, we'll could be generating much more textures, but that's kind of unavoidable,
    
            #       given that textures actually do not exist anymore in material context in Blender...
    
            ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
            for sock_name, fbx_name in PRINCIPLED_TEXTURE_SOCKETS_TO_FBX:
                tex = getattr(ma_wrap, sock_name)
    
                if tex is None or tex.image is None:
    
                blender_tex_key = (ma, sock_name)
                data_textures[blender_tex_key] = (get_blender_nodetexture_key(*blender_tex_key), fbx_name)
    
                img = tex.image
    
                vid_data = data_videos.setdefault(img, (get_blenderID_key(img), []))
                vid_data[1].append(blender_tex_key)
    
        perfmon.step("FBX export prepare: Wrapping Animations...")
    
    
        frame_start = scene.frame_start
        frame_end = scene.frame_end
    
        if settings.bake_anim:
            # From objects & bones only for a start.
    
            # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
            tmp_scdata = FBXExportData(
    
                settings, scene, depsgraph, objects, None, None, 0.0, 0.0,
    
                data_empties, data_lights, data_cameras, data_meshes, None,
    
                data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
    
            animations, animated, frame_start, frame_end = fbx_animations(tmp_scdata)
    
        # ##### Creation of templates...
    
        perfmon.step("FBX export prepare: Generating templates...")
    
    
        templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
    
    
        if data_empties:
            templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
    
    
        if data_lights:
            templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lights))
    
    
        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:
    
            nbr = len({me_key for me_key, _me, _free in data_meshes.values()})
    
            if data_deformers_shape:
                nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
            templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr)
    
    
        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_skin or data_deformers_shape:
            nbr = 0
            if data_deformers_skin:
                nbr += len(data_deformers_skin)
                nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values())
            if data_deformers_shape:
                nbr += len(data_deformers_shape)
                nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
            assert(nbr != 0)
    
            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))
    
    
            nbr_astacks = len(animations)
            nbr_acnodes = 0
            nbr_acurves = 0
            for _astack_key, astack, _al, _n, _fs, _fe in animations:
                for _alayer_key, alayer in astack.values():
                    for _acnode_key, acnode, _acnode_name in alayer.values():
                        nbr_acnodes += 1
                        for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
                            if acurve:
                                nbr_acurves += 1
    
            templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
    
            # Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
    
            # So for now, only one layer per anim stack.
            templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
            templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
            templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
    
        templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
    
    
        # ##### Creation of connections...
    
        perfmon.step("FBX export prepare: Generating Connections...")
    
    
        connections = []
    
        # Objects (with classical parenting).
    
            # Bones are handled later.
    
            if not ob_obj.is_bone:
                par_obj = ob_obj.parent
                # Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0).
                if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents:
                    connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None))
                else:
                    connections.append((b"OO", ob_obj.fbx_uuid, 0, None))
    
    
        # Armature & Bone chains.
    
        for bo_obj in data_bones.keys():
            par_obj = bo_obj.parent
            if par_obj not in objects:
    
            connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None))
    
        for ob_obj in objects:
            if ob_obj.is_bone:
                bo_data_key = data_bones[ob_obj]
    
                connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None))
    
                if ob_obj.type == 'LIGHT':
                    light_key = data_lights[ob_obj.bdata.data]
                    connections.append((b"OO", get_fbx_uuid_from_key(light_key), ob_obj.fbx_uuid, None))
    
                elif ob_obj.type == 'CAMERA':
                    cam_key = data_cameras[ob_obj]
    
                    connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None))
    
                elif ob_obj.type == 'EMPTY' or ob_obj.type == 'ARMATURE':
    
                    connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
    
                elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
    
                    mesh_key, _me, _free = data_meshes[ob_obj]
    
                    connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
    
        # Leaf Bones
        for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones:
    
            connections.append((b"OO", node_uuid, par_uuid, None))
    
            connections.append((b"OO", attr_uuid, node_uuid, None))
    
    
        # 'Shape' deformers (shape keys, only for meshes currently)...
        for me_key, shapes_key, shapes in data_deformers_shape.values():
            # shape -> geometry
            connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None))
            for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values():
                # shape channel -> shape
                connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None))
                # geometry (keys) -> shape channel
                connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None))
    
        # 'Skin' deformers (armature-to-geometry, only for meshes currently)...
        for arm, deformed_meshes in data_deformers_skin.items():
    
            for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
    
                mesh_key, _me, _free = data_meshes[ob_obj]
    
                connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
    
                for bo_obj, clstr_key in clusters.items():
    
                    connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None))
    
                    connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None))
    
        for ma, (ma_key, ob_objs) in data_materials.items():
    
                connections.append((b"OO", get_fbx_uuid_from_key(ma_key), ob_obj.fbx_uuid, None))
                # Get index of this material for this object (or dupliobject).
                # Material indices for mesh faces are determined by their order in 'ma to ob' connections.
                # Only materials for meshes currently...
                # Note in case of dupliobjects a same me/ma idx will be generated several times...
    
                # Should not be an issue in practice, and it's needed in case we export duplis but not the original!
                if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
                    continue
    
                _mesh_key, me, _free = data_meshes[ob_obj]
    
                idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
    
                mesh_material_indices.setdefault(me, {})[ma] = idx
    
        for (ma, sock_name), (tex_key, fbx_prop) in data_textures.items():
            ma_key, _ob_objs = data_materials[ma]
            # texture -> material properties
            connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_from_key(ma_key), fbx_prop))
    
        for vid, (vid_key, blender_tex_keys) in data_videos.items():
            for blender_tex_key in blender_tex_keys:
                tex_key, _fbx_prop = data_textures[blender_tex_key]
    
                connections.append((b"OO", get_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None))
    
        # Animations
    
        for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
    
            # Animstack itself is linked nowhere!
    
            astack_id = get_fbx_uuid_from_key(astack_key)
    
            alayer_id = get_fbx_uuid_from_key(alayer_key)
    
            connections.append((b"OO", alayer_id, astack_id, None))
    
            for elem_key, (alayer_key, acurvenodes) in astack.items():
                elem_id = get_fbx_uuid_from_key(elem_key)
    
                # Animlayer -> animstack.
    
                # alayer_id = get_fbx_uuid_from_key(alayer_key)
    
                # connections.append((b"OO", alayer_id, astack_id, None))
    
                for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
    
                    # Animcurvenode -> animalayer.
    
                    acurvenode_id = get_fbx_uuid_from_key(acurvenode_key)
    
                    connections.append((b"OO", acurvenode_id, alayer_id, None))
                    # Animcurvenode -> object property.
    
                    connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
    
                    for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
    
                        if acurve:
                            # Animcurve -> Animcurvenode.
    
                            connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
    
        # ##### And pack all this!
    
            templates, templates_users, connections,
    
            settings, scene, depsgraph, objects, animations, animated, frame_start, frame_end,
    
            data_empties, data_lights, data_cameras, data_meshes, mesh_material_indices,
    
            data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
    
            data_world, data_materials, data_textures, data_videos,
        )
    
    
    
    def fbx_scene_data_cleanup(scene_data):
        """
        Some final cleanup...
        """
        # Delete temp meshes.
    
        done_meshes = set()
        for me_key, me, free in scene_data.data_meshes.values():
            if free and me_key not in done_meshes:
    
    # ##### 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).
        """
    
        app_vendor = "Blender Foundation"
        app_name = "Blender (stable FBX IO)"
        app_ver = bpy.app.version_string
    
    
        import addon_utils
        import sys
        addon_ver = addon_utils.module_bl_info(sys.modules[__package__])['version']
    
    
        # ##### 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", "%s - %s - %d.%d.%d"
                                                    % (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
    
    
        # '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", app_vendor)
        original("p_string", b"ApplicationName", app_name)
        original("p_string", b"ApplicationVersion", app_ver)
    
        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", app_vendor)
        lastsaved("p_string", b"ApplicationName", app_name)
        lastsaved("p_string", b"ApplicationVersion", app_ver)
    
        lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
    
        original("p_string", b"ApplicationNativeFile", bpy.data.filepath)
    
        # ##### 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", "%s - %s - %d.%d.%d"
                                              % (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
    
        # ##### Start of GlobalSettings element.
    
        global_settings = elem_empty(root, b"GlobalSettings")
    
        scene = scene_data.scene
    
    
        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]
    
        #~ # DO NOT take into account global scale here! That setting is applied to object transformations during export
        #~ # (in other words, this is pure blender-exporter feature, and has nothing to do with FBX data).
        #~ if scene_data.settings.apply_unit_scale:
            #~ # Unit scaling is applied to objects' scale, so our unit is effectively FBX one (centimeter).
            #~ scale_factor_org = 1.0
            #~ scale_factor = 1.0 / units_blender_to_fbx_factor(scene)
        #~ else:
            #~ scale_factor_org = units_blender_to_fbx_factor(scene)
            #~ scale_factor = scale_factor_org
        scale_factor = scale_factor_org = scene_data.settings.unit_scale
    
        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_integer", b"OriginalUpAxis", -1)
        elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
    
        elem_props_set(props, "p_double", b"UnitScaleFactor", scale_factor)
    
        elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", scale_factor_org)
    
        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")
    
        r = scene.render
        _, fbx_fps_mode = FBX_FRAMERATES[0]  # Custom framerate.
        fbx_fps = fps = r.fps / r.fps_base
    
        for ref_fps, fps_mode in FBX_FRAMERATES:
            if similar_values(fps, ref_fps):
                fbx_fps = ref_fps
                fbx_fps_mode = fps_mode
        elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
    
        elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
        elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
    
        elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
    
        # ##### 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_fbx_uuid_from_key("__FBX_Document__" + name)
    
        doc = elem_data_single_int64(docs, b"Document", doc_uid)
    
        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")
    
    
        perfmon.step("FBX export fetch empties (%d)..." % len(scene_data.data_empties))
    
    
            fbx_data_empty_elements(objects, empty, scene_data)
    
    
        perfmon.step("FBX export fetch lamps (%d)..." % len(scene_data.data_lights))
    
        for lamp in scene_data.data_lights:
            fbx_data_light_elements(objects, lamp, scene_data)
    
        perfmon.step("FBX export fetch cameras (%d)..." % len(scene_data.data_cameras))
    
    
            fbx_data_camera_elements(objects, cam, scene_data)
    
    
        perfmon.step("FBX export fetch meshes (%d)..."
                     % len({me_key for me_key, _me, _free in scene_data.data_meshes.values()}))
    
        done_meshes = set()
    
        for me_obj in scene_data.data_meshes:
            fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
    
        perfmon.step("FBX export fetch objects (%d)..." % len(scene_data.objects))
    
    
        for ob_obj in scene_data.objects:
            if ob_obj.is_dupli:
    
            fbx_data_object_elements(objects, ob_obj, scene_data)
    
            for dp_obj in ob_obj.dupli_list_gen(scene_data.depsgraph):
    
                if dp_obj not in scene_data.objects:
                    continue
                fbx_data_object_elements(objects, dp_obj, scene_data)
    
        for ob_obj in scene_data.objects:
            if not (ob_obj.is_object and ob_obj.type == 'ARMATURE'):
    
            fbx_data_armature_elements(objects, ob_obj, scene_data)
    
        if scene_data.data_leaf_bones:
            fbx_data_leaf_bone_elements(objects, scene_data)
    
    
        for ma in scene_data.data_materials:
            fbx_data_material_elements(objects, ma, scene_data)
    
        for blender_tex_key in scene_data.data_textures:
            fbx_data_texture_file_elements(objects, blender_tex_key, scene_data)
    
            fbx_data_video_elements(objects, vid, scene_data)
    
    
        perfmon.step("FBX export fetch animations...")
        start_time = time.process_time()
    
    
        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):
        """
    
        # XXX Pretty sure takes are no more needed...
    
        takes = elem_empty(root, b"Takes")
        elem_data_single_string(takes, b"Current", b"")
    
    
        animations = scene_data.animations
    
        for astack_key, animations, alayer_key, name, f_start, f_end in animations:
            scene = scene_data.scene
            fps = scene.render.fps / scene.render.fps_base
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            start_ktime = int(convert_sec_to_ktime(f_start / fps))
            end_ktime = int(convert_sec_to_ktime(f_end / fps))
    
    
            take = elem_data_single_string(takes, b"Take", name)
            elem_data_single_string(take, b"FileName", name + b".tak")
            take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
            take_loc_time.add_int64(end_ktime)
            take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
            take_ref_time.add_int64(end_ktime)
    
    # ##### "Main" functions. #####
    
    # This func can be called with just the filepath
    
    def save_single(operator, scene, depsgraph, filepath="",
    
                    global_matrix=Matrix(),
    
                    apply_unit_scale=False,
    
                    global_scale=1.0,
                    apply_scale_options='FBX_SCALE_NONE',
    
                    axis_up="Z",
                    axis_forward="Y",
                    context_objects=None,
                    object_types=None,
                    use_mesh_modifiers=True,
    
                    mesh_smooth_type='FACE',
    
                    use_armature_deform_only=False,
    
                    bake_anim=True,