Skip to content
Snippets Groups Projects
export_fbx_bin.py 94.3 KiB
Newer Older
  • Learn to ignore specific revisions
  •     me.calc_normals_split()
        if 0:
            def _nortuples_gen(raw_nors):
                return zip(*(iter(raw_nors),) * 3)
    
    
            t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
    
            me.loops.foreach_get("normal", t_ln)
            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(_nortuples_gen(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"NormalIndex", (ln2idx[n] for n in _nortuples_gen(t_ln)))
    
            del ln2idx
            del t_ln
    
            del _nortuples_gen
        else:
    
            t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
    
            me.loops.foreach_get("normal", t_ln)
            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", 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
        tspacenumber = 0
        if scene_data.settings.use_tspace:
            tspacenumber = len(me.uv_layers)
            if tspacenumber:
    
                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)
    
                for idx, uvlayer in enumerate(me.uv_layers):
                    name = uvlayer.name
                    me.calc_tangents(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", t_ln)
                    # 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", t_ln)
                    # Tangent weights, no idea what it is.
    
                    elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw)
    
                me.free_tangents()
    
        me.free_normals_split()
    
        # Write VertexColor Layers
        # note, no programs seem to use this info :/
        vcolnumber = len(me.vertex_colors)
        if vcolnumber:
            def _coltuples_gen(raw_cols):
                def _infinite_gen(val):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    while 1:
                        yield val
    
                return zip(*(iter(raw_cols),) * 3 + (_infinite_gen(1.0),))  # We need a fake alpha...
    
    
            t_lc = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
    
            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:
            def _uvtuples_gen(raw_uvs):
                return zip(*(iter(raw_uvs),) * 2)
    
    
            t_luv = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 2
    
            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")
    
                uv2idx = tuple(set(_uvtuples_gen(t_luv)))
                elem_data_single_float64_array(lay_uv, b"UV", chain(*uv2idx))  # Flatten again...
    
                uv2idx = {uv: idx for idx, uv in enumerate(uv2idx)}
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                elem_data_single_int32_array(lay_uv, b"UVIndex", (uv2idx[uv] for uv in _uvtuples_gen(t_luv)))
    
                del uv2idx
            del t_luv
            del _uvtuples_gen
    
        # Face's materials.
        me_fbxmats_idx = None
        if me in scene_data.mesh_mat_indices:
            me_fbxmats_idx = scene_data.mesh_mat_indices[me]
            me_blmats = me.materials
            if me_fbxmats_idx and me_blmats:
                lay_mat = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
                elem_data_single_int32(lay_mat, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
                elem_data_single_string(lay_mat, b"Name", b"")
                nbr_mats = len(me_fbxmats_idx)
                if nbr_mats > 1:
    
                    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.
                    blmats_to_fbxmats_idxs = [me_fbxmats_idx[m] for m in me_blmats]
                    mat_idx_limit = len(blmats_to_fbxmats_idxs)
                    def_mat = blmats_to_fbxmats_idxs[0]
    
                    _gen = (blmats_to_fbxmats_idxs[m] if m < mat_idx_limit else def_mat for m in t_pm)
    
                    t_pm = array.array(data_types.ARRAY_INT32, _gen)
    
                    elem_data_single_string(lay_mat, 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_mat, b"ReferenceInformationType", b"IndexToDirect")
    
                    elem_data_single_int32_array(lay_mat, b"Materials", t_pm)
                    del t_pm
                else:
                    elem_data_single_string(lay_mat, b"MappingInformationType", b"AllSame")
                    elem_data_single_string(lay_mat, b"ReferenceInformationType", b"IndexToDirect")
                    elem_data_single_int32_array(lay_mat, 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)
        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 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 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)
    
    
    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_fbxuid_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 = 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_rgb", 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_rgb", b"AmbientColor", ambient_color)
        elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", mat.ambient)
        elem_props_template_set(tmpl, props, "p_color_rgb", 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_rgb", b"TransparentColor", mat.diffuse_color)
    
        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"BumpFactor": (1.0, "p_number"),
        b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
        b"DisplacementFactor": (0.0, "p_number"),
        """
        if mat_type == b"phong":
            elem_props_template_set(tmpl, props, "p_color_rgb", 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_rgb", 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(tmpl, 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_fbxuid_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 = 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(tmpl, 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_fbxuid_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???
        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:
            try:
                with open(vid.filepath, 'br') as f:
                    elem_data_single_byte_array(fbx_vid, b"Content", f.read())
            except Exception as e:
                print("WARNING: embeding file {} failed ({})".format(vid.filepath, e))
                elem_data_single_byte_array(fbx_vid, b"Content", b"")
        else:
            elem_data_single_byte_array(fbx_vid, b"Content", b"")
    
    
    def fbx_data_armature_elements(root, armature, 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...
        """
    
        # Bones "data".
        tmpl = scene_data.templates[b"Bone"]
        for bo in armature.data.bones:
            _bo_key, bo_data_key, _arm = scene_data.data_bones[bo]
            fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_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")
    
    
            props = elem_properties(fbx_bo)
            elem_props_template_set(tmpl, props, "p_number", b"Size", (bo.tail_local - bo.head_local).length)
    
            # Custom properties.
            if scene_data.settings.use_custom_properties:
                fbx_data_element_custom_properties(tmpl, props, bo)
    
        # Deformers and BindPoses.
        # Note: we might also use Deformers for our "parent to vertex" stuff???
        deformer = scene_data.data_deformers.get(armature, None)
        if deformer is not None:
            for me, (skin_key, 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(armature, me)
                fbx_pose = elem_data_single_int64(root, b"Pose", get_fbxuid_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(armature.data.bones))
    
                # First node is mesh/object.
                mat_world_obj = object_matrix(scene_data, obj, global_space=True)
                fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
                elem_data_single_int64(fbx_posenode, b"Node", get_fbxuid_from_key(scene_data.objects[obj]))
                elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(mat_world_obj))
                # And all bones of armature!
                mat_world_bones = {}
                for bo in armature.data.bones:
                    bomat = object_matrix(scene_data, bo, armature, global_space=True)
                    mat_world_bones[bo] = bomat
                    fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
                    elem_data_single_int64(fbx_posenode, b"Node", get_fbxuid_from_key(scene_data.objects[bo]))
                    elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(bomat))
    
                # Deformer.
                fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbxuid_from_key(skin_key))
                fbx_skin.add_string(fbx_name_class(armature.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...
    
                for bo, clstr_key in clusters.items():
                    # Find which vertices are affected by this bone/vgroup pair, and matching weights.
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    indices = []
                    weights = []
    
                    vg_idx = obj.vertex_groups[bo.name].index
                    for idx, v in enumerate(me.vertices):
                        vert_vg = [vg for vg in v.groups if vg.group == vg_idx]
                        if not vert_vg:
                            continue
                        indices.append(idx)
                        weights.append(vert_vg[0].weight)
    
                    # Create the cluster.
                    fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbxuid_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 and TransformLink matrices...
                    # They seem to be mostly the same as BindPose ones???
                    # 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].inverted() * mat_world_obj))
                    elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix_to_array(mat_world_bones[bo]))
    
    
    def fbx_data_object_elements(root, obj, scene_data):
        """
        Write the Object (Model) data blocks.
        Note we handle "Model" part of bones as well here!
        """
        obj_type = b"Null"  # default, sort of empty...
    
        if isinstance(obj, Bone):
    
            obj_type = b"LimbNode"
        elif (obj.type == 'MESH'):
            obj_type = b"Mesh"
        elif (obj.type == 'LAMP'):
            obj_type = b"Light"
        elif (obj.type == 'CAMERA'):
            obj_type = b"Camera"
        obj_key = scene_data.objects[obj]
        model = elem_data_single_int64(root, b"Model", get_fbxuid_from_key(obj_key))
        model.add_string(fbx_name_class(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 = object_tx(scene_data, obj)
    
        rot = tuple(units_convert_iter(rot, "radian", "degree"))
    
    
        tmpl = 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)
    
        # TODO: "constraints" (limit loc/rot/scale, and target-to-object).
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
            fbx_data_element_custom_properties(tmpl, props, obj)
    
        # 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")
    
    
        if isinstance(obj, Object) and obj.type == 'CAMERA':
    
            # 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_number", b"AspectW", width)
            elem_props_template_set(tmpl, props, "p_number", 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)
    
    
    ##### Top-level FBX data container. #####
    
    # Helper container gathering some data we need multiple times:
    #     * templates.
    #     * objects.
    #     * connections.
    #     * takes.
    FBXData = namedtuple("FBXData", (
        "templates", "templates_users", "connections",
        "settings", "scene", "objects",
        "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices",
        "data_bones", "data_deformers",
        "data_world", "data_materials", "data_textures", "data_videos",
    ))
    
    
    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.
        """
        # Tex influence does not exists in FBX, so assume influence < 0.5 = no influence... :/
        INFLUENCE_THRESHOLD = 0.5
    
        # 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:
            if getattr(tex, "use_map_" + use_map_name) and getattr(tex, name_factor + "_factor") >= INFLUENCE_THRESHOLD:
                tex_fbx_props.add(fbx_prop_name)
    
        return tex_fbx_props
    
    
    def fbx_skeleton_from_armature(scene, settings, armature, objects, 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.
        """
        arm = armature.data
        bones = {}
        for bo in arm.bones:
            key, data_key = get_blender_bone_key(armature, bo)
            objects[bo] = key
            data_bones[bo] = (key, data_key, armature)
            bones[bo.name] = bo
    
        for obj in objects.keys():
    
            if not isinstance(obj, Object):
    
                continue
            if obj.type not in {'MESH'}:
                continue
            if obj.parent != armature:
                continue
    
            # Always handled by an Armature modifier...
            found = False
            for mod in obj.modifiers:
                if mod.type not in {'ARMATURE'}:
                    continue
                # We only support vertex groups binding method, not bone envelopes one!
                if mod.object == armature and mod.use_vertex_groups:
                    found = True
                    break
    
            if not found:
                continue
    
            # Now we have a mesh using this armature. First, find out which bones are concerned!
            # XXX Assuming here non-used bones can have no cluster, this has to be checked!
            used_bones = tuple(bones[vg.name] for vg in obj.vertex_groups if vg.name in bones)
            if not used_bones:
                continue
    
            # 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!).
            me = obj.data
            clusters = {bo: get_blender_bone_cluster_key(armature, me, bo) for bo in used_bones}
            data_deformers.setdefault(armature, {})[me] = (get_blender_armature_skin_key(armature, me), obj, clusters)
    
            # We don't want a regular parent relationship for those in FBX...
            arm_parents.add((armature, obj))
    
    
    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 = {obj: get_blenderID_key(obj) for obj in scene.objects if obj.type in objtypes}
        data_lamps = {obj.data: get_blenderID_key(obj.data) for obj in objects if obj.type == 'LAMP'}
        # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
        data_cameras = {obj: get_blenderID_key(obj.data) for obj in objects if obj.type == 'CAMERA'}
        data_meshes = {obj.data: get_blenderID_key(obj.data) for obj in objects if obj.type == 'MESH'}
    
        # Armatures!
        data_bones = {}
        data_deformers = {}
        arm_parents = set()
        for obj in tuple(objects.keys()):
            if obj.type not in {'ARMATURE'}:
                continue
            fbx_skeleton_from_armature(scene, settings, obj, objects, data_bones, data_deformers, arm_parents)
    
        # Some world settings are embedded in FBX materials...
        if scene.world:
            data_world = {scene.world: get_blenderID_key(scene.world)}
        else:
            data_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...
        data_materials = {}
        for obj in objects:
            # Only meshes for now!
    
            if not isinstance(obj, Object) or obj.type not in {'MESH'}:
    
                continue
            for mat_s in obj.material_slots:
                mat = mat_s.material
                # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
                # However, I doubt anything else than Lambert/Phong is really portable!
                # TODO: Support nodes (*BIG* todo!).
                if mat.type in {'SURFACE'} and mat.diffuse_shader in {'LAMBERT'} and not mat.use_nodes:
                    if mat in data_materials:
                        data_materials[mat][1].append(obj)
                    else:
                        data_materials[mat] = (get_blenderID_key(mat), [obj])
    
        # Note FBX textures also hold their mapping info.
        # TODO: Support layers?
        data_textures = {}
        # FbxVideo also used to store static images...
        data_videos = {}
        # For now, do not use world textures, don't think they can be linked to anything FBX wise...
        for mat in data_materials.keys():
            for tex in mat.texture_slots:
                if tex is None:
                    continue
                # For now, only consider image textures.
                # Note FBX does has support for procedural, but this is not portable at all (opaque blob),
                # so not useful for us.
                # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
                #if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
                if tex.texture.type not in {'IMAGE'}:
                    continue
                img = tex.texture.image
                if img is None:
                    continue
                # Find out whether we can actually use this texture for this material, in FBX context.
                tex_fbx_props = fbx_mat_properties_from_texture(tex)
                if not tex_fbx_props:
                    continue
                if tex in data_textures:
                    data_textures[tex][1][mat] = tex_fbx_props
                else:
                    data_textures[tex] = (get_blenderID_key(tex), {mat: tex_fbx_props})
                if img in data_videos:
                    data_videos[img][1].append(tex)
                else:
                    data_videos[img] = (get_blenderID_key(img), [tex])
    
        ##### Creation of templates...
    
        templates = OrderedDict()
        templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
    
        if data_lamps:
            templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps))
    
        if data_cameras:
            templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
    
        if data_bones:
            templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
    
        if data_meshes:
            templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(data_meshes))
    
        if objects:
            templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
    
        if arm_parents:
            # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
            templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
    
        if data_deformers:
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            nbr = len(data_deformers)
            nbr += sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values())
    
            templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
    
        # No world support in FBX...
        """
        if data_world:
            templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
        """
    
        if data_materials:
            templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
    
        if data_textures:
            templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
    
        if data_videos:
            templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
    
        templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
    
        ##### Creation of connections...
    
        connections = []
    
        # Objects (with classical parenting).
        for obj, obj_key in objects.items():
            # Bones are handled later.
    
            if isinstance(obj, Object):
    
                par = obj.parent
                par_key = 0  # Convention, "root" node (never explicitly written).
                if par and par in objects:
                    par_type = obj.parent_type
                    if par_type in {'OBJECT', 'BONE'}:
                        # Meshes parented to armature also have 'OBJECT' par_type, in FBX this is handled separately,
                        # we do not want an extra object parenting!
                        if (par, obj) not in arm_parents:
                            par_key = objects[par]
                    else:
                        print("Sorry, “{}” parenting type is not supported".format(par_type))
                connections.append((b"OO", get_fbxuid_from_key(obj_key), get_fbxuid_from_key(par_key), None))
    
        # Armature & Bone chains.
        for bo, (bo_key, _bo_data_key, arm) in data_bones.items():
            par = bo.parent
            if not par:  # Root bone.
                par = arm
            if par not in objects:
                continue
            connections.append((b"OO", get_fbxuid_from_key(bo_key), get_fbxuid_from_key(objects[par]), None))
    
        # Cameras
        for obj_cam, cam_key in data_cameras.items():
            cam_obj_key = objects[obj_cam]
            connections.append((b"OO", get_fbxuid_from_key(cam_key), get_fbxuid_from_key(cam_obj_key), None))
    
        # Object data.
        for obj, obj_key in objects.items():
    
            if isinstance(obj, Bone):
    
                _bo_key, bo_data_key, _arm = data_bones[obj]
                assert(_bo_key == obj_key)
                connections.append((b"OO", get_fbxuid_from_key(bo_data_key), get_fbxuid_from_key(obj_key), None))
            elif obj.type == 'LAMP':
                lamp_key = data_lamps[obj.data]
                connections.append((b"OO", get_fbxuid_from_key(lamp_key), get_fbxuid_from_key(obj_key), None))
            elif obj.type == 'MESH':
                mesh_key = data_meshes[obj.data]
                connections.append((b"OO", get_fbxuid_from_key(mesh_key), get_fbxuid_from_key(obj_key), None))
    
        # Deformers (armature-to-geometry, only for meshes currently)...
        for arm, deformed_meshes in data_deformers.items():
            for me, (skin_key, _obj, clusters) in deformed_meshes.items():
                # skin -> geometry
                connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(data_meshes[me]), None))
                for bo, clstr_key in clusters.items():
                    # cluster -> skin
                    connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None))
                    # bone -> cluster
                    connections.append((b"OO", get_fbxuid_from_key(objects[bo]), get_fbxuid_from_key(clstr_key), None))
    
        # Materials
        mesh_mat_indices = {}
        _objs_indices = {}
        for mat, (mat_key, objs) in data_materials.items():
            for obj in objs:
                obj_key = objects[obj]
                connections.append((b"OO", get_fbxuid_from_key(mat_key), get_fbxuid_from_key(obj_key), None))
                # Get index of this mat for this object.
                # Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
                # Only mats for meshes currently...
                me = obj.data
                idx = _objs_indices[obj] = _objs_indices.get(obj, -1) + 1
                mesh_mat_indices.setdefault(me, {})[mat] = idx
        del _objs_indices
    
        # Textures
        for tex, (tex_key, mats) in data_textures.items():
            for mat, fbx_mat_props in mats.items():
                mat_key, _objs = data_materials[mat]
                for fbx_prop in fbx_mat_props:
                    # texture -> material properties
                    connections.append((b"OP", get_fbxuid_from_key(tex_key), get_fbxuid_from_key(mat_key), fbx_prop))
    
        # Images
        for vid, (vid_key, texs) in data_videos.items():
            for tex in texs:
                tex_key, _texs = data_textures[tex]
                connections.append((b"OO", get_fbxuid_from_key(vid_key), get_fbxuid_from_key(tex_key), None))
    
        ##### And pack all this!
    
        return FBXData(
            templates, templates_users, connections,
            settings, scene, objects,
            data_lamps, data_cameras, data_meshes, mesh_mat_indices,
            data_bones, data_deformers,
            data_world, data_materials, data_textures, data_videos,
        )
    
    
    ##### Top-level FBX elements generators. #####
    
    def fbx_header_elements(root, scene_data, time=None):
        """
        Write boiling code of FBX root.
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        ##### Start of FBXHeaderExtension element.
        header_ext = elem_empty(root, b"FBXHeaderExtension")
    
        elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
    
        elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
    
        # No encryption!
        elem_data_single_int32(header_ext, b"EncryptionType", 0)
    
        if time is None:
            time = datetime.datetime.now()
        elem = elem_empty(header_ext, b"CreationTimeStamp")
        elem_data_single_int32(elem, b"Version", 1000)
        elem_data_single_int32(elem, b"Year", time.year)
        elem_data_single_int32(elem, b"Month", time.month)
        elem_data_single_int32(elem, b"Day", time.day)
        elem_data_single_int32(elem, b"Hour", time.hour)
        elem_data_single_int32(elem, b"Minute", time.minute)
        elem_data_single_int32(elem, b"Second", time.second)
        elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
    
        elem_data_single_string_unicode(header_ext, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        # 'SceneInfo' seems mandatory to get a valid FBX file...
        # TODO use real values!
        # XXX Should we use scene.name.encode() here?
        scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
        scene_info.add_string(b"UserData")
        elem_data_single_string(scene_info, b"Type", b"UserData")
        elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
        meta_data = elem_empty(scene_info, b"MetaData")
        elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
        elem_data_single_string(meta_data, b"Title", b"")
        elem_data_single_string(meta_data, b"Subject", b"")
        elem_data_single_string(meta_data, b"Author", b"")
        elem_data_single_string(meta_data, b"Keywords", b"")
        elem_data_single_string(meta_data, b"Revision", b"")
        elem_data_single_string(meta_data, b"Comment", b"")
    
        props = elem_properties(scene_info)
        elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
        elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
        original = elem_props_compound(props, b"Original")
        original("p_string", b"ApplicationVendor", "Blender Foundation")
        original("p_string", b"ApplicationName", "Blender")
        original("p_string", b"ApplicationVersion", "2.70")
        original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
        original("p_string", b"FileName", "/foobar.fbx")
        lastsaved = elem_props_compound(props, b"LastSaved")
        lastsaved("p_string", b"ApplicationVendor", "Blender Foundation")
        lastsaved("p_string", b"ApplicationName", "Blender")
        lastsaved("p_string", b"ApplicationVersion", "2.70")
        lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
    
        ##### End of FBXHeaderExtension element.
    
        # FileID is replaced by dummy value currently...
        elem_data_single_bytes(root, b"FileId", b"FooBar")
    
        # CreationTime is replaced by dummy value currently, but anyway...
        elem_data_single_string_unicode(root, b"CreationTime",
                                        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
                                        "".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
                                                  time.microsecond * 1000))
    
        elem_data_single_string_unicode(root, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        ##### Start of GlobalSettings element.
        global_settings = elem_empty(root, b"GlobalSettings")
    
        elem_data_single_int32(global_settings, b"Version", 1000)
    
        props = elem_properties(global_settings)
        up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
        elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
        elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
        elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
        elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
        elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
        elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
        elem_props_set(props, "p_number", b"UnitScaleFactor", 1.0)
        elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
        elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
        # XXX Those time stuff is taken from a file, have no (complete) idea what it means!
        elem_props_set(props, "p_enum", b"TimeMode", 11)
        elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
        elem_props_set(props, "p_timestamp", b"TimeSpanStop", 46186158000)  # XXX One second!
    
        ##### End of GlobalSettings element.
    
    
    def fbx_documents_elements(root, scene_data):
        """
        Write 'Document' part of FBX root.
        Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        name = scene_data.scene.name
    
        ##### Start of Documents element.
        docs = elem_empty(root, b"Documents")
    
        elem_data_single_int32(docs, b"Count", 1)
    
        doc_uid = get_fbxuid_from_key("__FBX_Document__" + name)
        doc = elem_data_single_int64(docs, b"Document", doc_uid)
        doc.add_string(b"")
        doc.add_string_unicode(name)
    
        props = elem_properties(doc)
        elem_props_set(props, "p_object", b"SourceObject")
        elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
    
        # XXX Some kind of ID? Offset?
        #     Anyway, as long as we have only one doc, probably not an issue.
        elem_data_single_int64(doc, b"RootNode", 0)
    
    
    def fbx_references_elements(root, scene_data):
        """
        Have no idea what references are in FBX currently... Just writing empty element.