Skip to content
Snippets Groups Projects
export_fbx_bin.py 153 KiB
Newer Older
  • Learn to ignore specific revisions
  •             _map = b"ByEdge"
            lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0)
            elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION)
            elem_data_single_string(lay_smooth, b"Name", b"")
            elem_data_single_string(lay_smooth, b"MappingInformationType", _map)
            elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct")
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps)  # Sight, int32 for bool...
    
        # Edge crease for subdivision
        if write_crease:
            t_ec = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * edges_nbr
            for e in me.edges:
                if e.key not in edges_map:
                    continue  # Only loose edges, in theory!
    
                # Blender squares those values before sending them to OpenSubdiv, when other softwares don't,
                # so we need to compensate that to get similar results through FBX...
                t_ec[edges_map[e.key]] = e.crease * e.crease
    
    
            lay_crease = elem_data_single_int32(geom, b"LayerElementEdgeCrease", 0)
            elem_data_single_int32(lay_crease, b"Version", FBX_GEOMETRY_CREASE_VERSION)
            elem_data_single_string(lay_crease, b"Name", b"")
            elem_data_single_string(lay_crease, b"MappingInformationType", b"ByEdge")
            elem_data_single_string(lay_crease, b"ReferenceInformationType", b"Direct")
            elem_data_single_float64_array(lay_crease, b"EdgeCrease", t_ec)
            del t_ec
    
    
        # And we are done with edges!
        del edges_map
    
        # Loop normals.
    
            # NOTE: this is not supported by importer currently.
            # XXX Official docs says normals should use IndexToDirect,
            #     but this does not seem well supported by apps currently...
    
    
            t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
            me.loops.foreach_get("normal", t_ln)
    
            t_ln = nors_transformed_gen(t_ln, geom_mat_no)
    
            if 0:
                t_ln = tuple(t_ln)  # No choice... :/
    
                lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
                elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
                elem_data_single_string(lay_nor, b"Name", b"")
                elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect")
    
                ln2idx = tuple(set(t_ln))
                elem_data_single_float64_array(lay_nor, b"Normals", chain(*ln2idx))
                # Normal weights, no idea what it is.
                # t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(ln2idx)
                # elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw)
    
                ln2idx = {nor: idx for idx, nor in enumerate(ln2idx)}
                elem_data_single_int32_array(lay_nor, b"NormalsIndex", (ln2idx[n] for n in t_ln))
    
                del ln2idx
    
            else:
                lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
                elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
                elem_data_single_string(lay_nor, b"Name", b"")
                elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
                elem_data_single_float64_array(lay_nor, b"Normals", chain(*t_ln))
                # Normal weights, no idea what it is.
                # t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
                # elem_data_single_float64_array(lay_nor, b"NormalsW", t_ln)
            del t_ln
    
            # tspace
            if scene_data.settings.use_tspace:
                tspacenumber = len(me.uv_layers)
                if tspacenumber:
    
                    # We can only compute tspace on tessellated meshes, need to check that here...
    
                    t_lt = [None] * len(me.polygons)
                    me.polygons.foreach_get("loop_total", t_lt)
                    if any((lt > 4 for lt in t_lt)):
                        del t_lt
                        scene_data.settings.report(
                            {'WARNING'},
                            "Mesh '%s' has polygons with more than 4 vertices, "
                            "cannot compute/export tangent space for it" % me.name)
                    else:
                        del t_lt
                        t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
                        # t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
                        uv_names = [uvlayer.name for uvlayer in me.uv_layers]
                        for name in uv_names:
                            me.calc_tangents(uvmap=name)
                        for idx, uvlayer in enumerate(me.uv_layers):
                            name = uvlayer.name
                            # Loop bitangents (aka binormals).
                            # NOTE: this is not supported by importer currently.
                            me.loops.foreach_get("bitangent", t_ln)
                            lay_nor = elem_data_single_int32(geom, b"LayerElementBinormal", idx)
                            elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_BINORMAL_VERSION)
                            elem_data_single_string_unicode(lay_nor, b"Name", name)
                            elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                            elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
                            elem_data_single_float64_array(lay_nor, b"Binormals",
                                                           chain(*nors_transformed_gen(t_ln, geom_mat_no)))
                            # Binormal weights, no idea what it is.
                            # elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw)
    
                            # Loop tangents.
                            # NOTE: this is not supported by importer currently.
                            me.loops.foreach_get("tangent", t_ln)
                            lay_nor = elem_data_single_int32(geom, b"LayerElementTangent", idx)
                            elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_TANGENT_VERSION)
                            elem_data_single_string_unicode(lay_nor, b"Name", name)
                            elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                            elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
                            elem_data_single_float64_array(lay_nor, b"Tangents",
                                                           chain(*nors_transformed_gen(t_ln, geom_mat_no)))
                            # Tangent weights, no idea what it is.
                            # elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw)
    
                        del t_ln
                        # del t_lnw
                        me.free_tangents()
    
        vcolnumber = len(me.vertex_colors)
        if vcolnumber:
            def _coltuples_gen(raw_cols):
    
                return zip(*(iter(raw_cols),) * 4)
    
            t_lc = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 4
    
            for colindex, collayer in enumerate(me.vertex_colors):
                collayer.data.foreach_get("color", t_lc)
                lay_vcol = elem_data_single_int32(geom, b"LayerElementColor", colindex)
                elem_data_single_int32(lay_vcol, b"Version", FBX_GEOMETRY_VCOLOR_VERSION)
                elem_data_single_string_unicode(lay_vcol, b"Name", collayer.name)
                elem_data_single_string(lay_vcol, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_vcol, b"ReferenceInformationType", b"IndexToDirect")
    
                col2idx = tuple(set(_coltuples_gen(t_lc)))
                elem_data_single_float64_array(lay_vcol, b"Colors", chain(*col2idx))  # Flatten again...
    
                col2idx = {col: idx for idx, col in enumerate(col2idx)}
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                elem_data_single_int32_array(lay_vcol, b"ColorIndex", (col2idx[c] for c in _coltuples_gen(t_lc)))
    
                del col2idx
            del t_lc
            del _coltuples_gen
    
        # Write UV layers.
        # Note: LayerElementTexture is deprecated since FBX 2011 - luckily!
        #       Textures are now only related to materials, in FBX!
        uvnumber = len(me.uv_layers)
        if uvnumber:
    
            # Looks like this mapping is also expected to convey UV islands (arg..... :((((( ).
            # So we need to generate unique triplets (uv, vertex_idx) here, not only just based on UV values.
            def _uvtuples_gen(raw_uvs, raw_lvidxs):
                return zip(zip(*(iter(raw_uvs),) * 2), raw_lvidxs)
    
            t_luv = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 2
    
            t_lvidx = array.array(data_types.ARRAY_INT32, (0,)) * len(me.loops)
            me.loops.foreach_get("vertex_index", t_lvidx)
    
            for uvindex, uvlayer in enumerate(me.uv_layers):
                uvlayer.data.foreach_get("uv", t_luv)
                lay_uv = elem_data_single_int32(geom, b"LayerElementUV", uvindex)
                elem_data_single_int32(lay_uv, b"Version", FBX_GEOMETRY_UV_VERSION)
                elem_data_single_string_unicode(lay_uv, b"Name", uvlayer.name)
                elem_data_single_string(lay_uv, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_uv, b"ReferenceInformationType", b"IndexToDirect")
    
    
                uv_ids = tuple(set(_uvtuples_gen(t_luv, t_lvidx)))
                elem_data_single_float64_array(lay_uv, b"UV", chain(*(uv for uv, vidx in uv_ids)))  # Flatten again...
    
                uv2idx = {uv_id: idx for idx, uv_id in enumerate(uv_ids)}
                elem_data_single_int32_array(lay_uv, b"UVIndex", (uv2idx[uv_id] for uv_id in _uvtuples_gen(t_luv, t_lvidx)))
    
            del _uvtuples_gen
    
        # Face's materials.
    
        me_fbxmaterials_idx = scene_data.mesh_material_indices.get(me)
        if me_fbxmaterials_idx is not None:
    
            # We cannot use me.materials here, as this array is filled with None in case materials are linked to object...
            me_blmaterials = [mat_slot.material for mat_slot in me_obj.material_slots]
    
            if me_fbxmaterials_idx and me_blmaterials:
                lay_ma = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
                elem_data_single_int32(lay_ma, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
                elem_data_single_string(lay_ma, b"Name", b"")
                nbr_mats = len(me_fbxmaterials_idx)
    
                    t_pm = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
    
                    me.polygons.foreach_get("material_index", t_pm)
    
                    # We have to validate mat indices, and map them to FBX indices.
    
                    # Note a mat might not be in me_fbxmats_idx (e.g. node mats are ignored).
    
                    blmaterials_to_fbxmaterials_idxs = [me_fbxmaterials_idx[m]
                                                        for m in me_blmaterials if m in me_fbxmaterials_idx]
                    ma_idx_limit = len(blmaterials_to_fbxmaterials_idxs)
                    def_ma = blmaterials_to_fbxmaterials_idxs[0]
                    _gen = (blmaterials_to_fbxmaterials_idxs[m] if m < ma_idx_limit else def_ma for m in t_pm)
    
                    t_pm = array.array(data_types.ARRAY_INT32, _gen)
    
    
                    elem_data_single_string(lay_ma, b"MappingInformationType", b"ByPolygon")
    
                    # XXX Logically, should be "Direct" reference type, since we do not have any index array, and have one
                    #     value per polygon...
                    #     But looks like FBX expects it to be IndexToDirect here (maybe because materials are already
                    #     indices??? *sigh*).
    
                    elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
                    elem_data_single_int32_array(lay_ma, b"Materials", t_pm)
    
                    elem_data_single_string(lay_ma, b"MappingInformationType", b"AllSame")
                    elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
                    elem_data_single_int32_array(lay_ma, b"Materials", [0])
    
    
        # And the "layer TOC"...
    
        layer = elem_data_single_int32(geom, b"Layer", 0)
        elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
    
        if write_normals:
            lay_nor = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_nor, b"Type", b"LayerElementNormal")
            elem_data_single_int32(lay_nor, b"TypedIndex", 0)
    
        if tspacenumber:
            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", 0)
            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", 0)
    
        if smooth_type in {'FACE', 'EDGE'}:
            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 write_crease:
            lay_smooth = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_smooth, b"Type", b"LayerElementEdgeCrease")
            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_fbxmaterials_idx is not None:
            lay_ma = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_ma, b"Type", b"LayerElementMaterial")
            elem_data_single_int32(lay_ma, 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)
    
    
        # Shape keys...
        fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, tmpl, props)
    
        elem_props_template_finalize(tmpl, props)
    
        done_meshes.add(me_key)
    
    
    def fbx_data_material_elements(root, ma, 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())).color
    
        ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
        ma_key, _objs = scene_data.data_materials[ma]
        ma_type = b"Phong"
    
        fbx_ma = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(ma_key))
        fbx_ma.add_string(fbx_name_class(ma.name.encode(), b"Material"))
        fbx_ma.add_string(b"")
    
        elem_data_single_int32(fbx_ma, b"Version", FBX_MATERIAL_VERSION)
    
        # those are not yet properties, it seems...
    
        elem_data_single_string(fbx_ma, b"ShadingModel", ma_type)
        elem_data_single_int32(fbx_ma, b"MultiLayer", 0)  # Should be bool...
    
        tmpl = elem_props_template_init(scene_data.templates, b"Material")
    
        props = elem_properties(fbx_ma)
    
        elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", ma_type.decode())
        elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", ma_wrap.base_color)
        # Not in Principled BSDF, so assuming always 1
        elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", 1.0)
    
        # Principled BSDF only has an emissive color, so we assume factor to be always 1.0.
        elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", ma_wrap.emission_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", ma_wrap.emission_strength)
    
        # Not in Principled BSDF, so assuming always 0
        elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
        elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", 0.0)
    
        # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
        # According to one of its developers, Unity uses that formula to extract alpha value:
        #
        #   alpha = 1 - TransparencyFactor
        #   if (alpha == 1 or alpha == 0):
        #       alpha = 1 - TransparentColor.r
        #
        # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
    
        if ma_wrap.alpha < 1.0e-5 or ma_wrap.alpha > (1.0 - 1.0e-5):
            elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", (1.0 - ma_wrap.alpha,) * 3)
    
        else:
            elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", ma_wrap.base_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor", 1.0 - ma_wrap.alpha)
        elem_props_template_set(tmpl, props, "p_number", b"Opacity", ma_wrap.alpha)
    
        elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
    
        elem_props_template_set(tmpl, props, "p_double", b"BumpFactor", ma_wrap.normalmap_strength)
    
        # Not sure about those...
        """
        b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
        b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
        b"DisplacementFactor": (0.0, "p_double"),
        """
        # TODO: use specular tint?
        elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", ma_wrap.base_color)
        elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", ma_wrap.specular / 2.0)
        # See Material template about those two!
        # XXX Totally empirical conversion, trying to adapt it
        #     (from 0.0 - 100.0 FBX shininess range to 1.0 - 0.0 Principled BSDF range)...
        shininess = (1.0 - ma_wrap.roughness) * 10
        shininess *= shininess
        elem_props_template_set(tmpl, props, "p_number", b"Shininess", shininess)
        elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", shininess)
        elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", ma_wrap.base_color)
        elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor", ma_wrap.metallic)
    
        # Custom properties.
    
        if scene_data.settings.use_custom_props:
    
            fbx_data_element_custom_properties(props, ma)
    
    
    
    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, blender_tex_key, 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.
    
    
        ma, sock_name = blender_tex_key
        ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
        tex_key, _fbx_prop = scene_data.data_textures[blender_tex_key]
        tex = getattr(ma_wrap, sock_name)
        img = tex.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(sock_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(sock_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.alpha_mode != 'NONE':
    
            # ~ if tex.texture.use_calculate_alpha:
                # ~ alpha_source = 1  # RGBIntensity as alpha.
            # ~ else:
                # ~ alpha_source = 2  # Black, i.e. alpha channel.
            alpha_source = 2  # Black, i.e. alpha channel.
    
        # BlendMode not useful for now, only affects layered textures afaics.
    
        if tex.texcoords == 'ORCO':  # XXX Others?
            if tex.projection == 'FLAT':
    
                mapping = 1  # Planar
    
            elif tex.projection == 'CUBE':
    
            elif tex.projection == 'TUBE':
    
                mapping = 3  # Cylindrical
    
            elif tex.projection == 'SPHERE':
    
                mapping = 2  # Spherical
    
        elif tex.texcoords == 'UV':
    
            mapping = 0  # UV
            # Yuck, UVs are linked by mere names it seems... :/
    
            # XXX TODO how to get that now???
            # uvset = tex.uv_layer
    
        wrap_mode = 1  # Clamp
    
        if tex.extension == '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)
    
        if uvset is not None:
            elem_props_template_set(tmpl, props, "p_string", b"UVSet", uvset)
    
        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.translation)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Rotation", (-r for r in tex.rotation))
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", (((1.0 / s) if s != 0.0 else 1.0) for s in tex.scale))
    
        # UseMaterial should always be ON imho.
        elem_props_template_set(tmpl, props, "p_bool", b"UseMaterial", True)
    
        elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", False)
    
        # No custom properties, since that's not a data-block anymore.
    
    def fbx_data_video_elements(root, vid, scene_data):
        """
        Write the actual image data block.
        """
    
        msetts = scene_data.settings.media_settings
    
    
        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:
    
                # We only ever embed a given file once!
                if fname_abs not in msetts.embedded_set:
                    elem_data_single_bytes(fbx_vid, b"Content", vid.packed_file.data)
                    msetts.embedded_set.add(fname_abs)
    
            else:
                filepath = bpy.path.abspath(vid.filepath)
    
                # We only ever embed a given file once!
                if filepath not in msetts.embedded_set:
                    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"")
                    msetts.embedded_set.add(filepath)
    
        # Looks like we'd rather not write any 'Content' element in this case (see T44442).
        # Sounds suspect, but let's try it!
        #~ else:
            #~ 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.head_radius * bone_radius_scale)
    
            if scene_data.settings.use_custom_props:
    
                fbx_data_element_custom_properties(props, bo)
    
            # Store Blender bone length - XXX Not much useful actually :/
            # (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance:
            # http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f )
            # elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True)
    
    
        # Skin deformers and BindPoses.
    
        # Note: we might also use Deformers for our "parent to vertex" stuff???
    
        deformer = scene_data.data_deformers_skin.get(arm_obj, None)
    
        if deformer is not None:
    
            for me, (skin_key, ob_obj, clusters) in deformer.items():
    
                mat_world_obj, mat_world_bones = fbx_data_bindpose_element(root, ob_obj, me, scene_data,
                                                                           arm_obj, mat_world_arm, bones)
    
                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: {} 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"")
    
                    if indices:
                        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",
    
                                                   matrix4_to_array(mat_world_bones[bo_obj].inverted_safe() @ mat_world_obj))
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix4_to_array(mat_world_bones[bo_obj]))
    
                    elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm))
    
    def fbx_data_leaf_bone_elements(root, scene_data):
        # Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain
        for (node_name, _par_uuid, node_uuid, attr_uuid, matrix, hide, size) in scene_data.data_leaf_bones:
            # Bone 'data'...
            fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid)
            fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute"))
            fbx_bo.add_string(b"LimbNode")
            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", size)
            elem_props_template_finalize(tmpl, props)
    
            # And bone object.
            model = elem_data_single_int64(root, b"Model", node_uuid)
            model.add_string(fbx_name_class(node_name.encode(), b"Model"))
            model.add_string(b"LimbNode")
    
            elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
    
            # Object transform info.
            loc, rot, scale = matrix.decompose()
            rot = rot.to_euler('XYZ')
            rot = tuple(convert_rad_to_deg_iter(rot))
    
            tmpl = elem_props_template_init(scene_data.templates, b"Model")
            # For now add only loc/rot/scale...
            props = elem_properties(model)
    
            # Generated leaf bones are obviously never animated!
    
            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 hide))
    
            # Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
            # invalid -1 value...
            elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
    
            elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1)  # RSrs
    
            # 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")
    
            elem_props_template_finalize(tmpl, props)
    
    
    
    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"
    
            if scene_data.settings.armature_nodetype == 'ROOT':
                obj_type = b"Root"
            elif scene_data.settings.armature_nodetype == 'LIMBNODE':
                obj_type = b"LimbNode"
            else:  # Default, preferred option...
                obj_type = b"Null"
    
        elif (ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE):
    
        elif (ob_obj.type == 'LIGHT'):
    
            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)
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        rot = tuple(convert_rad_to_deg_iter(rot))
    
        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,
                                animatable=True, animated=((ob_obj.key, "Lcl Translation") in scene_data.animated))
        elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot,
                                animatable=True, animated=((ob_obj.key, "Lcl Rotation") in scene_data.animated))
        elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale,
                                animatable=True, animated=((ob_obj.key, "Lcl Scaling") in scene_data.animated))
    
        elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not ob_obj.hide))
    
        # Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
        # invalid -1 value...
        elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
    
        elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1)  # RSrs
    
    
        # Custom properties.
    
        if scene_data.settings.use_custom_props:
    
            # Here we want customprops from the 'pose' bone, not the 'edit' bone...
            bdata = ob_obj.bdata_pose_bone if ob_obj.is_bone else ob_obj.bdata
            fbx_data_element_custom_properties(props, 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):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            return (int(v) for v in convert_sec_to_ktime_iter((f / fps for f, _v in keys)))
    
        # 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
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            start = int(convert_sec_to_ktime(f_start / fps))
            end = int(convert_sec_to_ktime(f_end / fps))
    
            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. #####
    
    # Mapping Blender -> FBX (principled_socket_name, fbx_name).
    PRINCIPLED_TEXTURE_SOCKETS_TO_FBX = (
        # ("diffuse", "diffuse", b"DiffuseFactor"),
        ("base_color_texture", b"DiffuseColor"),
    
        ("alpha_texture", b"TransparencyFactor"),  # Will be inverted in fact, not much we can do really...
    
        # ("base_color_texture", b"TransparentColor"),  # Uses diffuse color in Blender!
    
        ("emission_strength_texture", b"EmissiveFactor"),
    
        ("emission_color_texture", b"EmissiveColor"),
    
        # ("ambient", "ambient", b"AmbientFactor"),
        # ("", "", b"AmbientColor"),  # World stuff in Blender, for now ignore...
        ("normalmap_texture", b"NormalMap"),
        # Note: unsure about those... :/
        # ("", "", b"Bump"),
        # ("", "", b"BumpFactor"),
        # ("", "", b"DisplacementColor"),
        # ("", "", b"DisplacementFactor"),
        ("specular_texture", b"SpecularFactor"),
        # ("base_color", b"SpecularColor"),  # TODO: use tint?
        # See Material template about those two!
        ("roughness_texture", b"Shininess"),
        ("roughness_texture", b"ShininessExponent"),
        # ("mirror", "mirror", b"ReflectionColor"),
        ("metallic_texture", b"ReflectionFactor"),
    )
    
    def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
    
                                   data_bones, data_deformers_skin, data_empties, 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.
        """
    
        # We need some data for our armature 'object' too!!!
        data_empties[arm_obj] = get_blender_empty_key(arm_obj.bdata)
    
    
            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 = {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)
    
    
                continue
    
            # Always handled by an Armature modifier...
            found = False
    
            for mod in ob_obj.bdata.modifiers:
    
                if mod.type not in {'ARMATURE'} or not mod.object:
    
                    continue
                # We only support vertex groups binding method, not bone envelopes one!
    
                if mod.object in {arm_obj.bdata, arm_obj.bdata.proxy} 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_obj]
    
            clusters = {bo: get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata) for bo in bones}
            data_deformers_skin.setdefault(arm_obj, {})[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
    
    
            # We don't want a regular parent relationship for those in FBX...
    
            # Needed to handle matrices/spaces (since we do not parent them to 'armature' in FBX :/ ).
            ob_obj.parented_to_armature = True
    
    def fbx_generate_leaf_bones(settings, data_bones):
        # find which bons have no children
    
        child_count = {bo: 0 for bo in data_bones.keys()}
        for bo in data_bones.keys():
    
            if bo.parent and bo.parent.is_bone:
                child_count[bo.parent] += 1
    
        bone_radius_scale = settings.global_scale * 33.0
    
        # generate bone data
        leaf_parents = [bo for bo, count in child_count.items() if count == 0]
        leaf_bones = []
        for parent in leaf_parents:
            node_name = parent.name + "_end"
            parent_uuid = parent.fbx_uuid
    
            parent_key = parent.key
            node_uuid = get_fbx_uuid_from_key(parent_key + "_end_node")
            attr_uuid = get_fbx_uuid_from_key(parent_key + "_end_nodeattr")
    
    
            hide = parent.hide
            size = parent.bdata.head_radius * bone_radius_scale
            bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length
            matrix = Matrix.Translation((0, bone_length, 0))
            if settings.bone_correction_matrix_inv:
    
                matrix = settings.bone_correction_matrix_inv @ matrix
    
                matrix = matrix @ settings.bone_correction_matrix
    
            leaf_bones.append((node_name, parent_uuid, node_uuid, attr_uuid, matrix, hide, size))
    
        return leaf_bones
    
    
    
    def fbx_animations_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
    
        simplify_fac = scene_data.settings.bake_anim_simplify_factor
    
        scene = scene_data.scene
    
        depsgraph = scene_data.depsgraph
    
        force_keying = scene_data.settings.bake_anim_use_all_bones
    
        force_sek = scene_data.settings.bake_anim_force_startend_keying
    
            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}
    
                for dp_obj in ob_obj.dupli_list_gen(depsgraph):
    
                    if dp_obj in scene_data.objects:
                        objects.add(dp_obj)
    
    
        back_currframe = scene.frame_current
    
            ACNW = AnimationCurveNodeWrapper
            loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data)
    
            rot_deg = tuple(convert_rad_to_deg_iter(rot))
    
            force_key = (simplify_fac == 0.0) or (ob_obj.is_bone and force_keying)
            animdata_ob[ob_obj] = (ACNW(ob_obj.key, 'LCL_TRANSLATION', force_key, force_sek, loc),
                                   ACNW(ob_obj.key, 'LCL_ROTATION', force_key, force_sek, rot_deg),
                                   ACNW(ob_obj.key, 'LCL_SCALING', force_key, force_sek, scale))
    
        for me, (me_key, _shapes_key, shapes) in scene_data.data_deformers_shape.items():
            # Ignore absolute shape keys for now!
            if not me.shape_keys.use_relative:
                continue
            for shape, (channel_key, geom_key, _shape_verts_co, _shape_verts_idx) in shapes.items():
    
                acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,))
    
                # Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
                acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
                animdata_shapes[channel_key] = (acnode, me, shape)
    
        for cam_obj, cam_key in scene_data.data_cameras.items():
            cam = cam_obj.bdata.data
            acnode = AnimationCurveNodeWrapper(cam_key, 'CAMERA_FOCAL', force_key, force_sek, (cam.lens,))
            animdata_cameras[cam_key] = (acnode, cam)
    
    
            real_currframe = currframe - f_start if start_zero else currframe
    
            scene.frame_set(int(currframe), subframe=currframe - int(currframe))
    
            for dp_obj in ob_obj.dupli_list_gen(depsgraph):
                pass  # Merely updating dupli matrix of ObjectWrapper...
    
            for ob_obj, (anim_loc, anim_rot, anim_scale) in animdata_ob.items():
    
                # We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).