Skip to content
Snippets Groups Projects
export_fbx_bin.py 133 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, PoseBone
    
    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
    
    # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
    # currently, apart from some AD products.
    FBX_GEOMETRY_NORMAL_VERSION = 101
    FBX_GEOMETRY_BINORMAL_VERSION = 101
    FBX_GEOMETRY_TANGENT_VERSION = 101
    
    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()
    
    BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
    BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
    
    
    
    # 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_empty_key(obj):
        """Return bone's keys (Model and NodeAttribute)."""
        return "|".join((get_blenderID_key(obj), "Empty"))
    
    
    
    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"))
    
    
    
        """Return single anim stack key."""
    
        if ID:
            return "|".join((get_blenderID_key(scene), get_blenderID_key(ID), "AnimStack"))
        else:
            return "|".join((get_blenderID_key(scene), "AnimStack"))
    
        """Return ID's anim layer key."""
    
        if ID:
            return "|".join((get_blenderID_key(scene), get_blenderID_key(ID), "AnimLayer"))
        else:
            return "|".join((get_blenderID_key(scene), "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_init(templates, template_type):
        """
        Init a writing template of given type, for *one* element's properties.
        """
        ret = None
        if template_type in templates:
            tmpl = templates[template_type]
            written = tmpl.written[0]
            props = tmpl.properties
            ret = OrderedDict((name, [val, ptype, anim, written]) for name, (val, ptype, anim) in props.items())
        return ret or OrderedDict()
    
    
    
    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]
    
        if len(ptype) > 3:
            value = tuple(value)
        tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False))
    
        # Note animatable flag from template takes precedence over given one, if applicable.
    
        if tmpl_ptype is not None:
    
            if (tmpl_written and
                ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
                 (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (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))
    
    def elem_props_template_finalize(template, elem):
        """
        Finalize one element's template/props.
        Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
        but values for only *one* subtype can be written as template. So we have to be sure we write those for ths other
        subtypes in each and every elements, if they are not overriden by that element.
        Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
        """
        for name, (value, ptype_name, animatable, written) in template.items():
            if written:
                continue
            ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
            _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", "written"))
    
    
    
    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.).
    
        ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()}
    
        templates = OrderedDict()
    
        for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values():
    
            if type_name not in templates:
    
                templates[type_name] = [OrderedDict(((prop_type_name, (properties, nbr_users)),)), nbr_users]
    
                templates[type_name][0][prop_type_name] = (properties, nbr_users)
    
                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)
    
    
            if len(subprops) == 1:
                prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items()))
                subprops = (prop_type_name, properties)
                ref_templates[(type_name, prop_type_name)].written[0] = True
            else:
                # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
                max_users = max_props = -1
                written_prop_type_name = None
                for prop_type_name, (properties, nbr_sub_type_users) in subprops.items():
                    if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props):
                        max_users = nbr_sub_type_users
                        max_props = len(properties)
                        written_prop_type_name = prop_type_name
                subprops = (written_prop_type_name, properties)
                ref_templates[(type_name, written_prop_type_name)].written[0] = True
    
            prop_type_name, properties = subprops
            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, [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", (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, [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.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, [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.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, [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", (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, [False])
    
    
    
    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, [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((
            (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, [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])
    
    ##### FBX objects generators. #####
    
    def has_valid_parent(scene_data, obj):
    
        if isinstance(obj, PoseBone):
            obj = obj.bone
    
        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, (PoseBone, Bone)) and
    
                obj.type in BLENDER_OBJECT_TYPES_MESHLIKE 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 both local_space and global_space are 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_posebone = isinstance(obj, PoseBone)
        is_bone = is_posebone or 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)))
    
    
        # Up till here, our matrix is in local space, time to bring it in its final desired space.
        if is_bone: