Skip to content
Snippets Groups Projects
export_fbx_bin.py 123 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### 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
    
    import collections
    from collections import namedtuple, OrderedDict
    import itertools
    from itertools import zip_longest, chain
    
    import bpy
    import bpy_extras
    
    from bpy.types import Object, Bone
    
    from mathutils import Vector, Matrix
    
    from . import encode_bin, data_types
    
    
    # "Constants"
    FBX_VERSION = 7400
    FBX_HEADER_VERSION = 1003
    FBX_SCENEINFO_VERSION = 100
    FBX_TEMPLATES_VERSION = 100
    
    FBX_MODELS_VERSION = 232
    
    FBX_GEOMETRY_VERSION = 124
    FBX_GEOMETRY_NORMAL_VERSION = 102
    FBX_GEOMETRY_BINORMAL_VERSION = 102
    FBX_GEOMETRY_TANGENT_VERSION = 102
    FBX_GEOMETRY_SMOOTHING_VERSION = 102
    FBX_GEOMETRY_VCOLOR_VERSION = 101
    FBX_GEOMETRY_UV_VERSION = 101
    FBX_GEOMETRY_MATERIAL_VERSION = 101
    FBX_GEOMETRY_LAYER_VERSION = 100
    FBX_POSE_BIND_VERSION = 100
    FBX_DEFORMER_SKIN_VERSION = 101
    FBX_DEFORMER_CLUSTER_VERSION = 100
    FBX_MATERIAL_VERSION = 102
    FBX_TEXTURE_VERSION = 202
    
    FBX_ANIM_KEY_VERSION = 4008
    
    
    FBX_NAME_CLASS_SEP = b"\x00\x01"
    
    FBX_KTIME = 46186158000  # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
    
    
    MAT_CONVERT_LAMP = Matrix.Rotation(math.pi / 2.0, 4, 'X')  # Blender is -Z, FBX is -Y.
    MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y')  # Blender is -Z, FBX is +X.
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    #MAT_CONVERT_BONE = Matrix.Rotation(math.pi / -2.0, 4, 'X')  # Blender is +Y, FBX is +Z.
    MAT_CONVERT_BONE = Matrix()
    
    
    
    # Lamps.
    FBX_LIGHT_TYPES = {
        'POINT': 0,  # Point.
        'SUN': 1,    # Directional.
        'SPOT': 2,   # Spot.
        'HEMI': 1,   # Directional.
        'AREA': 3,   # Area.
    }
    FBX_LIGHT_DECAY_TYPES = {
        'CONSTANT': 0,                   # None.
        'INVERSE_LINEAR': 1,             # Linear.
        'INVERSE_SQUARE': 2,             # Quadratic.
        'CUSTOM_CURVE': 2,               # Quadratic.
        'LINEAR_QUADRATIC_WEIGHTED': 2,  # Quadratic.
    }
    
    
    ##### Misc utilities #####
    
    # Note: this could be in a utility (math.units e.g.)...
    
    UNITS = {
        "meter": 1.0,  # Ref unit!
        "kilometer": 0.001,
        "millimeter": 1000.0,
        "foot": 1.0 / 0.3048,
        "inch": 1.0 / 0.0254,
        "turn": 1.0,  # Ref unit!
        "degree": 360.0,
        "radian": math.pi * 2.0,
        "second": 1.0,  # Ref unit!
        "ktime": FBX_KTIME,
    }
    
    
    def units_convert(val, u_from, u_to):
        """Convert value."""
        conv = UNITS[u_to] / UNITS[u_from]
    
        return val * conv
    
    
    def units_convert_iter(it, u_from, u_to):
        """Convert value."""
        conv = UNITS[u_to] / UNITS[u_from]
        return (v * conv for v in it)
    
    
    
    def matrix_to_array(mat):
        """Concatenate matrix's columns into a single, flat tuple"""
        # blender matrix is row major, fbx is col major so transpose on write
        return tuple(f for v in mat.transposed() for f in v)
    
    
    RIGHT_HAND_AXES = {
        # Up, Front -> FBX values (tuples of (axis, sign), Up, Front, Coord).
        # Note: Since we always stay in right-handed system, third coord sign is always positive!
        ('X',  'Y'):  ((0, 1),  (1, 1),  (2, 1)),
        ('X',  '-Y'): ((0, 1),  (1, -1), (2, 1)),
        ('X',  'Z'):  ((0, 1),  (2, 1),  (1, 1)),
        ('X',  '-Z'): ((0, 1),  (2, -1), (1, 1)),
        ('-X', 'Y'):  ((0, -1), (1, 1),  (2, 1)),
        ('-X', '-Y'): ((0, -1), (1, -1), (2, 1)),
        ('-X', 'Z'):  ((0, -1), (2, 1),  (1, 1)),
        ('-X', '-Z'): ((0, -1), (2, -1), (1, 1)),
        ('Y',  'X'):  ((1, 1),  (0, 1),  (2, 1)),
        ('Y',  '-X'): ((1, 1),  (0, -1), (2, 1)),
        ('Y',  'Z'):  ((1, 1),  (2, 1),  (0, 1)),
        ('Y',  '-Z'): ((1, 1),  (2, -1), (0, 1)),
        ('-Y', 'X'):  ((1, -1), (0, 1),  (2, 1)),
        ('-Y', '-X'): ((1, -1), (0, -1), (2, 1)),
        ('-Y', 'Z'):  ((1, -1), (2, 1),  (0, 1)),
        ('-Y', '-Z'): ((1, -1), (2, -1), (0, 1)),
        ('Z',  'X'):  ((2, 1),  (0, 1),  (1, 1)),
        ('Z',  '-X'): ((2, 1),  (0, -1), (1, 1)),
        ('Z',  'Y'):  ((2, 1),  (1, 1),  (0, 1)),  # Blender system!
        ('Z',  '-Y'): ((2, 1),  (1, -1), (0, 1)),
        ('-Z', 'X'):  ((2, -1), (0, 1),  (1, 1)),
        ('-Z', '-X'): ((2, -1), (0, -1), (1, 1)),
        ('-Z', 'Y'):  ((2, -1), (1, 1),  (0, 1)),
        ('-Z', '-Y'): ((2, -1), (1, -1), (0, 1)),
    }
    
    
    ##### UIDs code. #####
    
    # ID class (mere int).
    class UID(int):
        pass
    
    
    # UIDs storage.
    _keys_to_uids = {}
    _uids_to_keys = {}
    
    
    def _key_to_uid(uids, key):
        # TODO: Check this is robust enough for our needs!
        # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
        #       As int64 is signed in FBX, we keep uids below 2**63...
        if isinstance(key, int) and 0 <= key < 2**63:
            # We can use value directly as id!
            uid = key
        else:
            uid = hash(key)
            if uid < 0:
                uid = -uid
            if uid >= 2**63:
                uid //= 2
    
        # Try to make our uid shorter!
        if uid > int(1e9):
            t_uid = uid % int(1e9)
            if t_uid not in uids:
                uid = t_uid
    
        # Make sure our uid *is* unique.
        if uid in uids:
            inc = 1 if uid < 2**62 else -1
            while uid in uids:
                uid += inc
                if 0 > uid >= 2**63:
                    # Note that this is more that unlikely, but does not harm anyway...
                    raise ValueError("Unable to generate an UID for key {}".format(key))
        return UID(uid)
    
    
    def get_fbxuid_from_key(key):
        """
        Return an UID for given key, which is assumed hasable.
        """
        uid = _keys_to_uids.get(key, None)
        if uid is None:
            uid = _key_to_uid(_uids_to_keys, key)
            _keys_to_uids[key] = uid
            _uids_to_keys[uid] = key
        return uid
    
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    # XXX Not sure we'll actually need this one?
    
    def get_key_from_fbxuid(uid):
        """
        Return the key which generated this uid.
        """
        assert(uid.__class__ == UID)
        return _uids_to_keys.get(uid, None)
    
    
    # Blender-specific key generators
    def get_blenderID_key(bid):
        return "B" + bid.rna_type.name + "::" + bid.name
    
    
    def get_blender_bone_key(armature, bone):
        """Return bone's keys (Model and NodeAttribute)."""
        key = "|".join((get_blenderID_key(armature), get_blenderID_key(bone)))
        return key, key + "_Data"
    
    
    def get_blender_armature_bindpose_key(armature, mesh):
        """Return armature's bindpose key."""
        return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "BindPose"))
    
    
    def get_blender_armature_skin_key(armature, mesh):
        """Return armature's skin key."""
        return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin"))
    
    
    def get_blender_bone_cluster_key(armature, mesh, bone):
        """Return bone's cluster key."""
        return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh),
                         get_blenderID_key(bone), "SubDeformerCluster"))
    
    
    
    def get_blender_anim_stack_key(scene):
        """Return single anim stack key."""
        return "|".join((get_blenderID_key(scene), "AnimStack"))
    
    
    def get_blender_anim_layer_key(ID):
        """Return ID's anim layer key."""
        return "|".join((get_blenderID_key(ID), "AnimLayer"))
    
    
    def get_blender_anim_curve_node_key(ID, fbx_prop_name):
        """Return (ID, fbxprop) curve node key."""
        return "|".join((get_blenderID_key(ID), fbx_prop_name, "AnimCurveNode"))
    
    
    def get_blender_anim_curve_key(ID, fbx_prop_name, fbx_prop_item_name):
        """Return (ID, fbxprop, item) curve key."""
        return "|".join((get_blenderID_key(ID), fbx_prop_name, fbx_prop_item_name, "AnimCurve"))
    
    
    
    ##### Element generators. #####
    
    # Note: elem may be None, in this case the element is not added to any parent.
    def elem_empty(elem, name):
        sub_elem = encode_bin.FBXElem(name)
        if elem is not None:
            elem.elems.append(sub_elem)
        return sub_elem
    
    
    def elem_properties(elem):
        return elem_empty(elem, b"Properties70")
    
    
    def _elem_data_single(elem, name, value, func_name):
        sub_elem = elem_empty(elem, name)
        getattr(sub_elem, func_name)(value)
        return sub_elem
    
    
    def _elem_data_vec(elem, name, value, func_name):
        sub_elem = elem_empty(elem, name)
        func = getattr(sub_elem, func_name)
        for v in value:
            func(v)
        return sub_elem
    
    
    def elem_data_single_bool(elem, name, value):
        return _elem_data_single(elem, name, value, "add_bool")
    
    
    def elem_data_single_int16(elem, name, value):
        return _elem_data_single(elem, name, value, "add_int16")
    
    
    def elem_data_single_int32(elem, name, value):
        return _elem_data_single(elem, name, value, "add_int32")
    
    
    def elem_data_single_int64(elem, name, value):
        return _elem_data_single(elem, name, value, "add_int64")
    
    
    def elem_data_single_float32(elem, name, value):
        return _elem_data_single(elem, name, value, "add_float32")
    
    
    def elem_data_single_float64(elem, name, value):
        return _elem_data_single(elem, name, value, "add_float64")
    
    
    def elem_data_single_bytes(elem, name, value):
        return _elem_data_single(elem, name, value, "add_bytes")
    
    
    def elem_data_single_string(elem, name, value):
        return _elem_data_single(elem, name, value, "add_string")
    
    
    def elem_data_single_string_unicode(elem, name, value):
        return _elem_data_single(elem, name, value, "add_string_unicode")
    
    
    def elem_data_single_bool_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_bool_array")
    
    
    def elem_data_single_int32_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_int32_array")
    
    
    def elem_data_single_int64_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_int64_array")
    
    
    def elem_data_single_float32_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_float32_array")
    
    
    def elem_data_single_float64_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_float64_array")
    
    
    def elem_data_single_byte_array(elem, name, value):
        return _elem_data_single(elem, name, value, "add_byte_array")
    
    
    def elem_data_vec_float64(elem, name, value):
        return _elem_data_vec(elem, name, value, "add_float64")
    
    ##### Generators for standard FBXProperties70 properties. #####
    
    
    # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
    
    # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
    
    #     Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
    #     these are just Vector3D ultimately... *sigh* (again).
    
    FBX_PROPERTIES_DEFINITIONS = {
    
        "p_bool": (b"bool", b"", "add_int32"),  # Yes, int32 for a bool (and they do have a core bool type)!!!
        "p_integer": (b"int", b"Integer", "add_int32"),
    
        "p_ulonglong": (b"ULongLong", b"", "add_int64"),
    
        "p_double": (b"double", b"Number", "add_float64"),  # Non-animatable?
        "p_number": (b"Number", b"", "add_float64"),  # Animatable-only?
    
        "p_enum": (b"enum", b"", "add_int32"),
    
        "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"),  # Non-animatable?
        "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"),  # Animatable-only?
        "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"),  # Non-animatable?
        "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"),  # Animatable-only?
    
        "p_string": (b"KString", b"", "add_string_unicode"),
        "p_string_url": (b"KString", b"Url", "add_string_unicode"),
        "p_timestamp": (b"KTime", b"Time", "add_int64"),
        "p_datetime": (b"DateTime", b"", "add_string_unicode"),
    
        # Special types.
        "p_object": (b"object", b""),  # XXX Check this! No value for this prop??? Would really like to know how it works!
    
        "p_compound": (b"Compound", b""),
    
        # Specific types (sic).
        ## Objects (Models).
        "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"),
        "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"),
        "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"),
        "p_visibility": (b"Visibility", b"", "add_float64"),
        "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"),
        ## Cameras!!!
        "p_roll": (b"Roll", b"", "add_float64"),
        "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"),
        "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"),
        "p_fov": (b"FieldOfView", b"", "add_float64"),
        "p_fov_x": (b"FieldOfViewX", b"", "add_float64"),
        "p_fov_y": (b"FieldOfViewY", b"", "add_float64"),
    
    def _elem_props_set(elem, ptype, name, value, flags):
    
        p = elem_data_single_string(elem, b"P", name)
    
        for t in ptype[:2]:
    
        p.add_string(flags)
        if len(ptype) == 3:
            getattr(p, ptype[2])(value)
        elif len(ptype) > 3:
    
            # We assume value is iterable, else it's a bug!
    
            for callback, val in zip(ptype[2:], value):
    
    def _elem_props_flags(animatable, custom):
        if animatable and custom:
            return b"AU"
        elif animatable:
            return b"A"
        elif custom:
            return b"U"
        return b""
    
    
    def elem_props_set(elem, ptype, name, value=None, animatable=False, custom=False):
    
        ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
    
        _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, custom))
    
    def elem_props_compound(elem, cmpd_name, custom=False):
        def _setter(ptype, name, value, animatable=False, custom=False):
    
            name = cmpd_name + b"|" + name
    
            elem_props_set(elem, ptype, name, value, animatable=animatable, custom=custom)
    
        elem_props_set(elem, "p_compound", cmpd_name, custom=custom)
    
    def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False):
    
        """
        Only add a prop if the same value is not already defined in given template.
        Note it is important to not give iterators as value, here!
        """
        ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
    
        tmpl_val, tmpl_ptype, tmpl_animatable = template.properties.get(name, (None, None, False))
        # Note animatable flag from template takes precedence over given one, if applicable.
    
        if tmpl_ptype is not None:
    
            if ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
                    (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (tuple(value), ptype_name))):
    
                return  # Already in template and same value.
    
            _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, False))
        else:
            _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, 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)
    
    
    ##### Templates #####
    # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
    
    FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users"))
    
    
    def fbx_templates_generate(root, fbx_templates):
        # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
        # for Lights, Cameras, LibNodes, etc.).
        templates = OrderedDict()
        for type_name, prop_type_name, properties, nbr_users in fbx_templates.values():
            if type_name not in templates:
                templates[type_name] = [OrderedDict(((prop_type_name, properties),)), nbr_users]
            else:
                templates[type_name][0][prop_type_name] = properties
                templates[type_name][1] += nbr_users
    
        for type_name, (subprops, nbr_users) in templates.items():
            template = elem_data_single_string(root, b"ObjectType", type_name)
            elem_data_single_int32(template, b"Count", nbr_users)
    
            for prop_type_name, properties in subprops.items():
                if prop_type_name and properties:
                    elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name)
                    props = elem_properties(elem)
    
                    for name, (value, ptype, animatable) in properties.items():
                        elem_props_set(props, ptype, name, value, animatable=animatable)
    
    
    
    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)
    
    
    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", (1, "p_enum", False)),  # RSrs
            (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", (0, "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)
    
    
    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)
    
    
    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.8, 0.8, 0.8), "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", (2, "p_enum", False)),  # Fixed ratio, height and width in pixels.
            (b"AspectWidth", (float(r.resolution_x), "p_double", False)),
            (b"AspectHeight", (float(r.resolution_y), "p_double", False)),
            (b"PixelAspectRatio", (float(r.pixel_aspect_x / r.pixel_aspect_y), "p_double", False)),
            (b"FilmOffsetX", (0.0, "p_number", True)),
            (b"FilmOffsetY", (0.0, "p_number", True)),
            (b"FilmWidth", (1.2598425196850394, "p_double", False)),
            (b"FilmHeight", (0.7086614173228346, "p_double", False)),
            (b"FilmAspectRatio", (1.777777777777778, "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", (3, "p_enum", False)),  # 3 = focal length.
            (b"GateFit", (0, "p_enum", False)),  # 0 = no resolution gate fit.
            (b"FieldOfView", (49.13434207760448, "p_fov", True)),
            (b"FieldOfViewX", (49.13434207760448, "p_fov_x", True)),
            (b"FieldOfViewY", (28.841546110078532, "p_fov_y", True)),
            (b"FocalLength", (35.0, "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", (True, "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", (1.0, "p_double", False)),
            (b"FarPlane", (100.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", (100.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", (1.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)),
            (b"BackPlateFitImage", (False, "p_bool", False)),
            (b"BackPlateCrop", (False, "p_bool", False)),
            (b"BackPlateCenter", (True, "p_bool", False)),
            (b"BackPlateKeepRatio", (True, "p_bool", False)),
            (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", (True, "p_bool", False)),
            (b"DisplaySafeAreaOnRender", (False, "p_bool", False)),
            (b"SafeAreaDisplayStyle", (1, "p_enum", False)),  # 1 = rounded corners.
            (b"SafeAreaAspectRatio", (1.777777777777778, "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", (1, "p_enum", False)),  # 0 = camera interest, 1 = distance from camera interest.
            (b"FocusAngle", (3.5, "p_double", False)),  # ???
            (b"FocusDistance", (10.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)
    
    
    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)
    
    
    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)
    
    
    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.8, 0.8, 0.8), "p_color", True)),  # Same as diffuse.
    
            (b"EmissiveFactor", (0.0, "p_number", True)),
    
            (b"AmbientColor", ((0.0, 0.0, 0.0), "p_color", True)),
    
            (b"AmbientFactor", (1.0, "p_number", True)),
    
            (b"DiffuseColor", ((0.8, 0.8, 0.8), "p_color", True)),
    
            (b"DiffuseFactor", (0.8, "p_number", True)),
    
            (b"TransparentColor", ((0.8, 0.8, 0.8), "p_color", True)),  # Same as diffuse.
    
            (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", (0.0, "p_double", False)),
    
            (b"VectorDisplacementColor", ((0.0, 0.0, 0.0), "p_color_rgb", False)),
            (b"VectorDisplacementFactor", (0.0, "p_double", False)),
    
            (b"SpecularColor", ((1.0, 1.0, 1.0), "p_color", True)),
    
            (b"SpecularFactor", (0.5 / 2.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", ((50.0 - 1.0) / 5.10, "p_number", True)),
            (b"ShininessExponent", ((50.0 - 1.0) / 5.10, "p_number", True)),
    
            (b"ReflectionColor", ((1.0, 1.0, 1.0), "p_color", True)),
    
            (b"ReflectionFactor", (0.0, "p_number", True)),
        ))
    
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Material", b"FbxSurfacePhong", props, nbr_users)
    
    
    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", (False, "p_bool", False)),
            (b"CurrentTextureBlendMode", (0, "p_enum", False)),  # Translucent, assuming this means "Alpha over"!
            (b"CurrentMappingType", (1, "p_enum", False)),  # Planar.
            (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... At least, UseMaterial should always be ON imho.
    
            (b"UseMaterial", (True, "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)
    
    
    def fbx_template_def_video(scene, settings, override_defaults=None, nbr_users=0):
        # WIP...
    
        props = OrderedDict((
    
            (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).
    
            (b"StartFrame", (0, "p_integer", False)),
            (b"StopFrame", (0, "p_integer", False)),
            (b"Offset", (0, "p_timestamp", 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", (scene.render.fps / scene.render.fps_base, "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)
    
    
    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)
    
    
    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)
    
    
    
    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)
    
    
    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)
    
    
    def fbx_template_def_animcurvenode(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict((
            (b"d", (None, "p_compound", False)),
        ))
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"AnimationCurveNode", b"FbxAnimCurveNode", props, nbr_users)
    
    
    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)
    
    
    
    ##### FBX objects generators. #####
    
    def has_valid_parent(scene_data, obj):
        return obj.parent and obj.parent in scene_data.objects
    
    
    def use_bake_space_transform(scene_data, obj):
        # NOTE: Only applies to object types supporting this!!! Currently, only meshes...
        #       Also, do not apply it to children objects.
        # TODO: Check whether this can work for bones too...
    
        return (scene_data.settings.bake_space_transform and not isinstance(obj, Bone) and
                obj.type in {'MESH'} and not has_valid_parent(scene_data, obj))
    
    def fbx_object_matrix(scene_data, obj, armature=None, local_space=False, global_space=False):
    
        Generate object transform matrix (*always* in matching *FBX* space!).
        If local_space is True, returned matrix is *always* in local space.
        Else:
            If global_space is True, returned matrix is always in world space.
            If global_space is False, returned matrix is in parent space if parent is valid, else in world space.
        Note local_space has precedence over global_space.
    
        If obj is a bone, and global_space is True, armature must be provided (it's the bone's armature object!).
        Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX).
        """
    
        is_bone = isinstance(obj, Bone)
    
        # Objects which are not bones and do not have any parent are *always* in global space (unless local_space is True!).
        is_global = not local_space and (global_space or not (is_bone or has_valid_parent(scene_data, obj)))
    
    
        #assert((is_bone and is_global and armature is None) == False,
               #"You must provide an armature object to get bones transform matrix in global space!")
    
        matrix = obj.matrix_local
    
        # Lamps, cameras and bones need to be rotated (in local space!).
        if is_bone:
            matrix = matrix * MAT_CONVERT_BONE
        elif obj.type == 'LAMP':
            matrix = matrix * MAT_CONVERT_LAMP
        elif obj.type == 'CAMERA':
            matrix = matrix * MAT_CONVERT_CAMERA
    
        # Up till here, our matrix is in local space, time to bring it in its final desired space.
        if is_bone:
            # Bones are in armature (object) space currently, either bring them to global space or real
            # local space (relative to parent bone).
            if is_global:
    
                matrix = armature.matrix_world * matrix
    
            elif obj.parent:  # Parent bone, get matrix relative to it.
                par_matrix = obj.parent.matrix_local * MAT_CONVERT_BONE
                matrix = par_matrix.inverted() * matrix
    
        elif obj.parent:
            if is_global:
                # Move matrix to global Blender space.
    
                matrix = obj.parent.matrix_world * matrix
    
            elif use_bake_space_transform(scene_data, obj.parent):
                # Blender's and FBX's local space of parent may differ if we use bake_space_transform...
                # Apply parent's *Blender* local space...
                matrix = obj.parent.matrix_local * matrix
                # ...and move it back into parent's *FBX* local space.
    
                par_mat = fbx_object_matrix(scene_data, obj.parent, local_space=True)
    
                matrix = par_mat.inverted() * matrix
    
        if use_bake_space_transform(scene_data, obj):
            # If we bake the transforms we need to post-multiply inverse global transform.
            # This means that the global transform will not apply to children of this transform.
            matrix = matrix * scene_data.settings.global_matrix_inv
        if is_global:
            # In any case, pre-multiply the global matrix to get it in FBX global space!
    
            matrix = scene_data.settings.global_matrix * matrix
    
        return matrix
    
    
    
    def fbx_object_tx(scene_data, obj):
    
        """
        Generate object transform data (always in local space when possible).
        """
    
        matrix = fbx_object_matrix(scene_data, obj)
    
        loc, rot, scale = matrix.decompose()
        matrix_rot = rot.to_matrix()
        rot = rot.to_euler()  # quat -> euler, we always use 'XYZ' order.
    
        return loc, rot, scale, matrix, matrix_rot
    
    
    def fbx_name_class(name, cls):
        return FBX_NAME_CLASS_SEP.join((name, cls))
    
    
    
    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():
            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)
    
            if isinstance(v, float):
    
                elem_props_set(props, "p_double", k.encode(), v, custom=True)
    
    
    
    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']