Skip to content
Snippets Groups Projects
Select Git revision
  • ffaddb3a6f59dc9a2177c27391c8fd8444af3257
  • master default protected
  • blender-v3.6-release
  • main
  • blender-v4.1-release
  • blender-v4.0-release
  • blender-v3.3-release
  • asset-shelf
  • blender-v3.5-release
  • brush-assets-project
  • blender-v2.93-release
  • blender-v3.4-release
  • xr-dev
  • bholodeck-v3.3
  • blender-v3.2-release
  • temp-xr-tracker
  • blender-v3.1-release
  • screenshots-manual
  • gltf_vtree
  • blender-v2.83-release
  • blender-v3.0-release
  • v3.6.18
  • v3.6.19
  • v3.6.20
  • v3.6.21
  • v3.6.22
  • v3.6.23
  • v4.1.1
  • v4.1.0
  • v3.6.10
  • v3.6.11
  • v3.6.12
  • v3.6.13
  • v3.6.14
  • v3.6.15
  • v3.6.16
  • v3.6.17
  • v3.6.9
  • v3.3.16
  • v3.6.8
  • v3.3.15
41 results

export_fbx_bin.py

Blame
  • user avatar
    Bastien Montagne authored
    So, it appears some importers (at least UE4) do not use UnitScaleFactor defined by FBX,
    which is assumed to be a way to say 'this FBX file uses units n times default FBX unit'
    (default FBX unit being centimeter - afaik, at least I saw some FBX from Max with a
    UnitScaleFactor of 2.54 - inches).
    
    Hence, we have to add yet another stupid option to apply that 'unit scaling' to objects
    instead (as part of global scaling)... Hurra.
    ffaddb3a
    History
    export_fbx_bin.py 140.31 KiB
    # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    # <pep8 compliant>
    
    # Script copyright (C) Campbell Barton, Bastien Montagne
    
    
    import array
    import datetime
    import math
    import os
    import time
    
    from collections import OrderedDict
    from itertools import zip_longest, chain
    
    if "bpy" in locals():
        import importlib
        if "encode_bin" in locals():
            importlib.reload(encode_bin)
        if "data_types" in locals():
            importlib.reload(data_types)
        if "fbx_utils" in locals():
            importlib.reload(fbx_utils)
    
    import bpy
    import bpy_extras
    from mathutils import Vector, Matrix
    
    from . import encode_bin, data_types, fbx_utils
    from .fbx_utils import (
        # Constants.
        FBX_VERSION, FBX_HEADER_VERSION, FBX_SCENEINFO_VERSION, FBX_TEMPLATES_VERSION,
        FBX_MODELS_VERSION,
        FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION,
        FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION,
        FBX_GEOMETRY_MATERIAL_VERSION, FBX_GEOMETRY_LAYER_VERSION,
        FBX_GEOMETRY_SHAPE_VERSION, FBX_DEFORMER_SHAPE_VERSION, FBX_DEFORMER_SHAPECHANNEL_VERSION,
        FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION,
        FBX_MATERIAL_VERSION, FBX_TEXTURE_VERSION,
        FBX_ANIM_KEY_VERSION,
        FBX_ANIM_PROPSGROUP_NAME,
        FBX_KTIME,
        BLENDER_OTHER_OBJECT_TYPES, BLENDER_OBJECT_TYPES_MESHLIKE,
        FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES,
        RIGHT_HAND_AXES, FBX_FRAMERATES,
        # Miscellaneous utils.
        PerfMon,
        units_blender_to_fbx_factor, units_convertor, units_convertor_iter,
        matrix4_to_array, similar_values, similar_values_iter,
        # Mesh transform helpers.
        vcos_transformed_gen, nors_transformed_gen,
        # UUID from key.
        get_fbx_uuid_from_key,
        # Key generators.
        get_blenderID_key, get_blenderID_name,
        get_blender_mesh_shape_key, get_blender_mesh_shape_channel_key,
        get_blender_empty_key, get_blender_bone_key,
        get_blender_bindpose_key, get_blender_armature_skin_key, get_blender_bone_cluster_key,
        get_blender_anim_id_base, get_blender_anim_stack_key, get_blender_anim_layer_key,
        get_blender_anim_curve_node_key, get_blender_anim_curve_key,
        # FBX element data.
        elem_empty,
        elem_data_single_bool, elem_data_single_int16, elem_data_single_int32, elem_data_single_int64,
        elem_data_single_float32, elem_data_single_float64,
        elem_data_single_bytes, elem_data_single_string, elem_data_single_string_unicode,
        elem_data_single_bool_array, elem_data_single_int32_array, elem_data_single_int64_array,
        elem_data_single_float32_array, elem_data_single_float64_array, elem_data_vec_float64,
        # FBX element properties.
        elem_properties, elem_props_set, elem_props_compound,
        # FBX element properties handling templates.
        elem_props_template_init, elem_props_template_set, elem_props_template_finalize,
        # Templates.
        FBXTemplate, fbx_templates_generate,
        # Animation.
        AnimationCurveNodeWrapper,
        # Objects.
        ObjectWrapper, fbx_name_class,
        # Top level.
        FBXExportSettingsMedia, FBXExportSettings, FBXExportData,
    )
    
    # Units convertors!
    convert_sec_to_ktime = units_convertor("second", "ktime")
    convert_sec_to_ktime_iter = units_convertor_iter("second", "ktime")
    
    convert_mm_to_inch = units_convertor("millimeter", "inch")
    
    convert_rad_to_deg = units_convertor("radian", "degree")
    convert_rad_to_deg_iter = units_convertor_iter("radian", "degree")
    
    
    # ##### Templates #####
    # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
    
    def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"GlobalSettings", b"", props, nbr_users, [False])
    
    
    def fbx_template_def_model(scene, settings, override_defaults=None, nbr_users=0):
        gscale = settings.global_scale
        props = OrderedDict((
            # Name,                   Value, Type, Animatable
            (b"QuaternionInterpolate", (0, "p_enum", False)),  # 0 = no quat interpolation.
            (b"RotationOffset", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"RotationPivot", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"ScalingOffset", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"ScalingPivot", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"TranslationActive", (False, "p_bool", False)),
            (b"TranslationMin", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"TranslationMax", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"TranslationMinX", (False, "p_bool", False)),
            (b"TranslationMinY", (False, "p_bool", False)),
            (b"TranslationMinZ", (False, "p_bool", False)),
            (b"TranslationMaxX", (False, "p_bool", False)),
            (b"TranslationMaxY", (False, "p_bool", False)),
            (b"TranslationMaxZ", (False, "p_bool", False)),
            (b"RotationOrder", (0, "p_enum", False)),  # we always use 'XYZ' order.
            (b"RotationSpaceForLimitOnly", (False, "p_bool", False)),
            (b"RotationStiffnessX", (0.0, "p_double", False)),
            (b"RotationStiffnessY", (0.0, "p_double", False)),
            (b"RotationStiffnessZ", (0.0, "p_double", False)),
            (b"AxisLen", (10.0, "p_double", False)),
            (b"PreRotation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"PostRotation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"RotationActive", (False, "p_bool", False)),
            (b"RotationMin", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"RotationMax", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"RotationMinX", (False, "p_bool", False)),
            (b"RotationMinY", (False, "p_bool", False)),
            (b"RotationMinZ", (False, "p_bool", False)),
            (b"RotationMaxX", (False, "p_bool", False)),
            (b"RotationMaxY", (False, "p_bool", False)),
            (b"RotationMaxZ", (False, "p_bool", False)),
            (b"InheritType", (0, "p_enum", False)),  # RrSs
            (b"ScalingActive", (False, "p_bool", False)),
            (b"ScalingMin", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"ScalingMax", ((1.0, 1.0, 1.0), "p_vector_3d", False)),
            (b"ScalingMinX", (False, "p_bool", False)),
            (b"ScalingMinY", (False, "p_bool", False)),
            (b"ScalingMinZ", (False, "p_bool", False)),
            (b"ScalingMaxX", (False, "p_bool", False)),
            (b"ScalingMaxY", (False, "p_bool", False)),
            (b"ScalingMaxZ", (False, "p_bool", False)),
            (b"GeometricTranslation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"GeometricRotation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"GeometricScaling", ((1.0, 1.0, 1.0), "p_vector_3d", False)),
            (b"MinDampRangeX", (0.0, "p_double", False)),
            (b"MinDampRangeY", (0.0, "p_double", False)),
            (b"MinDampRangeZ", (0.0, "p_double", False)),
            (b"MaxDampRangeX", (0.0, "p_double", False)),
            (b"MaxDampRangeY", (0.0, "p_double", False)),
            (b"MaxDampRangeZ", (0.0, "p_double", False)),
            (b"MinDampStrengthX", (0.0, "p_double", False)),
            (b"MinDampStrengthY", (0.0, "p_double", False)),
            (b"MinDampStrengthZ", (0.0, "p_double", False)),
            (b"MaxDampStrengthX", (0.0, "p_double", False)),
            (b"MaxDampStrengthY", (0.0, "p_double", False)),
            (b"MaxDampStrengthZ", (0.0, "p_double", False)),
            (b"PreferedAngleX", (0.0, "p_double", False)),
            (b"PreferedAngleY", (0.0, "p_double", False)),
            (b"PreferedAngleZ", (0.0, "p_double", False)),
            (b"LookAtProperty", (None, "p_object", False)),
            (b"UpVectorProperty", (None, "p_object", False)),
            (b"Show", (True, "p_bool", False)),
            (b"NegativePercentShapeSupport", (True, "p_bool", False)),
            (b"DefaultAttributeIndex", (-1, "p_integer", False)),
            (b"Freeze", (False, "p_bool", False)),
            (b"LODBox", (False, "p_bool", False)),
            (b"Lcl Translation", ((0.0, 0.0, 0.0), "p_lcl_translation", True)),
            (b"Lcl Rotation", ((0.0, 0.0, 0.0), "p_lcl_rotation", True)),
            (b"Lcl Scaling", ((1.0, 1.0, 1.0), "p_lcl_scaling", True)),
            (b"Visibility", (1.0, "p_visibility", True)),
            (b"Visibility Inheritance", (1, "p_visibility_inheritance", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Model", b"FbxNode", props, nbr_users, [False])
    
    
    def fbx_template_def_null(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (b"Color", ((0.8, 0.8, 0.8), "p_color_rgb", False)),
            (b"Size", (100.0, "p_double", False)),
            (b"Look", (1, "p_enum", False)),  # Cross (0 is None, i.e. invisible?).
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"NodeAttribute", b"FbxNull", props, nbr_users, [False])
    
    
    def fbx_template_def_light(scene, settings, override_defaults=None, nbr_users=0):
        gscale = settings.global_scale
        props = OrderedDict((
            (b"LightType", (0, "p_enum", False)),  # Point light.
            (b"CastLight", (True, "p_bool", False)),
            (b"Color", ((1.0, 1.0, 1.0), "p_color", True)),
            (b"Intensity", (100.0, "p_number", True)),  # Times 100 compared to Blender values...
            (b"DecayType", (2, "p_enum", False)),  # Quadratic.
            (b"DecayStart", (30.0 * gscale, "p_double", False)),
            (b"CastShadows", (True, "p_bool", False)),
            (b"ShadowColor", ((0.0, 0.0, 0.0), "p_color", True)),
            (b"AreaLightShape", (0, "p_enum", False)),  # Rectangle.
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"NodeAttribute", b"FbxLight", props, nbr_users, [False])
    
    
    def fbx_template_def_camera(scene, settings, override_defaults=None, nbr_users=0):
        r = scene.render
        props = OrderedDict((
            (b"Color", ((0.8, 0.8, 0.8), "p_color_rgb", False)),
            (b"Position", ((0.0, 0.0, -50.0), "p_vector", True)),
            (b"UpVector", ((0.0, 1.0, 0.0), "p_vector", True)),
            (b"InterestPosition", ((0.0, 0.0, 0.0), "p_vector", True)),
            (b"Roll", (0.0, "p_roll", True)),
            (b"OpticalCenterX", (0.0, "p_opticalcenterx", True)),
            (b"OpticalCenterY", (0.0, "p_opticalcentery", True)),
            (b"BackgroundColor", ((0.63, 0.63, 0.63), "p_color", True)),
            (b"TurnTable", (0.0, "p_number", True)),
            (b"DisplayTurnTableIcon", (False, "p_bool", False)),
            (b"UseMotionBlur", (False, "p_bool", False)),
            (b"UseRealTimeMotionBlur", (True, "p_bool", False)),
            (b"Motion Blur Intensity", (1.0, "p_number", True)),
            (b"AspectRatioMode", (0, "p_enum", False)),  # WindowSize.
            (b"AspectWidth", (320.0, "p_double", False)),
            (b"AspectHeight", (200.0, "p_double", False)),
            (b"PixelAspectRatio", (1.0, "p_double", False)),
            (b"FilmOffsetX", (0.0, "p_number", True)),
            (b"FilmOffsetY", (0.0, "p_number", True)),
            (b"FilmWidth", (0.816, "p_double", False)),
            (b"FilmHeight", (0.612, "p_double", False)),
            (b"FilmAspectRatio", (1.3333333333333333, "p_double", False)),
            (b"FilmSqueezeRatio", (1.0, "p_double", False)),
            (b"FilmFormatIndex", (0, "p_enum", False)),  # Assuming this is ApertureFormat, 0 = custom.
            (b"PreScale", (1.0, "p_number", True)),
            (b"FilmTranslateX", (0.0, "p_number", True)),
            (b"FilmTranslateY", (0.0, "p_number", True)),
            (b"FilmRollPivotX", (0.0, "p_number", True)),
            (b"FilmRollPivotY", (0.0, "p_number", True)),
            (b"FilmRollValue", (0.0, "p_number", True)),
            (b"FilmRollOrder", (0, "p_enum", False)),  # 0 = rotate first (default).
            (b"ApertureMode", (2, "p_enum", False)),  # 2 = Vertical.
            (b"GateFit", (0, "p_enum", False)),  # 0 = no resolution gate fit.
            (b"FieldOfView", (25.114999771118164, "p_fov", True)),
            (b"FieldOfViewX", (40.0, "p_fov_x", True)),
            (b"FieldOfViewY", (40.0, "p_fov_y", True)),
            (b"FocalLength", (34.89327621672628, "p_number", True)),
            (b"CameraFormat", (0, "p_enum", False)),  # Custom camera format.
            (b"UseFrameColor", (False, "p_bool", False)),
            (b"FrameColor", ((0.3, 0.3, 0.3), "p_color_rgb", False)),
            (b"ShowName", (True, "p_bool", False)),
            (b"ShowInfoOnMoving", (True, "p_bool", False)),
            (b"ShowGrid", (True, "p_bool", False)),
            (b"ShowOpticalCenter", (False, "p_bool", False)),
            (b"ShowAzimut", (True, "p_bool", False)),
            (b"ShowTimeCode", (False, "p_bool", False)),
            (b"ShowAudio", (False, "p_bool", False)),
            (b"AudioColor", ((0.0, 1.0, 0.0), "p_vector_3d", False)),  # Yep, vector3d, not corlorgb… :cry:
            (b"NearPlane", (10.0, "p_double", False)),
            (b"FarPlane", (4000.0, "p_double", False)),
            (b"AutoComputeClipPanes", (False, "p_bool", False)),
            (b"ViewCameraToLookAt", (True, "p_bool", False)),
            (b"ViewFrustumNearFarPlane", (False, "p_bool", False)),
            (b"ViewFrustumBackPlaneMode", (2, "p_enum", False)),  # 2 = show back plane if texture added.
            (b"BackPlaneDistance", (4000.0, "p_number", True)),
            (b"BackPlaneDistanceMode", (1, "p_enum", False)),  # 1 = relative to camera.
            (b"ViewFrustumFrontPlaneMode", (2, "p_enum", False)),  # 2 = show front plane if texture added.
            (b"FrontPlaneDistance", (10.0, "p_number", True)),
            (b"FrontPlaneDistanceMode", (1, "p_enum", False)),  # 1 = relative to camera.
            (b"LockMode", (False, "p_bool", False)),
            (b"LockInterestNavigation", (False, "p_bool", False)),
            # BackPlate... properties **arggggg!**
            (b"FitImage", (False, "p_bool", False)),
            (b"Crop", (False, "p_bool", False)),
            (b"Center", (True, "p_bool", False)),
            (b"KeepRatio", (True, "p_bool", False)),
            # End of BackPlate...
            (b"BackgroundAlphaTreshold", (0.5, "p_double", False)),
            (b"ShowBackplate", (True, "p_bool", False)),
            (b"BackPlaneOffsetX", (0.0, "p_number", True)),
            (b"BackPlaneOffsetY", (0.0, "p_number", True)),
            (b"BackPlaneRotation", (0.0, "p_number", True)),
            (b"BackPlaneScaleX", (1.0, "p_number", True)),
            (b"BackPlaneScaleY", (1.0, "p_number", True)),
            (b"Background Texture", (None, "p_object", False)),
            (b"FrontPlateFitImage", (True, "p_bool", False)),
            (b"FrontPlateCrop", (False, "p_bool", False)),
            (b"FrontPlateCenter", (True, "p_bool", False)),
            (b"FrontPlateKeepRatio", (True, "p_bool", False)),
            (b"Foreground Opacity", (1.0, "p_double", False)),
            (b"ShowFrontplate", (True, "p_bool", False)),
            (b"FrontPlaneOffsetX", (0.0, "p_number", True)),
            (b"FrontPlaneOffsetY", (0.0, "p_number", True)),
            (b"FrontPlaneRotation", (0.0, "p_number", True)),
            (b"FrontPlaneScaleX", (1.0, "p_number", True)),
            (b"FrontPlaneScaleY", (1.0, "p_number", True)),
            (b"Foreground Texture", (None, "p_object", False)),
            (b"DisplaySafeArea", (False, "p_bool", False)),
            (b"DisplaySafeAreaOnRender", (False, "p_bool", False)),
            (b"SafeAreaDisplayStyle", (1, "p_enum", False)),  # 1 = rounded corners.
            (b"SafeAreaAspectRatio", (1.3333333333333333, "p_double", False)),
            (b"Use2DMagnifierZoom", (False, "p_bool", False)),
            (b"2D Magnifier Zoom", (100.0, "p_number", True)),
            (b"2D Magnifier X", (50.0, "p_number", True)),
            (b"2D Magnifier Y", (50.0, "p_number", True)),
            (b"CameraProjectionType", (0, "p_enum", False)),  # 0 = perspective, 1 = orthogonal.
            (b"OrthoZoom", (1.0, "p_double", False)),
            (b"UseRealTimeDOFAndAA", (False, "p_bool", False)),
            (b"UseDepthOfField", (False, "p_bool", False)),
            (b"FocusSource", (0, "p_enum", False)),  # 0 = camera interest, 1 = distance from camera interest.
            (b"FocusAngle", (3.5, "p_double", False)),  # ???
            (b"FocusDistance", (200.0, "p_double", False)),
            (b"UseAntialiasing", (False, "p_bool", False)),
            (b"AntialiasingIntensity", (0.77777, "p_double", False)),
            (b"AntialiasingMethod", (0, "p_enum", False)),  # 0 = oversampling, 1 = hardware.
            (b"UseAccumulationBuffer", (False, "p_bool", False)),
            (b"FrameSamplingCount", (7, "p_integer", False)),
            (b"FrameSamplingType", (1, "p_enum", False)),  # 0 = uniform, 1 = stochastic.
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"NodeAttribute", b"FbxCamera", props, nbr_users, [False])
    
    
    def fbx_template_def_bone(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"NodeAttribute", b"LimbNode", props, nbr_users, [False])
    
    
    def fbx_template_def_geometry(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (b"Color", ((0.8, 0.8, 0.8), "p_color_rgb", False)),
            (b"BBoxMin", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"BBoxMax", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"Primary Visibility", (True, "p_bool", False)),
            (b"Casts Shadows", (True, "p_bool", False)),
            (b"Receive Shadows", (True, "p_bool", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Geometry", b"FbxMesh", props, nbr_users, [False])
    
    
    def fbx_template_def_material(scene, settings, override_defaults=None, nbr_users=0):
        # WIP...
        props = OrderedDict((
            (b"ShadingModel", ("Phong", "p_string", False)),
            (b"MultiLayer", (False, "p_bool", False)),
            # Lambert-specific.
            (b"EmissiveColor", ((0.0, 0.0, 0.0), "p_color", True)),
            (b"EmissiveFactor", (1.0, "p_number", True)),
            (b"AmbientColor", ((0.2, 0.2, 0.2), "p_color", True)),
            (b"AmbientFactor", (1.0, "p_number", True)),
            (b"DiffuseColor", ((0.8, 0.8, 0.8), "p_color", True)),
            (b"DiffuseFactor", (1.0, "p_number", True)),
            (b"TransparentColor", ((0.0, 0.0, 0.0), "p_color", True)),
            (b"TransparencyFactor", (0.0, "p_number", True)),
            (b"Opacity", (1.0, "p_number", True)),
            (b"NormalMap", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"Bump", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"BumpFactor", (1.0, "p_double", False)),
            (b"DisplacementColor", ((0.0, 0.0, 0.0), "p_color_rgb", False)),
            (b"DisplacementFactor", (1.0, "p_double", False)),
            (b"VectorDisplacementColor", ((0.0, 0.0, 0.0), "p_color_rgb", False)),
            (b"VectorDisplacementFactor", (1.0, "p_double", False)),
            # Phong-specific.
            (b"SpecularColor", ((0.2, 0.2, 0.2), "p_color", True)),
            (b"SpecularFactor", (1.0, "p_number", True)),
            # Not sure about the name, importer uses this (but ShininessExponent for tex prop name!)
            # And in fbx exported by sdk, you have one in template, the other in actual material!!! :/
            # For now, using both.
            (b"Shininess", (20.0, "p_number", True)),
            (b"ShininessExponent", (20.0, "p_number", True)),
            (b"ReflectionColor", ((0.0, 0.0, 0.0), "p_color", True)),
            (b"ReflectionFactor", (1.0, "p_number", True)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Material", b"FbxSurfacePhong", props, nbr_users, [False])
    
    
    def fbx_template_def_texture_file(scene, settings, override_defaults=None, nbr_users=0):
        # WIP...
        # XXX Not sure about all names!
        props = OrderedDict((
            (b"TextureTypeUse", (0, "p_enum", False)),  # Standard.
            (b"AlphaSource", (2, "p_enum", False)),  # Black (i.e. texture's alpha), XXX name guessed!.
            (b"Texture alpha", (1.0, "p_double", False)),
            (b"PremultiplyAlpha", (True, "p_bool", False)),
            (b"CurrentTextureBlendMode", (1, "p_enum", False)),  # Additive...
            (b"CurrentMappingType", (0, "p_enum", False)),  # UV.
            (b"UVSet", ("default", "p_string", False)),  # UVMap name.
            (b"WrapModeU", (0, "p_enum", False)),  # Repeat.
            (b"WrapModeV", (0, "p_enum", False)),  # Repeat.
            (b"UVSwap", (False, "p_bool", False)),
            (b"Translation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"Rotation", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"Scaling", ((1.0, 1.0, 1.0), "p_vector_3d", False)),
            (b"TextureRotationPivot", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            (b"TextureScalingPivot", ((0.0, 0.0, 0.0), "p_vector_3d", False)),
            # Not sure about those two...
            (b"UseMaterial", (False, "p_bool", False)),
            (b"UseMipMap", (False, "p_bool", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Texture", b"FbxFileTexture", props, nbr_users, [False])
    
    
    def fbx_template_def_video(scene, settings, override_defaults=None, nbr_users=0):
        # WIP...
        props = OrderedDict((
            # All pictures.
            (b"Width", (0, "p_integer", False)),
            (b"Height", (0, "p_integer", False)),
            (b"Path", ("", "p_string_url", False)),
            (b"AccessMode", (0, "p_enum", False)),  # Disk (0=Disk, 1=Mem, 2=DiskAsync).
            # All videos.
            (b"StartFrame", (0, "p_integer", False)),
            (b"StopFrame", (0, "p_integer", False)),
            (b"Offset", (0, "p_timestamp", False)),
            (b"PlaySpeed", (0.0, "p_double", False)),
            (b"FreeRunning", (False, "p_bool", False)),
            (b"Loop", (False, "p_bool", False)),
            (b"InterlaceMode", (0, "p_enum", False)),  # None, i.e. progressive.
            # Image sequences.
            (b"ImageSequence", (False, "p_bool", False)),
            (b"ImageSequenceOffset", (0, "p_integer", False)),
            (b"FrameRate", (0.0, "p_double", False)),
            (b"LastFrame", (0, "p_integer", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Video", b"FbxVideo", props, nbr_users, [False])
    
    
    def fbx_template_def_pose(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Pose", b"", props, nbr_users, [False])
    
    
    def fbx_template_def_deformer(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Deformer", b"", props, nbr_users, [False])
    
    
    def fbx_template_def_animstack(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (b"Description", ("", "p_string", False)),
            (b"LocalStart", (0, "p_timestamp", False)),
            (b"LocalStop", (0, "p_timestamp", False)),
            (b"ReferenceStart", (0, "p_timestamp", False)),
            (b"ReferenceStop", (0, "p_timestamp", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"AnimationStack", b"FbxAnimStack", props, nbr_users, [False])
    
    
    def fbx_template_def_animlayer(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (b"Weight", (100.0, "p_number", True)),
            (b"Mute", (False, "p_bool", False)),
            (b"Solo", (False, "p_bool", False)),
            (b"Lock", (False, "p_bool", False)),
            (b"Color", ((0.8, 0.8, 0.8), "p_color_rgb", False)),
            (b"BlendMode", (0, "p_enum", False)),
            (b"RotationAccumulationMode", (0, "p_enum", False)),
            (b"ScaleAccumulationMode", (0, "p_enum", False)),
            (b"BlendModeBypass", (0, "p_ulonglong", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"AnimationLayer", b"FbxAnimLayer", props, nbr_users, [False])
    
    
    def fbx_template_def_animcurvenode(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (FBX_ANIM_PROPSGROUP_NAME.encode(), (None, "p_compound", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"AnimationCurveNode", b"FbxAnimCurveNode", props, nbr_users, [False])
    
    
    def fbx_template_def_animcurve(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"AnimationCurve", b"", props, nbr_users, [False])
    
    
    # ##### Generators for connection elements. #####
    
    def elem_connection(elem, c_type, uid_src, uid_dst, prop_dst=None):
        e = elem_data_single_string(elem, b"C", c_type)
        e.add_int64(uid_src)
        e.add_int64(uid_dst)
        if prop_dst is not None:
            e.add_string(prop_dst)
    
    
    # ##### FBX objects generators. #####
    
    def fbx_data_element_custom_properties(props, bid):
        """
        Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props.
        """
        for k, v in bid.items():
            list_val = getattr(v, "to_list", lambda: None)()
    
            if isinstance(v, str):
                elem_props_set(props, "p_string", k.encode(), v, custom=True)
            elif isinstance(v, int):
                elem_props_set(props, "p_integer", k.encode(), v, custom=True)
            elif isinstance(v, float):
                elem_props_set(props, "p_double", k.encode(), v, custom=True)
            elif list_val and len(list_val) == 3:
                elem_props_set(props, "p_vector", k.encode(), list_val, custom=True)
    
    
    def fbx_data_empty_elements(root, empty, scene_data):
        """
        Write the Empty data block.
        """
        empty_key = scene_data.data_empties[empty]
    
        null = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(empty_key))
        null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute"))
        null.add_string(b"Null")
    
        elem_data_single_string(null, b"TypeFlags", b"Null")
    
        tmpl = elem_props_template_init(scene_data.templates, b"Null")
        props = elem_properties(null)
        elem_props_template_finalize(tmpl, props)
    
        # No custom properties, already saved with object (Model).
    
    
    def fbx_data_lamp_elements(root, lamp, scene_data):
        """
        Write the Lamp data block.
        """
        gscale = scene_data.settings.global_scale
    
        lamp_key = scene_data.data_lamps[lamp]
        do_light = True
        decay_type = FBX_LIGHT_DECAY_TYPES['CONSTANT']
        do_shadow = False
        shadow_color = Vector((0.0, 0.0, 0.0))
        if lamp.type not in {'HEMI'}:
            if lamp.type not in {'SUN', 'AREA'}:
                decay_type = FBX_LIGHT_DECAY_TYPES[lamp.falloff_type]
            do_light = (not lamp.use_only_shadow) and (lamp.use_specular or lamp.use_diffuse)
            do_shadow = lamp.shadow_method not in {'NOSHADOW'}
            shadow_color = lamp.shadow_color
    
        light = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(lamp_key))
        light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
        light.add_string(b"Light")
    
        elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION)  # Sic...
    
        tmpl = elem_props_template_init(scene_data.templates, b"Light")
        props = elem_properties(light)
        elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
        elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
        elem_props_template_set(tmpl, props, "p_color", b"Color", lamp.color)
        elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
        elem_props_template_set(tmpl, props, "p_enum", b"DecayType", decay_type)
        elem_props_template_set(tmpl, props, "p_double", b"DecayStart", lamp.distance * gscale)
        elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
        elem_props_template_set(tmpl, props, "p_color", b"ShadowColor", shadow_color)
        if lamp.type in {'SPOT'}:
            elem_props_template_set(tmpl, props, "p_double", b"OuterAngle", math.degrees(lamp.spot_size))
            elem_props_template_set(tmpl, props, "p_double", b"InnerAngle",
                                    math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend)))
        elem_props_template_finalize(tmpl, props)
    
        # Custom properties.
        if scene_data.settings.use_custom_props:
            fbx_data_element_custom_properties(props, lamp)
    
    
    def fbx_data_camera_elements(root, cam_obj, scene_data):
        """
        Write the Camera data blocks.
        """
        gscale = scene_data.settings.global_scale
    
        cam = cam_obj.bdata
        cam_data = cam.data
        cam_key = scene_data.data_cameras[cam_obj]
    
        # Real data now, good old camera!
        # Object transform info.
        loc, rot, scale, matrix, matrix_rot = cam_obj.fbx_object_tx(scene_data)
        up = matrix_rot * Vector((0.0, 1.0, 0.0))
        to = matrix_rot * Vector((0.0, 0.0, -1.0))
        # Render settings.
        # TODO We could export much more...
        render = scene_data.scene.render
        width = render.resolution_x
        height = render.resolution_y
        aspect = width / height
        # Film width & height from mm to inches
        filmwidth = convert_mm_to_inch(cam_data.sensor_width)
        filmheight = convert_mm_to_inch(cam_data.sensor_height)
        filmaspect = filmwidth / filmheight
        # Film offset
        offsetx = filmwidth * cam_data.shift_x
        offsety = filmaspect * filmheight * cam_data.shift_y
    
        cam = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(cam_key))
        cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute"))
        cam.add_string(b"Camera")
    
        tmpl = elem_props_template_init(scene_data.templates, b"Camera")
        props = elem_properties(cam)
    
        elem_props_template_set(tmpl, props, "p_vector", b"Position", loc)
        elem_props_template_set(tmpl, props, "p_vector", b"UpVector", up)
        elem_props_template_set(tmpl, props, "p_vector", b"InterestPosition", loc + to)  # Point, not vector!
        # Should we use world value?
        elem_props_template_set(tmpl, props, "p_color", b"BackgroundColor", (0.0, 0.0, 0.0))
        elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True)
    
        elem_props_template_set(tmpl, props, "p_enum", b"AspectRatioMode", 2)  # FixedResolution
        elem_props_template_set(tmpl, props, "p_double", b"AspectWidth", float(render.resolution_x))
        elem_props_template_set(tmpl, props, "p_double", b"AspectHeight", float(render.resolution_y))
        elem_props_template_set(tmpl, props, "p_double", b"PixelAspectRatio",
                                float(render.pixel_aspect_x / render.pixel_aspect_y))
    
        elem_props_template_set(tmpl, props, "p_double", b"FilmWidth", filmwidth)
        elem_props_template_set(tmpl, props, "p_double", b"FilmHeight", filmheight)
        elem_props_template_set(tmpl, props, "p_double", b"FilmAspectRatio", filmaspect)
        elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetX", offsetx)
        elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetY", offsety)
    
        elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3)  # FocalLength.
        elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2)  # FitHorizontal.
        elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x))
        elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x))
        elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y))
        # No need to convert to inches here...
        elem_props_template_set(tmpl, props, "p_double", b"FocalLength", cam_data.lens)
        elem_props_template_set(tmpl, props, "p_double", b"SafeAreaAspectRatio", aspect)
        # Default to perspective camera.
        elem_props_template_set(tmpl, props, "p_enum", b"CameraProjectionType", 1 if cam_data.type == 'ORTHO' else 0)
        elem_props_template_set(tmpl, props, "p_double", b"OrthoZoom", cam_data.ortho_scale)
    
        elem_props_template_set(tmpl, props, "p_double", b"NearPlane", cam_data.clip_start * gscale)
        elem_props_template_set(tmpl, props, "p_double", b"FarPlane", cam_data.clip_end * gscale)
        elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1)  # RelativeToCamera.
        elem_props_template_set(tmpl, props, "p_double", b"BackPlaneDistance", cam_data.clip_end * gscale)
    
        elem_props_template_finalize(tmpl, props)
    
        # Custom properties.
        if scene_data.settings.use_custom_props:
            fbx_data_element_custom_properties(props, cam_data)
    
        elem_data_single_string(cam, b"TypeFlags", b"Camera")
        elem_data_single_int32(cam, b"GeometryVersion", 124)  # Sic...
        elem_data_vec_float64(cam, b"Position", loc)
        elem_data_vec_float64(cam, b"Up", up)
        elem_data_vec_float64(cam, b"LookAt", to)
        elem_data_single_int32(cam, b"ShowInfoOnMoving", 1)
        elem_data_single_int32(cam, b"ShowAudio", 0)
        elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0))
        elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)
    
    
    def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=None, mat_world_arm=None, bones=[]):
        """
        Helper, since bindpose are used by both meshes shape keys and armature bones...
        """
        if arm_obj is None:
            arm_obj = me_obj
        # We assume bind pose for our bones are their "Editmode" pose...
        # All matrices are expected in global (world) space.
        bindpose_key = get_blender_bindpose_key(arm_obj.bdata, me)
        fbx_pose = elem_data_single_int64(root, b"Pose", get_fbx_uuid_from_key(bindpose_key))
        fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose"))
        fbx_pose.add_string(b"BindPose")
    
        elem_data_single_string(fbx_pose, b"Type", b"BindPose")
        elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION)
        elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + (1 if (arm_obj != me_obj) else 0) + len(bones))
    
        # First node is mesh/object.
        mat_world_obj = me_obj.fbx_object_matrix(scene_data, global_space=True)
        fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
        elem_data_single_int64(fbx_posenode, b"Node", me_obj.fbx_uuid)
        elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj))
        # Second node is armature object itself.
        if arm_obj != me_obj:
            fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
            elem_data_single_int64(fbx_posenode, b"Node", arm_obj.fbx_uuid)
            elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_arm))
        # And all bones of armature!
        mat_world_bones = {}
        for bo_obj in bones:
            bomat = bo_obj.fbx_object_matrix(scene_data, rest=True, global_space=True)
            mat_world_bones[bo_obj] = bomat
            fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
            elem_data_single_int64(fbx_posenode, b"Node", bo_obj.fbx_uuid)
            elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(bomat))
    
        return mat_world_obj, mat_world_bones
    
    
    def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx_me_props):
        """
        Write shape keys related data.
        """
        if me not in scene_data.data_deformers_shape:
            return
    
        write_normals = True  # scene_data.settings.mesh_smooth_type in {'OFF'}
    
        # First, write the geometry data itself (i.e. shapes).
        _me_key, shape_key, shapes = scene_data.data_deformers_shape[me]
    
        channels = []
    
        for shape, (channel_key, geom_key, shape_verts_co, shape_verts_idx) in shapes.items():
            # Use vgroups as weights, if defined.
            if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups:
                shape_verts_weights = [0.0] * (len(shape_verts_co) // 3)
                vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
                for sk_idx, v_idx in enumerate(shape_verts_idx):
                    for vg in me.vertices[v_idx].groups:
                        if vg.group == vg_idx:
                            shape_verts_weights[sk_idx] = vg.weight * 100.0
            else:
                shape_verts_weights = [100.0] * (len(shape_verts_co) // 3)
            channels.append((channel_key, shape, shape_verts_weights))
    
            geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(geom_key))
            geom.add_string(fbx_name_class(shape.name.encode(), b"Geometry"))
            geom.add_string(b"Shape")
    
            tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
            props = elem_properties(geom)
            elem_props_template_finalize(tmpl, props)
    
            elem_data_single_int32(geom, b"Version", FBX_GEOMETRY_SHAPE_VERSION)
    
            elem_data_single_int32_array(geom, b"Indexes", shape_verts_idx)
            elem_data_single_float64_array(geom, b"Vertices", shape_verts_co)
            if write_normals:
                elem_data_single_float64_array(geom, b"Normals", [0.0] * len(shape_verts_co))
    
        # Yiha! BindPose for shapekeys too! Dodecasigh...
        # XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/
        fbx_data_bindpose_element(root, me_obj, me, scene_data)
    
        # ...and now, the deformers stuff.
        fbx_shape = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(shape_key))
        fbx_shape.add_string(fbx_name_class(me.name.encode(), b"Deformer"))
        fbx_shape.add_string(b"BlendShape")
    
        elem_data_single_int32(fbx_shape, b"Version", FBX_DEFORMER_SHAPE_VERSION)
    
        for channel_key, shape, shape_verts_weights in channels:
            fbx_channel = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(channel_key))
            fbx_channel.add_string(fbx_name_class(shape.name.encode(), b"SubDeformer"))
            fbx_channel.add_string(b"BlendShapeChannel")
    
            elem_data_single_int32(fbx_channel, b"Version", FBX_DEFORMER_SHAPECHANNEL_VERSION)
            elem_data_single_float64(fbx_channel, b"DeformPercent", shape.value * 100.0)  # Percents...
            elem_data_single_float64_array(fbx_channel, b"FullWeights", shape_verts_weights)
    
            # *WHY* add this in linked mesh properties too? *cry*
            # No idea whether it’s percent here too, or more usual factor (assume percentage for now) :/
            elem_props_template_set(fbx_me_tmpl, fbx_me_props, "p_number", shape.name.encode(), shape.value * 100.0,
                                    animatable=True)
    
    
    def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
        """
        Write the Mesh (Geometry) data block.
        """
        # Ugly helper... :/
        def _infinite_gen(val):
            while 1:
                yield val
    
        me_key, me, _free = scene_data.data_meshes[me_obj]
    
        # In case of multiple instances of same mesh, only write it once!
        if me_key in done_meshes:
            return
    
        # No gscale/gmat here, all data are supposed to be in object space.
        smooth_type = scene_data.settings.mesh_smooth_type
        write_normals = True  # smooth_type in {'OFF'}
    
        do_bake_space_transform = me_obj.use_bake_space_transform(scene_data)
    
        # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
        # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
        geom_mat_co = scene_data.settings.global_matrix if do_bake_space_transform else None
        # We need to apply the inverse transpose of the global matrix when transforming normals.
        geom_mat_no = Matrix(scene_data.settings.global_matrix_inv_transposed) if do_bake_space_transform else None
        if geom_mat_no is not None:
            # Remove translation & scaling!
            geom_mat_no.translation = Vector()
            geom_mat_no.normalize()
    
        geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(me_key))
        geom.add_string(fbx_name_class(me.name.encode(), b"Geometry"))
        geom.add_string(b"Mesh")
    
        tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
        props = elem_properties(geom)
    
        # Custom properties.
        if scene_data.settings.use_custom_props:
            fbx_data_element_custom_properties(props, me)
    
        elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
    
        # Vertex cos.
        t_co = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
        me.vertices.foreach_get("co", t_co)
        elem_data_single_float64_array(geom, b"Vertices", chain(*vcos_transformed_gen(t_co, geom_mat_co)))
        del t_co
    
        # Polygon indices.
        #
        # We do loose edges as two-vertices faces, if enabled...
        #
        # Note we have to process Edges in the same time, as they are based on poly's loops...
        loop_nbr = len(me.loops)
        t_pvi = array.array(data_types.ARRAY_INT32, (0,)) * loop_nbr
        t_ls = [None] * len(me.polygons)
    
        me.loops.foreach_get("vertex_index", t_pvi)
        me.polygons.foreach_get("loop_start", t_ls)
    
        # Add "fake" faces for loose edges.
        if scene_data.settings.use_mesh_edges:
            t_le = tuple(e.vertices for e in me.edges if e.is_loose)
            t_pvi.extend(chain(*t_le))
            t_ls.extend(range(loop_nbr, loop_nbr + len(t_le), 2))
            del t_le
    
        # Edges...
        # Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array.
        #       The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon.
        #       Advantage: Only one index per edge.
        #       Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons
        #                 for loose edges).
        #       We also have to store a mapping from real edges to their indices in this array, for edge-mapped data
        #       (like e.g. crease).
        t_eli = array.array(data_types.ARRAY_INT32)
        edges_map = {}
        edges_nbr = 0
        if t_ls and t_pvi:
            t_ls = set(t_ls)
            todo_edges = [None] * len(me.edges) * 2
            # Sigh, cannot access edge.key through foreach_get... :/
            me.edges.foreach_get("vertices", todo_edges)
            todo_edges = set((v1, v2) if v1 < v2 else (v2, v1) for v1, v2 in zip(*(iter(todo_edges),) * 2))
    
            li = 0
            vi = vi_start = t_pvi[0]
            for li_next, vi_next in enumerate(t_pvi[1:] + t_pvi[:1], start=1):
                if li_next in t_ls:  # End of a poly's loop.
                    vi2 = vi_start
                    vi_start = vi_next
                else:
                    vi2 = vi_next
    
                e_key = (vi, vi2) if vi < vi2 else (vi2, vi)
                if e_key in todo_edges:
                    t_eli.append(li)
                    todo_edges.remove(e_key)
                    edges_map[e_key] = edges_nbr
                    edges_nbr += 1
    
                vi = vi_next
                li = li_next
        # End of edges!
    
        # We have to ^-1 last index of each loop.
        for ls in t_ls:
            t_pvi[ls - 1] ^= -1
    
        # And finally we can write data!
        elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi)
        elem_data_single_int32_array(geom, b"Edges", t_eli)
        del t_pvi
        del t_ls
        del t_eli
    
        # And now, layers!
    
        # Smoothing.
        if smooth_type in {'FACE', 'EDGE'}:
            t_ps = None
            _map = b""
            if smooth_type == 'FACE':
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
                me.polygons.foreach_get("use_smooth", t_ps)
                _map = b"ByPolygon"
            else:  # EDGE
                # Write Edge Smoothing.
                # Note edge is sharp also if it's used by more than two faces, or one of its faces is flat.
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * edges_nbr
                sharp_edges = set()
                temp_sharp_edges = {}
                for p in me.polygons:
                    if not p.use_smooth:
                        sharp_edges.update(p.edge_keys)
                        continue
                    for k in p.edge_keys:
                        if temp_sharp_edges.setdefault(k, 0) > 1:
                            sharp_edges.add(k)
                        else:
                            temp_sharp_edges[k] += 1
                del temp_sharp_edges
                for e in me.edges:
                    if e.key not in edges_map:
                        continue  # Only loose edges, in theory!
                    t_ps[edges_map[e.key]] = not (e.use_edge_sharp or (e.key in sharp_edges))
                _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")
            elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps)  # Sight, int32 for bool...
            del t_ps
    
        # TODO: Edge crease (LayerElementCrease).
    
        # And we are done with edges!
        del edges_map
    
        # Loop normals.
        tspacenumber = 0
        if write_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...
            me.calc_normals_split()
    
            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
                # del t_lnw
            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:
                    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",
                                                       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()
    
            me.free_normals_split()
    
        # Write VertexColor Layers.
        vcolnumber = len(me.vertex_colors)
        if vcolnumber:
            def _coltuples_gen(raw_cols):
                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)}
                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)}
                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 = scene_data.mesh_mat_indices.get(me)
        if me_fbxmats_idx is not None:
            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.
                    # Note a mat might not be in me_fbxmats_idx (e.g. node mats are ignored).
                    blmats_to_fbxmats_idxs = [me_fbxmats_idx[m] for m in me_blmats if m in me_fbxmats_idx]
                    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)
        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 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)
    
        # 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 check_skip_material(mat):
        """Simple helper to check whether we actually support exporting that material or not"""
        return mat.type not in {'SURFACE'} or mat.use_nodes
    
    
    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]
        skip_mat = check_skip_material(mat)
        mat_type = b"Phong"
        # Approximation...
        if not skip_mat and mat.specular_shader not in {'COOKTORR', 'PHONG', 'BLINN'}:
            mat_type = b"Lambert"
    
        fbx_mat = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(mat_key))
        fbx_mat.add_string(fbx_name_class(mat.name.encode(), b"Material"))
        fbx_mat.add_string(b"")
    
        elem_data_single_int32(fbx_mat, b"Version", FBX_MATERIAL_VERSION)
        # those are not yet properties, it seems...
        elem_data_single_string(fbx_mat, b"ShadingModel", mat_type)
        elem_data_single_int32(fbx_mat, b"MultiLayer", 0)  # Should be bool...
    
        tmpl = elem_props_template_init(scene_data.templates, b"Material")
        props = elem_properties(fbx_mat)
    
        if not skip_mat:
            elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", mat_type.decode())
            elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", mat.diffuse_color)
            elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", mat.emit)
            elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
            elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", mat.ambient)
            elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", mat.diffuse_color)
            elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", mat.diffuse_intensity)
            elem_props_template_set(tmpl, props, "p_color", b"TransparentColor",
                                    mat.diffuse_color if mat.use_transparency else (1.0, 1.0, 1.0))
            elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor",
                                    1.0 - mat.alpha if mat.use_transparency else 0.0)
            elem_props_template_set(tmpl, props, "p_number", b"Opacity", mat.alpha if mat.use_transparency else 1.0)
            elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
            # Not sure about those...
            """
            b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"BumpFactor": (1.0, "p_double"),
            b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
            b"DisplacementFactor": (0.0, "p_double"),
            """
            if mat_type == b"Phong":
                elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", mat.specular_color)
                elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", mat.specular_intensity / 2.0)
                # See Material template about those two!
                elem_props_template_set(tmpl, props, "p_number", b"Shininess", (mat.specular_hardness - 1.0) / 5.10)
                elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", (mat.specular_hardness - 1.0) / 5.10)
                elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", mat.mirror_color)
                elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor",
                                        mat.raytrace_mirror.reflect_factor if mat.raytrace_mirror.use else 0.0)
    
        elem_props_template_finalize(tmpl, props)
    
        # Custom properties.
        if scene_data.settings.use_custom_props:
            fbx_data_element_custom_properties(props, mat)
    
    
    def _gen_vid_path(img, scene_data):
        msetts = scene_data.settings.media_settings
        fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode,
                                                       msetts.subdir, msetts.copy_set, img.library)
        fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel)))
        return fname_abs, fname_rel
    
    
    def fbx_data_texture_file_elements(root, tex, scene_data):
        """
        Write the (file) Texture data block.
        """
        # XXX All this is very fuzzy to me currently...
        #     Textures do not seem to use properties as much as they could.
        #     For now assuming most logical and simple stuff.
    
        tex_key, _mats = scene_data.data_textures[tex]
        img = tex.texture.image
        fname_abs, fname_rel = _gen_vid_path(img, scene_data)
    
        fbx_tex = elem_data_single_int64(root, b"Texture", get_fbx_uuid_from_key(tex_key))
        fbx_tex.add_string(fbx_name_class(tex.name.encode(), b"Texture"))
        fbx_tex.add_string(b"")
    
        elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip")
        elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION)
        elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(tex.name.encode(), b"Texture"))
        elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video"))
        elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs)
        elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel)
    
        alpha_source = 0  # None
        if img.use_alpha:
            if tex.texture.use_calculate_alpha:
                alpha_source = 1  # RGBIntensity as alpha.
            else:
                alpha_source = 2  # Black, i.e. alpha channel.
        # BlendMode not useful for now, only affects layered textures afaics.
        mapping = 0  # UV.
        uvset = 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'}:
            mapping = 0  # UV
            # Yuck, UVs are linked by mere names it seems... :/
            uvset = tex.uv_layer
        wrap_mode = 1  # Clamp
        if tex.texture.extension in {'REPEAT'}:
            wrap_mode = 0  # Repeat
    
        tmpl = elem_props_template_init(scene_data.templates, b"TextureFile")
        props = elem_properties(fbx_tex)
        elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source)
        elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha",
                                img.alpha_mode in {'STRAIGHT'})  # Or is it PREMUL?
        elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping)
        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.offset)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", 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", tex.texture.use_mipmap)
        elem_props_template_finalize(tmpl, props)
    
        # Custom properties.
        if scene_data.settings.use_custom_props:
            fbx_data_element_custom_properties(props, tex.texture)
    
    
    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)
    
        bone_radius_scale = 33.0
    
        # Bones "data".
        for bo_obj in bones:
            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")
            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)
            elem_props_template_finalize(tmpl, props)
    
            # Custom properties.
            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():
                # BindPose.
                mat_world_obj, mat_world_bones = fbx_data_bindpose_element(root, ob_obj, me, scene_data,
                                                                           arm_obj, mat_world_arm, bones)
    
                # Deformer.
                fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key))
                fbx_skin.add_string(fbx_name_class(arm_obj.name.encode(), b"Deformer"))
                fbx_skin.add_string(b"Skin")
    
                elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
                elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0)  # Only vague idea what it is...
    
                # Pre-process vertex weights (also to check vertices assigned ot more than four bones).
                ob = ob_obj.bdata
                bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
                             for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
                valid_idxs = set(bo_vg_idx.values())
                vgroups = {vg.index: OrderedDict() for vg in ob.vertex_groups}
                verts_vgroups = (sorted(((vg.group, vg.weight) for vg in v.groups if vg.weight and vg.group in valid_idxs),
                                        key=lambda e: e[1], reverse=True)
                                 for v in me.vertices)
                for idx, vgs in enumerate(verts_vgroups):
                    for vg_idx, w in vgs:
                        vgroups[vg_idx][idx] = w
    
                for bo_obj, clstr_key in clusters.items():
                    bo = bo_obj.bdata
                    # Find which vertices are affected by this bone/vgroup pair, and matching weights.
                    # Note we still write a cluster for bones not affecting the mesh, to get 'rest pose' data
                    # (the TransformBlah matrices).
                    vg_idx = bo_vg_idx.get(bo.name, None)
                    indices, weights = ((), ()) if vg_idx is None or not vgroups[vg_idx] else zip(*vgroups[vg_idx].items())
    
                    # Create the cluster.
                    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,
                    #          **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))
                    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...
        if ob_obj.is_bone:
            obj_type = b"LimbNode"
        elif (ob_obj.type == 'ARMATURE'):
            #~ obj_type = b"Root"
            obj_type = b"Null"
        elif (ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE):
            obj_type = b"Mesh"
        elif (ob_obj.type == 'LAMP'):
            obj_type = b"Light"
        elif (ob_obj.type == 'CAMERA'):
            obj_type = b"Camera"
        model = elem_data_single_int64(root, b"Model", ob_obj.fbx_uuid)
        model.add_string(fbx_name_class(ob_obj.name.encode(), b"Model"))
        model.add_string(obj_type)
    
        elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
    
        # Object transform info.
        loc, rot, scale, matrix, matrix_rot = ob_obj.fbx_object_tx(scene_data)
        rot = tuple(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:
            fbx_data_element_custom_properties(props, ob_obj.bdata)
    
        # Those settings would obviously need to be edited in a complete version of the exporter, may depends on
        # object type, etc.
        elem_data_single_int32(model, b"MultiLayer", 0)
        elem_data_single_int32(model, b"MultiTake", 0)
        elem_data_single_bool(model, b"Shading", True)
        elem_data_single_string(model, b"Culling", b"CullingOff")
    
        if obj_type == b"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_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)
    
        elem_props_template_finalize(tmpl, props)
    
    
    def fbx_data_animation_elements(root, scene_data):
        """
        Write animation data.
        """
        animations = scene_data.animations
        if not animations:
            return
        scene = scene_data.scene
    
        fps = scene.render.fps / scene.render.fps_base
    
        def keys_to_ktimes(keys):
            return (int(v) for v in 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
            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():
                # Animation layer.
                # 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"))
                # alayer.add_string(b"")
    
                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)
    
                        # Only create Animation curve if needed!
                        if keys:
                            acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
                            acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
                            acurve.add_string(b"")
    
                            # key attributes...
                            nbr_keys = len(keys)
                            # flags...
                            keyattr_flags = (
                                1 << 2 |   # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
                                1 << 8 |   # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
                                1 << 13 |  # tangent mode, 12 = generic clamp, 13 = generic time independent,
                                1 << 14 |  # tangent mode, 13 + 14 = generic clamp progressive.
                                0,
                            )
                            # Maybe values controlling TCB & co???
                            keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
    
                            # And now, the *real* data!
                            elem_data_single_float64(acurve, b"Default", def_value)
                            elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
                            elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
                            elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
                            elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
                            elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
                            elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
    
                    elem_props_template_finalize(acn_tmpl, acn_props)
    
    
    # ##### Top-level FBX data container. #####
    
    def fbx_mat_properties_from_texture(tex):
        """
        Returns a set of FBX metarial properties that are affected by the given texture.
        Quite obviously, this is a fuzzy and far-from-perfect mapping! Amounts of influence are completely lost, e.g.
        Note tex is actually expected to be a texture slot.
        """
        # Mapping Blender -> FBX (blend_use_name, blend_fact_name, fbx_name).
        blend_to_fbx = (
            # Lambert & Phong...
            ("diffuse", "diffuse", b"DiffuseFactor"),
            ("color_diffuse", "diffuse_color", b"DiffuseColor"),
            ("alpha", "alpha", b"TransparencyFactor"),
            ("diffuse", "diffuse", b"TransparentColor"),  # Uses diffuse color in Blender!
            ("emit", "emit", b"EmissiveFactor"),
            ("diffuse", "diffuse", b"EmissiveColor"),  # Uses diffuse color in Blender!
            ("ambient", "ambient", b"AmbientFactor"),
            # ("", "", b"AmbientColor"),  # World stuff in Blender, for now ignore...
            ("normal", "normal", b"NormalMap"),
            # Note: unsure about those... :/
            # ("", "", b"Bump"),
            # ("", "", b"BumpFactor"),
            # ("", "", b"DisplacementColor"),
            # ("", "", b"DisplacementFactor"),
            # Phong only.
            ("specular", "specular", b"SpecularFactor"),
            ("color_spec", "specular_color", b"SpecularColor"),
            # See Material template about those two!
            ("hardness", "hardness", b"Shininess"),
            ("hardness", "hardness", b"ShininessExponent"),
            ("mirror", "mirror", b"ReflectionColor"),
            ("raymir", "raymir", b"ReflectionFactor"),
        )
    
        tex_fbx_props = set()
        for use_map_name, name_factor, fbx_prop_name in blend_to_fbx:
            # Always export enabled textures, even if they have a null influence...
            if getattr(tex, "use_map_" + use_map_name):
                tex_fbx_props.add(fbx_prop_name)
    
        return tex_fbx_props
    
    
    def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
                                   data_bones, data_deformers_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)
    
        arm_data = arm_obj.bdata.data
        bones = OrderedDict()
        for bo in arm_obj.bones:
            if settings.use_armature_deform_only:
                if bo.bdata.use_deform:
                    bones[bo] = True
                    bo_par = bo.parent
                    while bo_par.is_bone:
                        bones[bo_par] = True
                        bo_par = bo_par.parent
                elif bo not in bones:  # Do not override if already set in the loop above!
                    bones[bo] = False
            else:
                bones[bo] = True
    
        bones = OrderedDict((bo, None) for bo, use in bones.items() if use)
    
        if not bones:
            return
    
        data_bones.update((bo, get_blender_bone_key(arm_obj.bdata, bo.bdata)) for bo in bones)
    
        for ob_obj in objects:
            if not ob_obj.is_deformed_by_armature(arm_obj):
                continue
    
            # Always handled by an Armature modifier...
            found = False
            for mod in ob_obj.bdata.modifiers:
                if mod.type not in {'ARMATURE'}:
                    continue
                # We only support vertex groups binding method, not bone envelopes one!
                if mod.object == arm_obj.bdata and mod.use_vertex_groups:
                    found = True
                    break
    
            if not found:
                continue
    
            # Now we have a mesh using this armature.
            # 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 = OrderedDict((bo, get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata)) for bo in bones)
            data_deformers_skin.setdefault(arm_obj, OrderedDict())[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
                                                                          ob_obj, clusters)
    
            # We don't want a regular parent relationship for those in FBX...
            arm_parents.add((arm_obj, ob_obj))
            # Needed to handle matrices/spaces (since we do not parent them to 'armature' in FBX :/ ).
            ob_obj.parented_to_armature = True
    
        objects.update(bones)
    
    
    def fbx_generate_leaf_bones(settings, data_bones):
        # find which bons have no children
        child_count = {bo: 0 for bo, _bo_key in data_bones.items()}
        for bo, _bo_key in data_bones.items():
            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
            node_uuid = get_fbx_uuid_from_key(node_name + "_node")
            attr_uuid = get_fbx_uuid_from_key(node_name + "_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
            if settings.bone_correction_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
        scene = scene_data.scene
        meshes = scene_data.data_meshes
        force_keying = scene_data.settings.bake_anim_use_all_bones
    
        if objects is not None:
            # Add bones and duplis!
            for ob_obj in tuple(objects):
                if not ob_obj.is_object:
                    continue
                if ob_obj.type == 'ARMATURE':
                    objects |= {bo_obj for bo_obj in ob_obj.bones if bo_obj in scene_data.objects}
                ob_obj.dupli_list_create(scene, 'RENDER')
                for dp_obj in ob_obj.dupli_list:
                    if dp_obj in scene_data.objects:
                        objects.add(dp_obj)
                ob_obj.dupli_list_clear()
        else:
            objects = scene_data.objects
    
        back_currframe = scene.frame_current
        animdata_ob = OrderedDict()
        p_rots = {}
    
        for ob_obj in objects:
            ACNW = AnimationCurveNodeWrapper
            loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data)
            rot_deg = tuple(convert_rad_to_deg_iter(rot))
            animdata_ob[ob_obj] = (ACNW(ob_obj.key, 'LCL_TRANSLATION', ob_obj.is_bone and force_keying, loc),
                                   ACNW(ob_obj.key, 'LCL_ROTATION', ob_obj.is_bone and force_keying, rot_deg),
                                   ACNW(ob_obj.key, 'LCL_SCALING', ob_obj.is_bone and force_keying, scale))
            p_rots[ob_obj] = rot
    
        animdata_shapes = OrderedDict()
        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', False, (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)
    
        currframe = f_start
        while currframe <= f_end:
            real_currframe = currframe - f_start if start_zero else currframe
            scene.frame_set(int(currframe), currframe - int(currframe))
    
            for ob_obj in animdata_ob:
                ob_obj.dupli_list_create(scene, 'RENDER')
            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!).
                p_rot = p_rots.get(ob_obj, None)
                loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
                p_rots[ob_obj] = rot
                anim_loc.add_keyframe(real_currframe, loc)
                anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
                anim_scale.add_keyframe(real_currframe, scale)
            for ob_obj in objects:
                ob_obj.dupli_list_clear()
            for anim_shape, me, shape in animdata_shapes.values():
                anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
            currframe += bake_step
    
        scene.frame_set(back_currframe, 0.0)
    
        animations = OrderedDict()
        simplify_fac = scene_data.settings.bake_anim_simplify_factor
    
        # And now, produce final data (usable by FBX export code)
        # Objects-like loc/rot/scale...
        for ob_obj, anims in animdata_ob.items():
            for anim in anims:
                anim.simplify(simplify_fac, bake_step, force_keep)
                if not anim:
                    continue
                for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
                    anim_data = animations.get(obj_key)
                    if anim_data is None:
                        anim_data = animations[obj_key] = ("dummy_unused_key", OrderedDict())
                    anim_data[1][fbx_group] = (group_key, group, fbx_gname)
    
        # And meshes' shape keys.
        for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
            final_keys = OrderedDict()
            anim_shape.simplify(simplify_fac, bake_step, force_keep)
            if not anim_shape:
                continue
            for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
                    anim_data = animations.get(elem_key)
                    if anim_data is None:
                        anim_data = animations[elem_key] = ("dummy_unused_key", OrderedDict())
                    anim_data[1][fbx_group] = (group_key, group, fbx_gname)
    
        astack_key = get_blender_anim_stack_key(scene, ref_id)
        alayer_key = get_blender_anim_layer_key(scene, ref_id)
        name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
    
        if start_zero:
            f_end -= f_start
            f_start = 0.0
    
        return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
    
    
    def fbx_animations(scene_data):
        """
        Generate global animation data from objects.
        """
        scene = scene_data.scene
        animations = []
        animated = set()
        frame_start = 1e100
        frame_end = -1e100
    
        def add_anim(animations, animated, anim):
            nonlocal frame_start, frame_end
            if anim is not None:
                animations.append(anim)
                f_start, f_end = anim[4:6]
                if f_start < frame_start:
                    frame_start = f_start
                if f_end > frame_end:
                    frame_end = f_end
    
                _astack_key, astack, _alayer_key, _name, _fstart, _fend = anim
                for elem_key, (alayer_key, acurvenodes) in astack.items():
                    for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
                        animated.add((elem_key, fbx_prop))
    
        # Per-NLA strip animstacks.
        if scene_data.settings.bake_anim_use_nla_strips:
            strips = []
            for ob_obj in scene_data.objects:
                # NLA tracks only for objects, not bones!
                if not ob_obj.is_object:
                    continue
                ob = ob_obj.bdata  # Back to real Blender Object.
                if not ob.animation_data:
                    continue
                for track in ob.animation_data.nla_tracks:
                    if track.mute:
                        continue
                    for strip in track.strips:
                        if strip.mute:
                            continue
                        strips.append(strip)
                        strip.mute = True
    
            for strip in strips:
                strip.mute = False
                add_anim(animations, animated,
                         fbx_animations_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
                strip.mute = True
    
            for strip in strips:
                strip.mute = False
    
        # All actions.
        if scene_data.settings.bake_anim_use_all_actions:
            def validate_actions(act, path_resolve):
                for fc in act.fcurves:
                    data_path = fc.data_path
                    if fc.array_index:
                        data_path = data_path + "[%d]" % fc.array_index
                    try:
                        path_resolve(data_path)
                    except ValueError:
                        return False  # Invalid.
                return True  # Valid.
    
            def restore_object(ob_to, ob_from):
                # Restore org state of object (ugh :/ ).
                props = (
                    'location', 'rotation_quaternion', 'rotation_axis_angle', 'rotation_euler', 'rotation_mode', 'scale',
                    'delta_location', 'delta_rotation_euler', 'delta_rotation_quaternion', 'delta_scale',
                    'lock_location', 'lock_rotation', 'lock_rotation_w', 'lock_rotations_4d', 'lock_scale',
                    'tag', 'layers', 'select', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
                    'matrix_parent_inverse', 'empty_draw_type', 'empty_draw_size', 'empty_image_offset', 'pass_index',
                    'color', 'hide', 'hide_select', 'hide_render', 'use_slow_parent', 'slow_parent_offset',
                    'use_extra_recalc_object', 'use_extra_recalc_data', 'dupli_type', 'use_dupli_frames_speed',
                    'use_dupli_vertices_rotation', 'use_dupli_faces_scale', 'dupli_faces_scale', 'dupli_group',
                    'dupli_frames_start', 'dupli_frames_end', 'dupli_frames_on', 'dupli_frames_off',
                    'draw_type', 'show_bounds', 'draw_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
                    'show_wire', 'show_all_edges', 'show_transparent', 'show_x_ray',
                    'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
                )
                for p in props:
                    if not ob_to.is_property_readonly(p):
                        setattr(ob_to, p, getattr(ob_from, p))
    
            for ob_obj in scene_data.objects:
                # Actions only for objects, not bones!
                if not ob_obj.is_object:
                    continue
    
                ob = ob_obj.bdata  # Back to real Blender Object.
    
                if not ob.animation_data:
                    continue  # Do not export animations for objects that are absolutely not animated, see T44386.
    
                # We can't play with animdata and actions and get back to org state easily.
                # So we have to add a temp copy of the object to the scene, animate it, and remove it... :/
                ob_copy = ob.copy()
                # Great, have to handle bones as well if needed...
                pbones_matrices = [pbo.matrix_basis.copy() for pbo in ob.pose.bones] if ob.type == 'ARMATURE' else ...
    
                org_act = ob.animation_data.action
                path_resolve = ob.path_resolve
    
                for act in bpy.data.actions:
                    # For now, *all* paths in the action must be valid for the object, to validate the action.
                    # Unless that action was already assigned to the object!
                    if act != org_act and not validate_actions(act, path_resolve):
                        continue
                    ob.animation_data.action = act
                    frame_start, frame_end = act.frame_range  # sic!
                    add_anim(animations, animated,
                             fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True, {ob_obj}, True))
                    # Ugly! :/
                    if pbones_matrices is not ...:
                        for pbo, mat in zip(ob.pose.bones, pbones_matrices):
                            pbo.matrix_basis = mat.copy()
                    ob.animation_data.action = org_act
                    restore_object(ob, ob_copy)
    
                if pbones_matrices is not ...:
                    for pbo, mat in zip(ob.pose.bones, pbones_matrices):
                        pbo.matrix_basis = mat.copy()
                ob.animation_data.action = org_act
    
                bpy.data.objects.remove(ob_copy)
    
        # Global (containing everything) animstack, only if not exporting NLA strips and/or all actions.
        if not scene_data.settings.bake_anim_use_nla_strips and not scene_data.settings.bake_anim_use_all_actions:
            add_anim(animations, animated, fbx_animations_do(scene_data, None, scene.frame_start, scene.frame_end, False))
    
        # Be sure to update all matrices back to org state!
        scene.frame_set(scene.frame_current, 0.0)
    
        return animations, animated, frame_start, frame_end
    
    
    def fbx_data_from_scene(scene, settings):
        """
        Do some pre-processing over scene's data...
        """
        objtypes = settings.object_types
        perfmon = PerfMon()
        perfmon.level_up()
    
        # ##### Gathering data...
    
        perfmon.step("FBX export prepare: Wrapping Objects...")
    
        # This is rather simple for now, maybe we could end generating templates with most-used values
        # instead of default ones?
        objects = OrderedDict()  # Because we do not have any ordered set...
        for ob in settings.context_objects:
            if ob.type not in objtypes:
                continue
            ob_obj = ObjectWrapper(ob)
            objects[ob_obj] = None
            # Duplis...
            ob_obj.dupli_list_create(scene, 'RENDER')
            for dp_obj in ob_obj.dupli_list:
                objects[dp_obj] = None
            ob_obj.dupli_list_clear()
    
        perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...")
    
        data_lamps = OrderedDict((ob_obj.bdata.data, get_blenderID_key(ob_obj.bdata.data))
                                 for ob_obj in objects if ob_obj.type == 'LAMP')
        # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
        data_cameras = OrderedDict((ob_obj, get_blenderID_key(ob_obj.bdata.data))
                                   for ob_obj in objects if ob_obj.type == 'CAMERA')
        # Yep! Contains nothing, but needed!
        data_empties = OrderedDict((ob_obj, get_blender_empty_key(ob_obj.bdata))
                                   for ob_obj in objects if ob_obj.type == 'EMPTY')
    
        perfmon.step("FBX export prepare: Wrapping Meshes...")
    
        data_meshes = OrderedDict()
        for ob_obj in objects:
            if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
                continue
            ob = ob_obj.bdata
            if ob in data_meshes:  # Happens with dupli instances.
                continue
            use_org_data = True
            if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES:
                use_org_data = False
                tmp_mods = []
                if ob.type == 'MESH':
                    # No need to create a new mesh in this case, if no modifier is active!
                    use_org_data = True
                    for mod in ob.modifiers:
                        # For meshes, when armature export is enabled, disable Armature modifiers here!
                        if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
                            tmp_mods.append((mod, mod.show_render))
                            mod.show_render = False
                        if mod.show_render:
                            use_org_data = False
                if not use_org_data:
                    tmp_me = ob.to_mesh(scene, apply_modifiers=True, settings='RENDER')
                    data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
                # Re-enable temporary disabled modifiers.
                for mod, show_render in tmp_mods:
                    mod.show_render = show_render
            if use_org_data:
                data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
    
        perfmon.step("FBX export prepare: Wrapping ShapeKeys...")
    
        # ShapeKeys.
        data_deformers_shape = OrderedDict()
        geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
        for me_obj, (me_key, me, _org) in data_meshes.items():
            if not (me.shape_keys and me.shape_keys.key_blocks):
                continue
    
            shapes_key = get_blender_mesh_shape_key(me)
            _cos = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
            me.vertices.foreach_get("co", _cos)
            v_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
            for shape in me.shape_keys.key_blocks:
                # Only write vertices really different from org coordinates!
                # XXX FBX does not like empty shapes (makes Unity crash e.g.), so we have to do this here... :/
                shape_verts_co = []
                shape_verts_idx = []
    
                shape.data.foreach_get("co", _cos)
                sv_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
                for idx, (sv_co, v_co) in enumerate(zip(sv_cos, v_cos)):
                    if similar_values_iter(sv_co, v_co):
                        # Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not
                        #       have this at all... Anyway, this should cover most common cases imho.
                        continue
                    shape_verts_co.extend(Vector(sv_co) - Vector(v_co))
                    shape_verts_idx.append(idx)
                if not shape_verts_co:
                    continue
                channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
                data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
                data_deformers_shape.setdefault(me, (me_key, shapes_key, OrderedDict()))[2][shape] = data
    
        perfmon.step("FBX export prepare: Wrapping Armatures...")
    
        # Armatures!
        data_deformers_skin = OrderedDict()
        data_bones = OrderedDict()
        arm_parents = set()
        for ob_obj in tuple(objects):
            if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
                continue
            fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
                                       data_bones, data_deformers_skin, data_empties, arm_parents)
    
        # Generate leaf bones
        data_leaf_bones = []
        if settings.add_leaf_bones:
            data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones)
    
        perfmon.step("FBX export prepare: Wrapping World...")
    
        # Some world settings are embedded in FBX materials...
        if scene.world:
            data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
        else:
            data_world = OrderedDict()
    
        perfmon.step("FBX export prepare: Wrapping Materials...")
    
        # 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 = OrderedDict()
        for ob_obj in objects:
            # If obj is not a valid object for materials, wrapper will just return an empty tuple...
            for mat_s in ob_obj.material_slots:
                mat = mat_s.material
                if mat is None:
                    continue  # Empty slots!
                # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
                # However, I doubt anything else than Lambert/Phong is really portable!
                # We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing.
                # Note we want to keep a 'dummy' empty mat even when we can't really support it, see T41396.
                mat_data = data_materials.get(mat)
                if mat_data is not None:
                    mat_data[1].append(ob_obj)
                else:
                    data_materials[mat] = (get_blenderID_key(mat), [ob_obj])
    
        perfmon.step("FBX export prepare: Wrapping Textures...")
    
        # Note FBX textures also hold their mapping info.
        # TODO: Support layers?
        data_textures = OrderedDict()
        # FbxVideo also used to store static images...
        data_videos = OrderedDict()
        # For now, do not use world textures, don't think they can be linked to anything FBX wise...
        for mat in data_materials.keys():
            if check_skip_material(mat):
                continue
            for tex, use_tex in zip(mat.texture_slots, mat.use_textures):
                if tex is None or tex.texture is None or not use_tex:
                    continue
                # For now, only consider image textures.
                # Note FBX does has support for procedural, but this is not portable at all (opaque blob),
                # so not useful for us.
                # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
                # if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
                if tex.texture.type not in {'IMAGE'}:
                    continue
                img = tex.texture.image
                if img is None:
                    continue
                # Find out whether we can actually use this texture for this material, in FBX context.
                tex_fbx_props = fbx_mat_properties_from_texture(tex)
                if not tex_fbx_props:
                    continue
                tex_data = data_textures.get(tex)
                if tex_data is not None:
                    tex_data[1][mat] = tex_fbx_props
                else:
                    data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),)))
                vid_data = data_videos.get(img)
                if vid_data is not None:
                    vid_data[1].append(tex)
                else:
                    data_videos[img] = (get_blenderID_key(img), [tex])
    
        perfmon.step("FBX export prepare: Wrapping Animations...")
    
        # Animation...
        animations = ()
        animated = set()
        frame_start = scene.frame_start
        frame_end = scene.frame_end
        if settings.bake_anim:
            # From objects & bones only for a start.
            # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
            tmp_scdata = FBXExportData(
                None, None, None,
                settings, scene, objects, None, None, 0.0, 0.0,
                data_empties, data_lamps, data_cameras, data_meshes, None,
                data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
                data_world, data_materials, data_textures, data_videos,
            )
            animations, animated, frame_start, frame_end = fbx_animations(tmp_scdata)
    
        # ##### Creation of templates...
    
        perfmon.step("FBX export prepare: Generating templates...")
    
        templates = OrderedDict()
        templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
    
        if data_empties:
            templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
    
        if data_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:
            nbr = len(data_meshes)
            if data_deformers_shape:
                nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
            templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr)
    
        if objects:
            templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
    
        if arm_parents:
            # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
            templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
    
        if data_deformers_skin or data_deformers_shape:
            nbr = 0
            if data_deformers_skin:
                nbr += len(data_deformers_skin)
                nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values())
            if data_deformers_shape:
                nbr += len(data_deformers_shape)
                nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
            assert(nbr != 0)
            templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
    
        # No world support in FBX...
        """
        if data_world:
            templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
        """
    
        if data_materials:
            templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
    
        if data_textures:
            templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
    
        if data_videos:
            templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
    
        if animations:
            nbr_astacks = len(animations)
            nbr_acnodes = 0
            nbr_acurves = 0
            for _astack_key, astack, _al, _n, _fs, _fe in animations:
                for _alayer_key, alayer in astack.values():
                    for _acnode_key, acnode, _acnode_name in alayer.values():
                        nbr_acnodes += 1
                        for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
                            if acurve:
                                nbr_acurves += 1
    
            templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
            # Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
            # So for now, only one layer per anim stack.
            templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
            templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
            templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
    
        templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
    
        # ##### Creation of connections...
    
        perfmon.step("FBX export prepare: Generating Connections...")
    
        connections = []
    
        # Objects (with classical parenting).
        for ob_obj in objects:
            # Bones are handled later.
            if not ob_obj.is_bone:
                par_obj = ob_obj.parent
                # Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0).
                if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents:
                    connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None))
                else:
                    connections.append((b"OO", ob_obj.fbx_uuid, 0, None))
    
        # Armature & Bone chains.
        for bo_obj in data_bones.keys():
            par_obj = bo_obj.parent
            if par_obj not in objects:
                continue
            connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None))
    
        # Object data.
        for ob_obj in objects:
            if ob_obj.is_bone:
                bo_data_key = data_bones[ob_obj]
                connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None))
            else:
                if ob_obj.type == 'LAMP':
                    lamp_key = data_lamps[ob_obj.bdata.data]
                    connections.append((b"OO", get_fbx_uuid_from_key(lamp_key), ob_obj.fbx_uuid, None))
                elif ob_obj.type == 'CAMERA':
                    cam_key = data_cameras[ob_obj]
                    connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None))
                elif ob_obj.type == 'EMPTY' or ob_obj.type == 'ARMATURE':
                    empty_key = data_empties[ob_obj]
                    connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
                elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
                    mesh_key, _me, _free = data_meshes[ob_obj]
                    connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
    
        # Leaf Bones
        for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones:
            connections.append((b"OO", node_uuid, par_uuid, None))
            connections.append((b"OO", attr_uuid, node_uuid, None))
    
        # 'Shape' deformers (shape keys, only for meshes currently)...
        for me_key, shapes_key, shapes in data_deformers_shape.values():
            # shape -> geometry
            connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None))
            for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values():
                # shape channel -> shape
                connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None))
                # geometry (keys) -> shape channel
                connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None))
    
        # 'Skin' deformers (armature-to-geometry, only for meshes currently)...
        for arm, deformed_meshes in data_deformers_skin.items():
            for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
                # skin -> geometry
                mesh_key, _me, _free = data_meshes[ob_obj]
                assert(me == _me)
                connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
                for bo_obj, clstr_key in clusters.items():
                    # cluster -> skin
                    connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None))
                    # bone -> cluster
                    connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None))
    
        # Materials
        mesh_mat_indices = OrderedDict()
        _objs_indices = {}
        for mat, (mat_key, ob_objs) in data_materials.items():
            for ob_obj in ob_objs:
                connections.append((b"OO", get_fbx_uuid_from_key(mat_key), ob_obj.fbx_uuid, None))
                # Get index of this mat for this object (or dupliobject).
                # Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
                # Only mats for meshes currently...
                # Note in case of dupliobjects a same me/mat idx will be generated several times...
                # Should not be an issue in practice, and it's needed in case we export duplis but not the original!
                if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
                    continue
                _mesh_key, me, _free = data_meshes[ob_obj]
                idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
                mesh_mat_indices.setdefault(me, OrderedDict())[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, _ob_objs = data_materials[mat]
                for fbx_prop in fbx_mat_props:
                    # texture -> material properties
                    connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_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_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None))
    
        # Animations
        for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
            # Animstack itself is linked nowhere!
            astack_id = get_fbx_uuid_from_key(astack_key)
            # For now, only one layer!
            alayer_id = get_fbx_uuid_from_key(alayer_key)
            connections.append((b"OO", alayer_id, astack_id, None))
            for elem_key, (alayer_key, acurvenodes) in astack.items():
                elem_id = get_fbx_uuid_from_key(elem_key)
                # Animlayer -> animstack.
                # alayer_id = get_fbx_uuid_from_key(alayer_key)
                # connections.append((b"OO", alayer_id, astack_id, None))
                for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
                    # Animcurvenode -> animalayer.
                    acurvenode_id = get_fbx_uuid_from_key(acurvenode_key)
                    connections.append((b"OO", acurvenode_id, alayer_id, None))
                    # Animcurvenode -> object property.
                    connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
                    for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
                        if acurve:
                            # Animcurve -> Animcurvenode.
                            connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
    
        perfmon.level_down()
    
        # ##### And pack all this!
    
        return FBXExportData(
            templates, templates_users, connections,
            settings, scene, objects, animations, animated, frame_start, frame_end,
            data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
            data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
            data_world, data_materials, data_textures, data_videos,
        )
    
    
    def fbx_scene_data_cleanup(scene_data):
        """
        Some final cleanup...
        """
        # Delete temp meshes.
        for _key, me, free in scene_data.data_meshes.values():
            if free:
                bpy.data.meshes.remove(me)
    
    
    # ##### Top-level FBX elements generators. #####
    
    def fbx_header_elements(root, scene_data, time=None):
        """
        Write boiling code of FBX root.
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        app_vendor = "Blender Foundation"
        app_name = "Blender (stable FBX IO)"
        app_ver = bpy.app.version_string
        # ##### Start of FBXHeaderExtension element.
        header_ext = elem_empty(root, b"FBXHeaderExtension")
    
        elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
    
        elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
    
        # No encryption!
        elem_data_single_int32(header_ext, b"EncryptionType", 0)
    
        if time is None:
            time = datetime.datetime.now()
        elem = elem_empty(header_ext, b"CreationTimeStamp")
        elem_data_single_int32(elem, b"Version", 1000)
        elem_data_single_int32(elem, b"Year", time.year)
        elem_data_single_int32(elem, b"Month", time.month)
        elem_data_single_int32(elem, b"Day", time.day)
        elem_data_single_int32(elem, b"Hour", time.hour)
        elem_data_single_int32(elem, b"Minute", time.minute)
        elem_data_single_int32(elem, b"Second", time.second)
        elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
    
        elem_data_single_string_unicode(header_ext, b"Creator", "%s - %s" % (app_name, app_ver))
    
        # 'SceneInfo' seems mandatory to get a valid FBX file...
        # TODO use real values!
        # XXX Should we use scene.name.encode() here?
        scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
        scene_info.add_string(b"UserData")
        elem_data_single_string(scene_info, b"Type", b"UserData")
        elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
        meta_data = elem_empty(scene_info, b"MetaData")
        elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
        elem_data_single_string(meta_data, b"Title", b"")
        elem_data_single_string(meta_data, b"Subject", b"")
        elem_data_single_string(meta_data, b"Author", b"")
        elem_data_single_string(meta_data, b"Keywords", b"")
        elem_data_single_string(meta_data, b"Revision", b"")
        elem_data_single_string(meta_data, b"Comment", b"")
    
        props = elem_properties(scene_info)
        elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
        elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
        original = elem_props_compound(props, b"Original")
        original("p_string", b"ApplicationVendor", app_vendor)
        original("p_string", b"ApplicationName", app_name)
        original("p_string", b"ApplicationVersion", app_ver)
        original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
        original("p_string", b"FileName", "/foobar.fbx")
        lastsaved = elem_props_compound(props, b"LastSaved")
        lastsaved("p_string", b"ApplicationVendor", app_vendor)
        lastsaved("p_string", b"ApplicationName", app_name)
        lastsaved("p_string", b"ApplicationVersion", app_ver)
        lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
    
        # ##### End of FBXHeaderExtension element.
    
        # FileID is replaced by dummy value currently...
        elem_data_single_bytes(root, b"FileId", b"FooBar")
    
        # CreationTime is replaced by dummy value currently, but anyway...
        elem_data_single_string_unicode(root, b"CreationTime",
                                        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
                                        "".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
                                                  time.microsecond * 1000))
    
        elem_data_single_string_unicode(root, b"Creator", "%s - %s" % (app_name, app_ver))
    
        # ##### Start of GlobalSettings element.
        global_settings = elem_empty(root, b"GlobalSettings")
        scene = scene_data.scene
    
        elem_data_single_int32(global_settings, b"Version", 1000)
    
        props = elem_properties(global_settings)
        up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
        if scene_data.settings.apply_unit_scale:
            # Unit scaling is applied to objects' scale, so our unit is effectively FBX one (centimeter).
            scale_factor_org = 1.0
            scale_factor = scene_data.settings.global_scale / units_blender_to_fbx_factor(scene)
        else:
            scale_factor_org = units_blender_to_fbx_factor(scene)
            scale_factor = scene_data.settings.global_scale * units_blender_to_fbx_factor(scene)
        elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
        elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
        elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
        elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
        elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
        elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
        elem_props_set(props, "p_integer", b"OriginalUpAxis", -1)
        elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
        elem_props_set(props, "p_double", b"UnitScaleFactor", scale_factor)
        elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", scale_factor_org)
        elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
        elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
    
        # Global timing data.
        r = scene.render
        _, fbx_fps_mode = FBX_FRAMERATES[0]  # Custom framerate.
        fbx_fps = fps = r.fps / r.fps_base
        for ref_fps, fps_mode in FBX_FRAMERATES:
            if similar_values(fps, ref_fps):
                fbx_fps = ref_fps
                fbx_fps_mode = fps_mode
        elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
        elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
        elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
        elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
    
        # ##### End of GlobalSettings element.
    
    
    def fbx_documents_elements(root, scene_data):
        """
        Write 'Document' part of FBX root.
        Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        name = scene_data.scene.name
    
        # ##### Start of Documents element.
        docs = elem_empty(root, b"Documents")
    
        elem_data_single_int32(docs, b"Count", 1)
    
        doc_uid = get_fbx_uuid_from_key("__FBX_Document__" + name)
        doc = elem_data_single_int64(docs, b"Document", doc_uid)
        doc.add_string_unicode(name)
        doc.add_string_unicode(name)
    
        props = elem_properties(doc)
        elem_props_set(props, "p_object", b"SourceObject")
        elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
    
        # XXX Some kind of ID? Offset?
        #     Anyway, as long as we have only one doc, probably not an issue.
        elem_data_single_int64(doc, b"RootNode", 0)
    
    
    def fbx_references_elements(root, scene_data):
        """
        Have no idea what references are in FBX currently... Just writing empty element.
        """
        docs = elem_empty(root, b"References")
    
    
    def fbx_definitions_elements(root, scene_data):
        """
        Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
        """
        definitions = elem_empty(root, b"Definitions")
    
        elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
        elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
    
        fbx_templates_generate(definitions, scene_data.templates)
    
    
    def fbx_objects_elements(root, scene_data):
        """
        Data (objects, geometry, material, textures, armatures, etc.).
        """
        perfmon = PerfMon()
        perfmon.level_up()
        objects = elem_empty(root, b"Objects")
    
        perfmon.step("FBX export fetch empties (%d)..." % len(scene_data.data_empties))
    
        for empty in scene_data.data_empties:
            fbx_data_empty_elements(objects, empty, scene_data)
    
        perfmon.step("FBX export fetch lamps (%d)..." % len(scene_data.data_lamps))
    
        for lamp in scene_data.data_lamps:
            fbx_data_lamp_elements(objects, lamp, scene_data)
    
        perfmon.step("FBX export fetch cameras (%d)..." % len(scene_data.data_cameras))
    
        for cam in scene_data.data_cameras:
            fbx_data_camera_elements(objects, cam, scene_data)
    
        perfmon.step("FBX export fetch meshes (%d)..." % len(scene_data.data_meshes))
    
        done_meshes = set()
        for me_obj in scene_data.data_meshes:
            fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
        del done_meshes
    
        perfmon.step("FBX export fetch objects (%d)..." % len(scene_data.objects))
    
        for ob_obj in scene_data.objects:
            if ob_obj.is_dupli:
                continue
            fbx_data_object_elements(objects, ob_obj, scene_data)
            ob_obj.dupli_list_create(scene_data.scene, 'RENDER')
            for dp_obj in ob_obj.dupli_list:
                if dp_obj not in scene_data.objects:
                    continue
                fbx_data_object_elements(objects, dp_obj, scene_data)
            ob_obj.dupli_list_clear()
    
        perfmon.step("FBX export fetch remaining...")
    
        for ob_obj in scene_data.objects:
            if not (ob_obj.is_object and ob_obj.type == 'ARMATURE'):
                continue
            fbx_data_armature_elements(objects, ob_obj, scene_data)
    
        if scene_data.data_leaf_bones:
            fbx_data_leaf_bone_elements(objects, scene_data)
    
        for mat in scene_data.data_materials:
            fbx_data_material_elements(objects, mat, scene_data)
    
        for tex in scene_data.data_textures:
            fbx_data_texture_file_elements(objects, tex, scene_data)
    
        for vid in scene_data.data_videos:
            fbx_data_video_elements(objects, vid, scene_data)
    
        perfmon.step("FBX export fetch animations...")
        start_time = time.process_time()
    
        fbx_data_animation_elements(objects, scene_data)
    
        perfmon.level_down()
    
    
    def fbx_connections_elements(root, scene_data):
        """
        Relations between Objects (which material uses which texture, and so on).
        """
        connections = elem_empty(root, b"Connections")
    
        for c in scene_data.connections:
            elem_connection(connections, *c)
    
    
    def fbx_takes_elements(root, scene_data):
        """
        Animations.
        """
        # XXX Pretty sure takes are no more needed...
        takes = elem_empty(root, b"Takes")
        elem_data_single_string(takes, b"Current", b"")
    
        animations = scene_data.animations
        for astack_key, animations, alayer_key, name, f_start, f_end in animations:
            scene = scene_data.scene
            fps = scene.render.fps / scene.render.fps_base
            start_ktime = int(convert_sec_to_ktime(f_start / fps))
            end_ktime = int(convert_sec_to_ktime(f_end / fps))
    
            take = elem_data_single_string(takes, b"Take", name)
            elem_data_single_string(take, b"FileName", name + b".tak")
            take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
            take_loc_time.add_int64(end_ktime)
            take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
            take_ref_time.add_int64(end_ktime)
    
    
    # ##### "Main" functions. #####
    
    # This func can be called with just the filepath
    def save_single(operator, scene, filepath="",
                    global_matrix=Matrix(),
                    apply_unit_scale=False,
                    axis_up="Z",
                    axis_forward="Y",
                    context_objects=None,
                    object_types=None,
                    use_mesh_modifiers=True,
                    mesh_smooth_type='FACE',
                    use_armature_deform_only=False,
                    bake_anim=True,
                    bake_anim_use_all_bones=True,
                    bake_anim_use_nla_strips=True,
                    bake_anim_use_all_actions=True,
                    bake_anim_step=1.0,
                    bake_anim_simplify_factor=1.0,
                    add_leaf_bones=False,
                    primary_bone_axis='Y',
                    secondary_bone_axis='X',
                    use_metadata=True,
                    path_mode='AUTO',
                    use_mesh_edges=True,
                    use_tspace=True,
                    embed_textures=False,
                    use_custom_props=False,
                    bake_space_transform=False,
                    **kwargs
                    ):
    
        # Clear cached ObjectWrappers (just in case...).
        ObjectWrapper.cache_clear()
    
        if object_types is None:
            object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH', 'OTHER'}
    
        if 'OTHER' in object_types:
            object_types |= BLENDER_OTHER_OBJECT_TYPES
    
        if apply_unit_scale:
            global_matrix = global_matrix * Matrix.Scale(units_blender_to_fbx_factor(scene), 4)
        global_scale = global_matrix.median_scale
        global_matrix_inv = global_matrix.inverted()
        # For transforming mesh normals.
        global_matrix_inv_transposed = global_matrix_inv.transposed()
    
        # Only embed textures in COPY mode!
        if embed_textures and path_mode != 'COPY':
            embed_textures = False
    
        # Calcuate bone correction matrix
        bone_correction_matrix = None  # Default is None = no change
        bone_correction_matrix_inv = None
        if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
            from bpy_extras.io_utils import axis_conversion
            bone_correction_matrix = axis_conversion(from_forward=secondary_bone_axis,
                                                     from_up=primary_bone_axis,
                                                     to_forward='X',
                                                     to_up='Y',
                                                     ).to_4x4()
            bone_correction_matrix_inv = bone_correction_matrix.inverted()
    
    
        media_settings = FBXExportSettingsMedia(
            path_mode,
            os.path.dirname(bpy.data.filepath),  # base_src
            os.path.dirname(filepath),  # base_dst
            # Local dir where to put images (medias), using FBX conventions.
            os.path.splitext(os.path.basename(filepath))[0] + ".fbm",  # subdir
            embed_textures,
            set(),  # copy_set
            set(),  # embedded_set
        )
    
        settings = FBXExportSettings(
            operator.report, (axis_up, axis_forward), global_matrix, global_scale, apply_unit_scale,
            bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
            context_objects, object_types, use_mesh_modifiers,
            mesh_smooth_type, use_mesh_edges, use_tspace,
            use_armature_deform_only, add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
            bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions,
            bake_anim_step, bake_anim_simplify_factor,
            False, media_settings, use_custom_props,
        )
    
        import bpy_extras.io_utils
    
        print('\nFBX export starting... %r' % filepath)
        start_time = time.process_time()
    
        # Generate some data about exported scene...
        scene_data = fbx_data_from_scene(scene, settings)
    
        root = elem_empty(None, b"")  # Root element has no id, as it is not saved per se!
    
        # Mostly FBXHeaderExtension and GlobalSettings.
        fbx_header_elements(root, scene_data)
    
        # Documents and References are pretty much void currently.
        fbx_documents_elements(root, scene_data)
        fbx_references_elements(root, scene_data)
    
        # Templates definitions.
        fbx_definitions_elements(root, scene_data)
    
        # Actual data.
        fbx_objects_elements(root, scene_data)
    
        # How data are inter-connected.
        fbx_connections_elements(root, scene_data)
    
        # Animation.
        fbx_takes_elements(root, scene_data)
    
        # Cleanup!
        fbx_scene_data_cleanup(scene_data)
    
        # And we are down, we can write the whole thing!
        encode_bin.write(filepath, root, FBX_VERSION)
    
        # Clear cached ObjectWrappers!
        ObjectWrapper.cache_clear()
    
        # copy all collected files, if we did not embed them.
        if not media_settings.embed_textures:
            bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
    
        print('export finished in %.4f sec.' % (time.process_time() - start_time))
        return {'FINISHED'}
    
    
    # defaults for applications, currently only unity but could add others.
    def defaults_unity3d():
        return {
            # These options seem to produce the same result as the old Ascii exporter in Unity3D:
            "version": 'BIN7400',
            "axis_up": 'Y',
            "axis_forward": '-Z',
            "global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
            # Should really be True, but it can cause problems if a model is already in a scene or prefab
            # with the old transforms.
            "bake_space_transform": False,
    
            "use_selection": False,
    
            "object_types": {'ARMATURE', 'EMPTY', 'MESH', 'OTHER'},
            "use_mesh_modifiers": True,
            "use_mesh_edges": False,
            "mesh_smooth_type": 'FACE',
            "use_tspace": False,  # XXX Why? Unity is expected to support tspace import...
    
            "use_armature_deform_only": True,
    
            "use_custom_props": True,
    
            "bake_anim": True,
            "bake_anim_simplify_factor": 1.0,
            "bake_anim_step": 1.0,
            "bake_anim_use_nla_strips": True,
            "bake_anim_use_all_actions": True,
            "add_leaf_bones": False,  # Avoid memory/performance cost for something only useful for modelling
            "primary_bone_axis": 'Y',  # Doesn't really matter for Unity, so leave unchanged
            "secondary_bone_axis": 'X',
    
            "path_mode": 'AUTO',
            "embed_textures": False,
            "batch_mode": 'OFF',
        }
    
    
    def save(operator, context,
             filepath="",
             use_selection=False,
             batch_mode='OFF',
             use_batch_own_dir=False,
             **kwargs
             ):
        """
        This is a wrapper around save_single, which handles multi-scenes (or groups) cases, when batch-exporting a whole
        .blend file.
        """
    
        ret = None
    
        org_mode = None
        if context.active_object and context.active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
            org_mode = context.active_object.mode
            bpy.ops.object.mode_set(mode='OBJECT')
    
        if batch_mode == 'OFF':
            kwargs_mod = kwargs.copy()
            if use_selection:
                kwargs_mod["context_objects"] = context.selected_objects
            else:
                kwargs_mod["context_objects"] = context.scene.objects
    
            ret = save_single(operator, context.scene, filepath, **kwargs_mod)
        else:
            fbxpath = filepath
    
            prefix = os.path.basename(fbxpath)
            if prefix:
                fbxpath = os.path.dirname(fbxpath)
    
            if batch_mode == 'GROUP':
                data_seq = tuple(grp for grp in bpy.data.groups if grp.objects)
            else:
                data_seq = bpy.data.scenes
    
            # call this function within a loop with BATCH_ENABLE == False
            # no scene switching done at the moment.
            # orig_sce = context.scene
    
            new_fbxpath = fbxpath  # own dir option modifies, we need to keep an original
            for data in data_seq:  # scene or group
                newname = "_".join((prefix, bpy.path.clean_name(data.name))) if prefix else bpy.path.clean_name(data.name)
    
                if use_batch_own_dir:
                    new_fbxpath = os.path.join(fbxpath, newname)
                    # path may already exist
                    # TODO - might exist but be a file. unlikely but should probably account for it.
    
                    if not os.path.exists(new_fbxpath):
                        os.makedirs(new_fbxpath)
    
                filepath = os.path.join(new_fbxpath, newname + '.fbx')
    
                print('\nBatch exporting %s as...\n\t%r' % (data, filepath))
    
                if batch_mode == 'GROUP':  # group
                    # group, so objects update properly, add a dummy scene.
                    scene = bpy.data.scenes.new(name="FBX_Temp")
                    scene.layers = [True] * 20
                    # bpy.data.scenes.active = scene # XXX, cant switch
                    src_scenes = {}  # Count how much each 'source' scenes are used.
                    for ob_base in data.objects:
                        for src_sce in ob_base.users_scene:
                            if src_sce not in src_scenes:
                                src_scenes[src_sce] = 0
                            src_scenes[src_sce] += 1
                        scene.objects.link(ob_base)
    
                    # Find the 'most used' source scene, and use its unit settings. This is somewhat weak, but should work
                    # fine in most cases, and avoids stupid issues like T41931.
                    best_src_scene = None
                    best_src_scene_users = 0
                    for sce, nbr_users in src_scenes.items():
                        if (nbr_users) > best_src_scene_users:
                            best_src_scene_users = nbr_users
                            best_src_scene = sce
                    scene.unit_settings.system = best_src_scene.unit_settings.system
                    scene.unit_settings.system_rotation = best_src_scene.unit_settings.system_rotation
                    scene.unit_settings.scale_length = best_src_scene.unit_settings.scale_length
    
                    scene.update()
                    # TODO - BUMMER! Armatures not in the group wont animate the mesh
                else:
                    scene = data
    
                kwargs_batch = kwargs.copy()
                kwargs_batch["context_objects"] = data.objects
    
                save_single(operator, scene, filepath, **kwargs_batch)
    
                if batch_mode == 'GROUP':
                    # remove temp group scene
                    bpy.data.scenes.remove(scene)
    
            # no active scene changing!
            # bpy.data.scenes.active = orig_sce
    
            ret = {'FINISHED'}  # so the script wont run after we have batch exported.
    
        if context.active_object and org_mode and bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set(mode=org_mode)
    
        return ret