Skip to content
Snippets Groups Projects
export_fbx_bin.py 121 KiB
Newer Older
  • Learn to ignore specific revisions
  •         lay_smooth = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing")
            elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
        if vcolnumber:
            lay_vcol = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
            elem_data_single_int32(lay_vcol, b"TypedIndex", 0)
        if uvnumber:
            lay_uv = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
            elem_data_single_int32(lay_uv, b"TypedIndex", 0)
        if me_fbxmats_idx is not None:
            lay_mat = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_mat, b"Type", b"LayerElementMaterial")
            elem_data_single_int32(lay_mat, b"TypedIndex", 0)
    
        # Add other uv and/or vcol layers...
        for vcolidx, uvidx, tspaceidx in zip_longest(range(1, vcolnumber), range(1, uvnumber), range(1, tspacenumber),
                                                     fillvalue=0):
            layer = elem_data_single_int32(geom, b"Layer", max(vcolidx, uvidx))
            elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
            if vcolidx:
                lay_vcol = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
                elem_data_single_int32(lay_vcol, b"TypedIndex", vcolidx)
            if uvidx:
                lay_uv = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
                elem_data_single_int32(lay_uv, b"TypedIndex", uvidx)
            if tspaceidx:
                lay_binor = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
                elem_data_single_int32(lay_binor, b"TypedIndex", tspaceidx)
                lay_tan = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
                elem_data_single_int32(lay_tan, b"TypedIndex", tspaceidx)
    
    
        done_meshes.add(me_key)
    
    
    
    def fbx_data_material_elements(root, mat, scene_data):
        """
        Write the Material data block.
        """
        ambient_color = (0.0, 0.0, 0.0)
        if scene_data.data_world:
            ambient_color = next(iter(scene_data.data_world.keys())).ambient_color
    
        mat_key, _objs = scene_data.data_materials[mat]
        # Approximation...
    
        mat_type = b"Phong" if mat.specular_shader in {'COOKTORR', 'PHONG', 'BLINN'} else b"Lambert"
    
        fbx_mat = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(mat_key))
    
        fbx_mat.add_string(fbx_name_class(mat.name.encode(), b"Material"))
        fbx_mat.add_string(b"")
    
        elem_data_single_int32(fbx_mat, b"Version", FBX_MATERIAL_VERSION)
        # those are not yet properties, it seems...
        elem_data_single_string(fbx_mat, b"ShadingModel", mat_type)
        elem_data_single_int32(fbx_mat, b"MultiLayer", 0)  # Should be bool...
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Material")
    
        props = elem_properties(fbx_mat)
    
        elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", mat_type.decode())
    
        elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", mat.diffuse_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", mat.emit)
    
        elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", mat.ambient)
    
        elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", mat.diffuse_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", mat.diffuse_intensity)
    
        elem_props_template_set(tmpl, props, "p_color", b"TransparentColor",
    
                                mat.diffuse_color if mat.use_transparency else (1.0, 1.0, 1.0))
    
        elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor",
                                1.0 - mat.alpha if mat.use_transparency else 0.0)
        elem_props_template_set(tmpl, props, "p_number", b"Opacity", mat.alpha if mat.use_transparency else 1.0)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
        # Not sure about those...
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        """
    
        b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
    
        b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
    
            elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", mat.specular_color)
    
            elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", mat.specular_intensity / 2.0)
            # See Material template about those two!
            elem_props_template_set(tmpl, props, "p_number", b"Shininess", (mat.specular_hardness - 1.0) / 5.10)
            elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", (mat.specular_hardness - 1.0) / 5.10)
    
            elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", mat.mirror_color)
    
            elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor",
                                    mat.raytrace_mirror.reflect_factor if mat.raytrace_mirror.use else 0.0)
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, mat)
    
    
    
    def _gen_vid_path(img, scene_data):
        msetts = scene_data.settings.media_settings
        fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode,
                                                       msetts.subdir, msetts.copy_set, img.library)
        fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel)))
        return fname_abs, fname_rel
    
    
    def fbx_data_texture_file_elements(root, tex, scene_data):
        """
        Write the (file) Texture data block.
        """
        # XXX All this is very fuzzy to me currently...
        #     Textures do not seem to use properties as much as they could.
        #     For now assuming most logical and simple stuff.
    
        tex_key, _mats = scene_data.data_textures[tex]
        img = tex.texture.image
        fname_abs, fname_rel = _gen_vid_path(img, scene_data)
    
    
        fbx_tex = elem_data_single_int64(root, b"Texture", get_fbx_uuid_from_key(tex_key))
    
        fbx_tex.add_string(fbx_name_class(tex.name.encode(), b"Texture"))
        fbx_tex.add_string(b"")
    
        elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip")
        elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION)
        elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(tex.name.encode(), b"Texture"))
        elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video"))
        elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs)
        elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel)
    
        alpha_source = 0  # None
        if img.use_alpha:
            if tex.texture.use_calculate_alpha:
                alpha_source = 1  # RGBIntensity as alpha.
            else:
                alpha_source = 2  # Black, i.e. alpha channel.
        # BlendMode not useful for now, only affects layered textures afaics.
        mapping = 0  # None.
        if tex.texture_coords in {'ORCO'}:  # XXX Others?
            if tex.mapping in {'FLAT'}:
                mapping = 1  # Planar
            elif tex.mapping in {'CUBE'}:
                mapping = 4  # Box
            elif tex.mapping in {'TUBE'}:
                mapping = 3  # Cylindrical
            elif tex.mapping in {'SPHERE'}:
                mapping = 2  # Spherical
        elif tex.texture_coords in {'UV'}:
            # XXX *HOW* do we link to correct UVLayer???
            mapping = 6  # UV
        wrap_mode = 1  # Clamp
        if tex.texture.extension in {'REPEAT'}:
            wrap_mode = 0  # Repeat
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"TextureFile")
    
        props = elem_properties(fbx_tex)
        elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source)
        elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha",
                                img.alpha_mode in {'STRAIGHT'})  # Or is it PREMUL?
        elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping)
        elem_props_template_set(tmpl, props, "p_enum", b"WrapModeU", wrap_mode)
        elem_props_template_set(tmpl, props, "p_enum", b"WrapModeV", wrap_mode)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Translation", tex.offset)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", tex.scale)
        elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", tex.texture.use_mipmap)
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, tex.texture)
    
    def fbx_data_video_elements(root, vid, scene_data):
        """
        Write the actual image data block.
        """
        vid_key, _texs = scene_data.data_videos[vid]
        fname_abs, fname_rel = _gen_vid_path(vid, scene_data)
    
    
        fbx_vid = elem_data_single_int64(root, b"Video", get_fbx_uuid_from_key(vid_key))
    
        fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video"))
        fbx_vid.add_string(b"Clip")
    
        elem_data_single_string(fbx_vid, b"Type", b"Clip")
        # XXX No Version???
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Video")
        props = elem_properties(fbx_vid)
        elem_props_template_set(tmpl, props, "p_string_url", b"Path", fname_abs)
        elem_props_template_finalize(tmpl, props)
    
        elem_data_single_int32(fbx_vid, b"UseMipMap", 0)
    
        elem_data_single_string_unicode(fbx_vid, b"FileName", fname_abs)
        elem_data_single_string_unicode(fbx_vid, b"RelativeFilename", fname_rel)
    
        if scene_data.settings.media_settings.embed_textures:
    
            if vid.packed_file is not None:
                elem_data_single_bytes(fbx_vid, b"Content", vid.packed_file.data)
            else:
                filepath = bpy.path.abspath(vid.filepath)
                try:
                    with open(filepath, 'br') as f:
                        elem_data_single_bytes(fbx_vid, b"Content", f.read())
                except Exception as e:
                    print("WARNING: embedding file {} failed ({})".format(filepath, e))
                    elem_data_single_bytes(fbx_vid, b"Content", b"")
    
            elem_data_single_bytes(fbx_vid, b"Content", b"")
    
    def fbx_data_armature_elements(root, arm_obj, scene_data):
    
        """
        Write:
            * Bones "data" (NodeAttribute::LimbNode, contains pretty much nothing!).
            * Deformers (i.e. Skin), bind between an armature and a mesh.
            ** SubDeformers (i.e. Cluster), one per bone/vgroup pair.
            * BindPose.
        Note armature itself has no data, it is a mere "Null" Model...
        """
    
        mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True)
    
        bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects)
    
            bo = bo_obj.bdata
            bo_data_key = scene_data.data_bones[bo_obj]
    
            fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(bo_data_key))
    
            fbx_bo.add_string(fbx_name_class(bo.name.encode(), b"NodeAttribute"))
            fbx_bo.add_string(b"LimbNode")
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
    
            tmpl = elem_props_template_init(scene_data.templates, b"Bone")
    
            props = elem_properties(fbx_bo)
    
            elem_props_template_set(tmpl, props, "p_double", b"Size", (bo.tail_local - bo.head_local).length)
    
    
            # Custom properties.
            if scene_data.settings.use_custom_properties:
    
                fbx_data_element_custom_properties(props, bo)
    
    
        # Deformers and BindPoses.
        # Note: we might also use Deformers for our "parent to vertex" stuff???
    
        deformer = scene_data.data_deformers.get(arm_obj, None)
    
        if deformer is not None:
    
            for me, (skin_key, ob_obj, clusters) in deformer.items():
    
                # BindPose.
                # We assume bind pose for our bones are their "Editmode" pose...
                # All matrices are expected in global (world) space.
    
                bindpose_key = get_blender_armature_bindpose_key(arm_obj.bdata, me)
    
                fbx_pose = elem_data_single_int64(root, b"Pose", get_fbx_uuid_from_key(bindpose_key))
    
                fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose"))
                fbx_pose.add_string(b"BindPose")
    
                elem_data_single_string(fbx_pose, b"Type", b"BindPose")
                elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION)
    
                elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + len(bones))
    
    
                # First node is mesh/object.
    
                mat_world_obj = ob_obj.fbx_object_matrix(scene_data, global_space=True)
    
                fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
    
                elem_data_single_int64(fbx_posenode, b"Node", ob_obj.fbx_uuid)
    
                elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(mat_world_obj))
                # And all bones of armature!
                mat_world_bones = {}
    
                    bomat = bo_obj.fbx_object_matrix(scene_data, rest=True, global_space=True)
    
                    fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
    
                    elem_data_single_int64(fbx_posenode, b"Node", bo_obj.fbx_uuid)
    
                    elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(bomat))
    
                # Deformer.
    
                fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key))
    
                fbx_skin.add_string(fbx_name_class(arm_obj.name.encode(), b"Deformer"))
    
                fbx_skin.add_string(b"Skin")
    
                elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
                elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0)  # Only vague idea what it is...
    
    
                # Pre-process vertex weights (also to check vertices assigned ot more than four bones).
    
                ob = ob_obj.bdata
                bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
                             for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
    
                vgroups = {vg.index: OrderedDict() for vg in ob.vertex_groups}
    
                verts_vgroups = (sorted(((vg.group, vg.weight) for vg in v.groups if vg.weight and vg.group in valid_idxs),
                                        key=lambda e: e[1], reverse=True)
                                 for v in me.vertices)
                for idx, vgs in enumerate(verts_vgroups):
                    for vg_idx, w in vgs:
                        vgroups[vg_idx][idx] = w
    
    
                for bo_obj, clstr_key in clusters.items():
                    bo = bo_obj.bdata
    
                    # Find which vertices are affected by this bone/vgroup pair, and matching weights.
    
                    # Note we still write a cluster for bones not affecting the mesh, to get 'rest pose' data
                    # (the TransformBlah matrices).
                    vg_idx = bo_vg_idx.get(bo.name, None)
    
                    indices, weights = ((), ()) if vg_idx is None or not vgroups[vg_idx] else zip(*vgroups[vg_idx].items())
    
                    fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(clstr_key))
    
                    fbx_clstr.add_string(fbx_name_class(bo.name.encode(), b"SubDeformer"))
                    fbx_clstr.add_string(b"Cluster")
    
                    elem_data_single_int32(fbx_clstr, b"Version", FBX_DEFORMER_CLUSTER_VERSION)
                    # No idea what that user data might be...
                    fbx_userdata = elem_data_single_string(fbx_clstr, b"UserData", b"")
                    fbx_userdata.add_string(b"")
    
                    elem_data_single_int32_array(fbx_clstr, b"Indexes", indices)
                    elem_data_single_float64_array(fbx_clstr, b"Weights", weights)
    
                    # Transform, TransformLink and TransformAssociateModel matrices...
                    # They seem to be doublons of BindPose ones??? Have armature (associatemodel) in addition, though.
    
                    # WARNING! Even though official FBX API presents Transform in global space,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    #          **it is stored in bone space in FBX data!** See:
                    #          http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/why-the-values-return-
                    #                 by-fbxcluster-gettransformmatrix-x-not-same-with-the-value-in-ascii-fbx-file/
    
                    elem_data_single_float64_array(fbx_clstr, b"Transform",
    
                                                   matrix_to_array(mat_world_bones[bo_obj].inverted() * mat_world_obj))
                    elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix_to_array(mat_world_bones[bo_obj]))
    
                    elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix_to_array(mat_world_arm))
    
    def fbx_data_object_elements(root, ob_obj, scene_data):
    
        """
        Write the Object (Model) data blocks.
    
        Note this "Model" can also be bone or dupli!
    
        """
        obj_type = b"Null"  # default, sort of empty...
    
            obj_type = b"LimbNode"
    
        elif (ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE):
    
            obj_type = b"Light"
    
            obj_type = b"Camera"
    
        model = elem_data_single_int64(root, b"Model", ob_obj.fbx_uuid)
        model.add_string(fbx_name_class(ob_obj.name.encode(), b"Model"))
    
        model.add_string(obj_type)
    
        elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
    
        # Object transform info.
    
        loc, rot, scale, matrix, matrix_rot = ob_obj.fbx_object_tx(scene_data)
    
        rot = tuple(units_convert_iter(rot, "radian", "degree"))
    
        tmpl = elem_props_template_init(scene_data.templates, b"Model")
    
        # For now add only loc/rot/scale...
        props = elem_properties(model)
        elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc)
        elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot)
        elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale)
    
        elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not ob_obj.hide))
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, ob_obj.bdata)
    
    
        # Those settings would obviously need to be edited in a complete version of the exporter, may depends on
        # object type, etc.
        elem_data_single_int32(model, b"MultiLayer", 0)
        elem_data_single_int32(model, b"MultiTake", 0)
        elem_data_single_bool(model, b"Shading", True)
        elem_data_single_string(model, b"Culling", b"CullingOff")
    
    
            # Why, oh why are FBX cameras such a mess???
            # And WHY add camera data HERE??? Not even sure this is needed...
            render = scene_data.scene.render
            width = render.resolution_x * 1.0
            height = render.resolution_y * 1.0
            elem_props_template_set(tmpl, props, "p_enum", b"ResolutionMode", 0)  # Don't know what it means
    
            elem_props_template_set(tmpl, props, "p_double", b"AspectW", width)
            elem_props_template_set(tmpl, props, "p_double", b"AspectH", height)
    
            elem_props_template_set(tmpl, props, "p_bool", b"ViewFrustum", True)
            elem_props_template_set(tmpl, props, "p_enum", b"BackgroundMode", 0)  # Don't know what it means
            elem_props_template_set(tmpl, props, "p_bool", b"ForegroundTransparent", True)
    
    
    def fbx_data_animation_elements(root, scene_data):
        """
        Write animation data.
        """
        animations = scene_data.animations
        if not animations:
            return
        scene = scene_data.scene
    
        fps = scene.render.fps / scene.render.fps_base
    
        def keys_to_ktimes(keys):
            return (int(v) for v in units_convert_iter((f / fps for f, _v in keys), "second", "ktime"))
    
    
        # Animation stacks.
        for astack_key, alayers, alayer_key, name, f_start, f_end in animations:
    
            astack = elem_data_single_int64(root, b"AnimationStack", get_fbx_uuid_from_key(astack_key))
    
            astack.add_string(fbx_name_class(name, b"AnimStack"))
            astack.add_string(b"")
    
            astack_tmpl = elem_props_template_init(scene_data.templates, b"AnimationStack")
            astack_props = elem_properties(astack)
            r = scene_data.scene.render
            fps = r.fps / r.fps_base
            start = int(units_convert(f_start / fps, "second", "ktime"))
            end = int(units_convert(f_end / fps, "second", "ktime"))
            elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStart", start)
            elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStop", end)
            elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStart", start)
            elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStop", end)
            elem_props_template_finalize(astack_tmpl, astack_props)
    
            # For now, only one layer for all animations.
    
            alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
    
            alayer.add_string(fbx_name_class(name, b"AnimLayer"))
            alayer.add_string(b"")
    
    
            for ob_obj, (alayer_key, acurvenodes) in alayers.items():
    
                # alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
    
                # alayer.add_string(fbx_name_class(ob_obj.name.encode(), b"AnimLayer"))
    
                for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
                    # Animation curve node.
    
                    acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbx_uuid_from_key(acurvenode_key))
    
                    acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode"))
                    acurvenode.add_string(b"")
    
                    acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
                    acn_props = elem_properties(acurvenode)
    
                    for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
    
                        elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(),
                                                def_value, animatable=True)
    
                            acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
    
                            acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
                            acurve.add_string(b"")
    
                            # key attributes...
                            nbr_keys = len(keys)
                            # flags...
                            keyattr_flags = (
    
                                1 << 2 |   # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
    
                                1 << 8 |   # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
                                1 << 13 |  # tangent mode, 12 = generic clamp, 13 = generic time independent,
                                1 << 14 |  # tangent mode, 13 + 14 = generic clamp progressive.
                                0,
                            )
                            # Maybe values controlling TCB & co???
                            keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
    
                            # And now, the *real* data!
                            elem_data_single_float64(acurve, b"Default", def_value)
                            elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
                            elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
                            elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
                            elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
                            elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
                            elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
    
                    elem_props_template_finalize(acn_tmpl, acn_props)
    
    ##### Top-level FBX data container. #####
    
    def fbx_mat_properties_from_texture(tex):
        """
        Returns a set of FBX metarial properties that are affected by the given texture.
        Quite obviously, this is a fuzzy and far-from-perfect mapping! Amounts of influence are completely lost, e.g.
        Note tex is actually expected to be a texture slot.
        """
        # Mapping Blender -> FBX (blend_use_name, blend_fact_name, fbx_name).
        blend_to_fbx = (
            # Lambert & Phong...
            ("diffuse", "diffuse", b"DiffuseFactor"),
            ("color_diffuse", "diffuse_color", b"DiffuseColor"),
            ("alpha", "alpha", b"TransparencyFactor"),
            ("diffuse", "diffuse", b"TransparentColor"),  # Uses diffuse color in Blender!
            ("emit", "emit", b"EmissiveFactor"),
            ("diffuse", "diffuse", b"EmissiveColor"),  # Uses diffuse color in Blender!
            ("ambient", "ambient", b"AmbientFactor"),
            #("", "", b"AmbientColor"),  # World stuff in Blender, for now ignore...
    
            ("normal", "normal", b"NormalMap"),
            # Note: unsure about those... :/
    
            #("", "", b"Bump"),
            #("", "", b"BumpFactor"),
            #("", "", b"DisplacementColor"),
            #("", "", b"DisplacementFactor"),
            # Phong only.
            ("specular", "specular", b"SpecularFactor"),
            ("color_spec", "specular_color", b"SpecularColor"),
            # See Material template about those two!
            ("hardness", "hardness", b"Shininess"),
            ("hardness", "hardness", b"ShininessExponent"),
            ("mirror", "mirror", b"ReflectionColor"),
            ("raymir", "raymir", b"ReflectionFactor"),
        )
    
        tex_fbx_props = set()
        for use_map_name, name_factor, fbx_prop_name in blend_to_fbx:
    
            # Always export enabled textures, even if they have a null influence...
            if getattr(tex, "use_map_" + use_map_name):
    
                tex_fbx_props.add(fbx_prop_name)
    
        return tex_fbx_props
    
    
    
    def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
    
                                   data_bones, data_deformers, arm_parents):
    
        """
        Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh,
        create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster).
        Also supports "parent to bone" (simple parent to Model/LimbNode).
        arm_parents is a set of tuples (armature, object) for all successful armature bindings.
        """
    
            if settings.use_armature_deform_only:
                if bo.bdata.use_deform:
                    bones[bo] = True
                    bo_par = bo.parent
                    while bo_par.is_bone:
                        bones[bo_par] = True
                        bo_par = bo_par.parent
                elif bo not in bones:  # Do not override if already set in the loop above!
                    bones[bo] = False
            else:
                bones[bo] = True
    
        bones = OrderedDict((bo, None) for bo, use in bones.items() if use)
    
        data_bones.update((bo, get_blender_bone_key(arm_obj.bdata, bo.bdata)) for bo in bones)
    
    
            if not (ob_obj.is_object and ob_obj.type == 'MESH' and ob_obj.parent == arm_obj):
    
                continue
    
            # Always handled by an Armature modifier...
    
                if mod.type not in {'ARMATURE'}:
                    continue
                # We only support vertex groups binding method, not bone envelopes one!
    
                if mod.object == arm_obj.bdata and mod.use_vertex_groups:
    
            # Note: bindpose have no relations at all (no connections), so no need for any preprocess for them.
            # Create skin & clusters relations (note skins are connected to geometry, *not* model!).
    
            _key, me, _free = data_meshes[ob]
            clusters = OrderedDict((bo, get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata)) for bo in bones)
            data_deformers.setdefault(arm_obj, OrderedDict())[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
                                                                     ob_obj, clusters)
    
    
            # We don't want a regular parent relationship for those in FBX...
    
            arm_parents.add((arm_obj, ob_obj))
    
        objects.update(bones)
    
    def fbx_animations_simplify(scene_data, animdata):
        """
        Simplifies FCurves!
        """
        fac = scene_data.settings.bake_anim_simplify_factor
        step = scene_data.settings.bake_anim_step
        # So that, with default factor and step values (1), we get:
        max_frame_diff = step * fac * 10  # max step of 10 frames.
        value_diff_fac = fac / 1000  # min value evolution: 0.1% of whole range.
    
            if not keys:
                continue
            extremums = [(min(values), max(values)) for values in zip(*(k[1] for k in keys))]
    
            min_diffs = [max((mx - mn) * value_diff_fac, min_significant_diff) for mn, mx in extremums]
    
            p_currframe, p_key, p_key_write = keys[0]
            p_keyed = [(p_currframe - max_frame_diff, val) for val in p_key]
    
            for currframe, key, key_write in keys:
                for idx, (val, p_val) in enumerate(zip(key, p_key)):
                    p_keyedframe, p_keyedval = p_keyed[idx]
                    if val == p_val:
                        # Never write keyframe when value is exactly the same as prev one!
                        continue
                    if abs(val - p_val) >= min_diffs[idx]:
                        # If enough difference from previous sampled value, key this value *and* the previous one!
                        key_write[idx] = True
                        p_key_write[idx] = True
                        p_keyed[idx] = (currframe, val)
    
                        are_keyed[idx] = True
                    else:
                        frame_diff = currframe - p_keyedframe
                        val_diff = abs(val - p_keyedval)
                        if ((val_diff >= min_diffs[idx]) or
                            ((val_diff >= min_significant_diff) and (frame_diff >= max_frame_diff))):
                            # Else, if enough difference from previous keyed value
                            # (or any significant difference and max gap between keys is reached),
                            # key this value only!
                            key_write[idx] = True
                            p_keyed[idx] = (currframe, val)
                            are_keyed[idx] = True
    
                p_currframe, p_key, p_key_write = currframe, key, key_write
    
            # If we did key something, ensure first and last sampled values are keyed as well.
            for idx, is_keyed in enumerate(are_keyed):
                if is_keyed:
                    keys[0][2][idx] = keys[-1][2][idx] = True
    
    def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False):
    
        Generate animation data (a single AnimStack) from objects, for a given frame range.
    
        bake_step = scene_data.settings.bake_anim_step
        scene = scene_data.scene
    
    
            for ob_obj in tuple(objects):
                if not ob_obj.is_object:
    
                    objects |= {bo_obj for bo_obj in ob_obj.bones if bo_obj in scene_data.objects}
    
                ob_obj.dupli_list_create(scene, 'RENDER')
                for dp_obj in ob_obj.dupli_list:
                    if dp_obj in scene_data.objects:
                        objects.add(dp_obj)
                ob_obj.dupli_list_clear()
    
    
        # FBX mapping info: Property affected, and name of the "sub" property (to distinguish e.g. vector's channels).
        fbx_names = (
    
            ("Lcl Translation", "T", "d|X"), ("Lcl Translation", "T", "d|Y"), ("Lcl Translation", "T", "d|Z"),
            ("Lcl Rotation", "R", "d|X"), ("Lcl Rotation", "R", "d|Y"), ("Lcl Rotation", "R", "d|Z"),
            ("Lcl Scaling", "S", "d|X"), ("Lcl Scaling", "S", "d|Y"), ("Lcl Scaling", "S", "d|Z"),
    
        )
    
        back_currframe = scene.frame_current
    
        animdata = OrderedDict((obj, []) for obj in objects)
    
            real_currframe = currframe - f_start if start_zero else currframe
    
            scene.frame_set(int(currframe), currframe - int(currframe))
    
            for ob_obj in objects:
                ob_obj.dupli_list_create(scene, 'RENDER')
            for ob_obj in objects:
    
                # We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
    
                p_rot = p_rots.get(ob_obj, None)
                loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
                p_rots[ob_obj] = rot
    
                tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
    
                animdata[ob_obj].append((real_currframe, tx, [False] * len(tx)))
            for ob_obj in objects:
                ob_obj.dupli_list_clear()
    
            currframe += bake_step
    
        scene.frame_set(back_currframe, 0.0)
    
        fbx_animations_simplify(scene_data, animdata)
    
    
    
        # And now, produce final data (usable by FBX export code)...
    
            if not keys:
                continue
            curves = [[] for k in keys[0][1]]
            for currframe, key, key_write in keys:
                for idx, (val, wrt) in enumerate(zip(key, key_write)):
                    if wrt:
                        curves[idx].append((currframe, val))
    
    
            # Get PoseBone from bone...
    
            #tobj = bone_map[obj] if isinstance(obj, Bone) else obj
            #loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj)
            #tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
            dtx = (0.0, 0.0, 0.0) + (0.0, 0.0, 0.0) + (1.0, 1.0, 1.0)
    
            # If animation for a channel, (True, keyframes), else (False, current value).
    
            for idx, c in enumerate(curves):
    
                fbx_group, fbx_gname, fbx_item = fbx_names[idx]
    
                fbx_item_key = get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_group, fbx_item)
    
                if fbx_group not in final_keys:
    
                    fbx_group_key = get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_group)
    
                    final_keys[fbx_group] = (fbx_group_key, OrderedDict(), fbx_gname)
    
                final_keys[fbx_group][1][fbx_item] = (fbx_item_key, dtx[idx], c,
                                                      True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
    
            # And now, remove anim groups (i.e. groups of curves affecting a single FBX property) with no curve at all!
            del_groups = []
    
            for grp, (_k, data, _n) in final_keys.items():
                if True in (d[3] for d in data.values()):
    
                    continue
                del_groups.append(grp)
            for grp in del_groups:
                del final_keys[grp]
    
            if final_keys:
    
                #animations[obj] = (get_blender_anim_layer_key(scene, obj.bdata), final_keys)
                animations[ob_obj] = ("dummy_unused_key", final_keys)
    
    
        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
    
    
    def fbx_animations_objects(scene_data):
        """
        Generate global animation data from objects.
        """
        scene = scene_data.scene
        animations = []
    
        frame_start = 1e100
        frame_end = -1e100
    
        def add_anim(animations, anim):
            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
    
    
        # 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
                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
    
                add_anim(animations, fbx_animations_objects_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
    
        # 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', 'layers', 'select', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
                    'matrix_parent_inverse', 'empty_draw_type', 'empty_draw_size', 'empty_image_offset', 'pass_index',
                    'color', 'hide', 'hide_select', 'hide_render', 'use_slow_parent', 'slow_parent_offset',
                    'use_extra_recalc_object', 'use_extra_recalc_data', 'dupli_type', 'use_dupli_frames_speed',
                    'use_dupli_vertices_rotation', 'use_dupli_faces_scale', 'dupli_faces_scale', 'dupli_group',
                    'dupli_frames_start', 'dupli_frames_end', 'dupli_frames_on', 'dupli_frames_off',
                    'draw_type', 'show_bounds', 'draw_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
                    'show_wire', 'show_all_edges', 'show_transparent', 'show_x_ray',
                    'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
                )
                for p in props:
    
                # Actions only for objects, not bones!
    
                ob = ob_obj.bdata  # Back to real Blender Object.
    
    
                # 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... :/
    
                if ob.animation_data:
                    org_act = ob.animation_data.action
    
                    ob.animation_data_create()
                path_resolve = ob.path_resolve
    
    
                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!
                    add_anim(animations,
    
                             fbx_animations_objects_do(scene_data, (ob, act), frame_start, frame_end, True, {ob_obj}, True))
    
                    ob.animation_data.action = None if org_act is ... else org_act
                    restore_object(ob, ob_copy)
    
        # Global (containing everything) animstack.
        if not scene_data.settings.bake_anim_use_nla_strips or not animations:
    
            add_anim(animations, fbx_animations_objects_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, 0.0)
    
    
        return animations, frame_start, frame_end
    
    def fbx_data_from_scene(scene, settings):
        """
        Do some pre-processing over scene's data...
        """
        objtypes = settings.object_types
    
        ##### Gathering data...
    
        # This is rather simple for now, maybe we could end generating templates with most-used values
        # instead of default ones?
    
        objects = OrderedDict()  # 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...
            ob_obj.dupli_list_create(scene, 'RENDER')
            for dp_obj in ob_obj.dupli_list:
                objects[dp_obj] = None
            ob_obj.dupli_list_clear()
    
        data_lamps = OrderedDict((ob_obj.bdata.data, get_blenderID_key(ob_obj.bdata.data))
                                 for ob_obj in objects if ob_obj.type == 'LAMP')
    
        # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
    
        data_cameras = OrderedDict((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 = OrderedDict((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
            if ob in data_meshes:  # Happens with dupli instances.
    
            use_org_data = True
    
            if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES:
    
                use_org_data = False
    
                    # No need to create a new mesh in this case, if no modifier is active!
                    use_org_data = True
    
                        # For meshes, when armature export is enabled, disable Armature modifiers here!
                        if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
    
                            tmp_mods.append((mod, mod.show_render))
                            mod.show_render = False
    
                        if mod.show_render:
                            use_org_data = False
                if not use_org_data:
    
                    tmp_me = ob.to_mesh(scene, apply_modifiers=True, settings='RENDER')
                    data_meshes[ob] = (get_blenderID_key(tmp_me), tmp_me, True)
    
                # Re-enable temporary disabled modifiers.
                for mod, show_render in tmp_mods:
                    mod.show_render = show_render
    
                data_meshes[ob] = (get_blenderID_key(ob.data), ob.data, False)
    
        data_bones = OrderedDict()
        data_deformers = OrderedDict()
    
        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, arm_parents)
    
    
        # Some world settings are embedded in FBX materials...
        if scene.world:
    
            data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
    
    
        # TODO: Check all the mat stuff works even when mats 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 mat_s in ob_obj.material_slots:
    
                mat = mat_s.material
    
                if mat 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!
    
                # We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing.
    
                # TODO: Support nodes (*BIG* todo!).
    
                if mat.type in {'SURFACE'} and not mat.use_nodes:
    
                    if mat in data_materials:
    
                        data_materials[mat] = (get_blenderID_key(mat), [ob_obj])
    
    
        # 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 mat in data_materials.keys():
    
            for tex, use_tex in zip(mat.texture_slots, mat.use_textures):
                if tex is None or not use_tex:
    
                    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), OrderedDict(((mat, tex_fbx_props),)))
    
                if img in data_videos:
                    data_videos[img][1].append(tex)
                else:
                    data_videos[img] = (get_blenderID_key(img), [tex])
    
    
        frame_start = scene.frame_start
        frame_end = scene.frame_end
    
        if settings.bake_anim:
            # From objects & bones 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, 0.0, 0.0,