Skip to content
Snippets Groups Projects
export_fbx_bin.py 113 KiB
Newer Older
# ##### 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
    # 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_enum": (b"enum", b"", "add_int32"),
    "p_number": (b"double", b"Number", "add_float64"),
    "p_visibility": (b"Visibility", 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"),
    "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"),
    "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_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"),
    "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"),
    "p_object": (b"object", b""),  # XXX Check this! No value for this prop???
    "p_compound": (b"Compound", b""),
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", (False, "p_bool", False)),
        (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_number", False)),
        (b"RotationStiffnessY", (0.0, "p_number", False)),
        (b"RotationStiffnessZ", (0.0, "p_number", False)),
        (b"AxisLen", (10.0, "p_number", 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", (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d", False)),
        (b"ScalingMax", (Vector((1.0, 1.0, 1.0)) * gscale, "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", (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d", False)),
        (b"MinDampRangeX", (0.0, "p_number", False)),
        (b"MinDampRangeY", (0.0, "p_number", False)),
        (b"MinDampRangeZ", (0.0, "p_number", False)),
        (b"MaxDampRangeX", (0.0, "p_number", False)),
        (b"MaxDampRangeY", (0.0, "p_number", False)),
        (b"MaxDampRangeZ", (0.0, "p_number", False)),
        (b"MinDampStrengthX", (0.0, "p_number", False)),
        (b"MinDampStrengthY", (0.0, "p_number", False)),
        (b"MinDampStrengthZ", (0.0, "p_number", False)),
        (b"MaxDampStrengthX", (0.0, "p_number", False)),
        (b"MaxDampStrengthY", (0.0, "p_number", False)),
        (b"MaxDampStrengthZ", (0.0, "p_number", False)),
        (b"PreferedAngleX", (0.0, "p_number", False)),
        (b"PreferedAngleY", (0.0, "p_number", False)),
        (b"PreferedAngleZ", (0.0, "p_number", 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", (Vector((1.0, 1.0, 1.0)) * gscale, "p_lcl_scaling", True)),
        (b"Visibility", (1.0, "p_visibility", True)),
    ))
    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_rgb", 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_number", False)),
        (b"CastShadows", (True, "p_bool", False)),
        (b"ShadowColor", ((0.0, 0.0, 0.0), "p_color_rgb", 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):
    props = OrderedDict()  # TODO!!!
    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_rgb", True)),  # Same as diffuse.
        (b"EmissiveFactor", (0.0, "p_number", True)),
        (b"AmbientColor", ((0.0, 0.0, 0.0), "p_color_rgb", True)),
        (b"AmbientFactor", (1.0, "p_number", True)),
        (b"DiffuseColor", ((0.8, 0.8, 0.8), "p_color_rgb", True)),
        (b"DiffuseFactor", (0.8, "p_number", True)),
        (b"TransparentColor", ((0.8, 0.8, 0.8), "p_color_rgb", 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_number", False)),
        (b"DisplacementColor", ((0.0, 0.0, 0.0), "p_color_rgb", False)),
        (b"DisplacementFactor", (0.0, "p_number", False)),
        (b"SpecularColor", ((1.0, 1.0, 1.0), "p_color_rgb", 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_rgb", 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_number", 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"PlaySpeed", (1.0, "p_number", 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_number", 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", True)),
        (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_number", 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']
    do_shadow = False
    shadow_color = Vector((0.0, 0.0, 0.0))
    if lamp.type not in {'HEMI'}:
        if lamp.type not in {'SUN'}:
            decay_type = FBX_LIGHT_DECAY_TYPES[lamp.falloff_type]
        do_light = (not lamp.use_only_shadow) and (lamp.use_specular or lamp.use_diffuse)
        do_shadow = lamp.shadow_method not in {'NOSHADOW'}
        shadow_color = lamp.shadow_color

    light = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(lamp_key))
    light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
    light.add_string(b"Light")

    elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION)  # Sic...

    tmpl = scene_data.templates[b"Light"]
    props = elem_properties(light)
    elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
    elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
    elem_props_template_set(tmpl, props, "p_color_rgb", b"Color", lamp.color)
    elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
    elem_props_template_set(tmpl, props, "p_enum", b"DecayType", decay_type)
    elem_props_template_set(tmpl, props, "p_number", b"DecayStart", lamp.distance * gscale)
    elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
    elem_props_template_set(tmpl, props, "p_color_rgb", b"ShadowColor", shadow_color)
    if lamp.type in {'SPOT'}:
        elem_props_template_set(tmpl, props, "p_number", b"OuterAngle", math.degrees(lamp.spot_size))
        elem_props_template_set(tmpl, props, "p_number", b"InnerAngle",
                                math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend)))

    # Custom properties.
    if scene_data.settings.use_custom_properties:
        fbx_data_element_custom_properties(props, lamp)


def fbx_data_camera_elements(root, cam_obj, scene_data):
    """
    Write the Camera data blocks.
    """
    gscale = scene_data.settings.global_scale

    cam_data = cam_obj.data
    cam_key = scene_data.data_cameras[cam_obj]

    # Real data now, good old camera!
    # Object transform info.
    loc, rot, scale, matrix, matrix_rot = fbx_object_tx(scene_data, cam_obj)
    up = matrix_rot * Vector((0.0, 1.0, 0.0))
    to = matrix_rot * Vector((0.0, 0.0, -1.0))
    # Render settings.
    # TODO We could export much more...
    render = scene_data.scene.render
    width = render.resolution_x
    height = render.resolution_y
    aspect = width / height
    # Film width & height from mm to inches
    filmwidth = units_convert(cam_data.sensor_width, "millimeter", "inch")
    filmheight = units_convert(cam_data.sensor_height, "millimeter", "inch")
    filmaspect = filmwidth / filmheight
    # Film offset
    offsetx = filmwidth * cam_data.shift_x
    offsety = filmaspect * filmheight * cam_data.shift_y

    cam = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(cam_key))
    cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute"))
    cam.add_string(b"Camera")

    tmpl = scene_data.templates[b"Camera"]
    props = elem_properties(cam)
    elem_props_template_set(tmpl, props, "p_vector_3d", b"Position", loc)
    elem_props_template_set(tmpl, props, "p_vector_3d", b"UpVector", up)
    elem_props_template_set(tmpl, props, "p_vector_3d", b"InterestPosition", to)
    # Should we use world value?
    elem_props_template_set(tmpl, props, "p_color_rgb", b"BackgroundColor", (0.0, 0.0, 0.0))
    elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True)

    elem_props_template_set(tmpl, props, "p_number", b"FilmWidth", filmwidth)
    elem_props_template_set(tmpl, props, "p_number", b"FilmHeight", filmheight)
    elem_props_template_set(tmpl, props, "p_number", b"FilmAspectRatio", filmaspect)
    elem_props_template_set(tmpl, props, "p_number", b"FilmOffsetX", offsetx)
    elem_props_template_set(tmpl, props, "p_number", b"FilmOffsetY", offsety)

    elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3)  # FocalLength.
    elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2)  # FitHorizontal.
    elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x))
    elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x))
    elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y))
    # No need to convert to inches here...
    elem_props_template_set(tmpl, props, "p_number", b"FocalLength", cam_data.lens)
    elem_props_template_set(tmpl, props, "p_number", b"SafeAreaAspectRatio", aspect)

    elem_props_template_set(tmpl, props, "p_number", b"NearPlane", cam_data.clip_start * gscale)
    elem_props_template_set(tmpl, props, "p_number", b"FarPlane", cam_data.clip_end * gscale)
    elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1)  # RelativeToCamera.
    elem_props_template_set(tmpl, props, "p_number", b"BackPlaneDistance", cam_data.clip_end * gscale)

    # Custom properties.
    if scene_data.settings.use_custom_properties:
        fbx_data_element_custom_properties(props, cam_data)

    elem_data_single_string(cam, b"TypeFlags", b"Camera")
    elem_data_single_int32(cam, b"GeometryVersion", 124)  # Sic...
    elem_data_vec_float64(cam, b"Position", loc)
    elem_data_vec_float64(cam, b"Up", up)
    elem_data_vec_float64(cam, b"LookAt", to)
    elem_data_single_int32(cam, b"ShowInfoOnMoving", 1)
    elem_data_single_int32(cam, b"ShowAudio", 0)
    elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0))
    elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)


def fbx_data_mesh_elements(root, me, scene_data):
    """
    Write the Mesh (Geometry) data block.
    """
    # Ugly helper... :/
    def _infinite_gen(val):
        while 1:
            yield val

    me_key, me_obj = scene_data.data_meshes[me]

    # No gscale/gmat here, all data are supposed to be in object space.
    smooth_type = scene_data.settings.mesh_smooth_type

    do_bake_space_transform = use_bake_space_transform(scene_data, me_obj)

    # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the