Skip to content
Snippets Groups Projects
export_fbx_bin.py 93.6 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_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"))
    
    
    ##### 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"type_2", b"type_3", "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!
    #     Among other things, what are those "A"/"A+"/"AU" codes?
    FBX_PROPERTIES_DEFINITIONS = {
        "p_bool": (b"bool", b"", b"", "add_int32"),  # Yes, int32 for a bool (and they do have a core bool type)!!!
        "p_integer": (b"int", b"Integer", b"", "add_int32"),
        "p_enum": (b"enum", b"", b"", "add_int32"),
        "p_number": (b"double", b"Number", b"", "add_float64"),
        "p_visibility": (b"Visibility", b"", b"A+", "add_float64"),
        "p_fov": (b"FieldOfView", b"", b"A+", "add_float64"),
        "p_fov_x": (b"FieldOfViewX", b"", b"A+", "add_float64"),
        "p_fov_y": (b"FieldOfViewY", b"", b"A+", "add_float64"),
        "p_vector_3d": (b"Vector3D", b"Vector", b"", "add_float64", "add_float64", "add_float64"),
        "p_lcl_translation": (b"Lcl Translation", b"", b"A+", "add_float64", "add_float64", "add_float64"),
        "p_lcl_rotation": (b"Lcl Rotation", b"", b"A+", "add_float64", "add_float64", "add_float64"),
        "p_lcl_scaling": (b"Lcl Scaling", b"", b"A+", "add_float64", "add_float64", "add_float64"),
        "p_color_rgb": (b"ColorRGB", b"Color", b"", "add_float64", "add_float64", "add_float64"),
        "p_string": (b"KString", b"", b"", "add_string_unicode"),
        "p_string_url": (b"KString", b"Url", b"", "add_string_unicode"),
        "p_timestamp": (b"KTime", b"Time", b"", "add_int64"),
        "p_datetime": (b"DateTime", b"", b"", "add_string_unicode"),
        "p_object": (b"object", b"", b""),  # XXX Check this! No value for this prop???
        "p_compound": (b"Compound", b"", b""),  # XXX Check this! No value for this prop???
    }
    
    
    def _elem_props_set(elem, ptype, name, value):
        p = elem_data_single_string(elem, b"P", name)
        for t in ptype[:3]:
            p.add_string(t)
        if len(ptype) == 4:
            getattr(p, ptype[3])(value)
        elif len(ptype) > 4:
            # We assume value is iterable, else it's a bug!
            for callback, val in zip(ptype[3:], value):
                getattr(p, callback)(val)
    
    
    def elem_props_set(elem, ptype, name, value=None):
        ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
        _elem_props_set(elem, ptype, name, value)
    
    
    def elem_props_compound(elem, cmpd_name):
        def _setter(ptype, name, value):
            name = cmpd_name + b"|" + name
            elem_props_set(elem, ptype, name, value)
    
        elem_props_set(elem, "p_compound", cmpd_name)
        return _setter
    
    
    def elem_props_template_set(template, elem, ptype_name, name, value):
        """
        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 = template.properties.get(name, (None, None))
        if tmpl_ptype is not None:
            if ((len(ptype) == 4 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    (len(ptype) > 4 and (tuple(tmpl_val), tmpl_ptype) == (tuple(value), ptype_name))):
    
                return  # Already in template and same value.
        _elem_props_set(elem, ptype, name, value)
    
    
    ##### 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) in properties.items():
                        elem_props_set(props, ptype, name, value)
    
    
    def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0):
        props = {}
        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 = {
            b"QuaternionInterpolate": (False, "p_bool"),
            b"RotationOffset": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"RotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"ScalingOffset": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"ScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"TranslationActive": (False, "p_bool"),
            b"TranslationMin": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"TranslationMax": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"TranslationMinX": (False, "p_bool"),
            b"TranslationMinY": (False, "p_bool"),
            b"TranslationMinZ": (False, "p_bool"),
            b"TranslationMaxX": (False, "p_bool"),
            b"TranslationMaxY": (False, "p_bool"),
            b"TranslationMaxZ": (False, "p_bool"),
            b"RotationOrder": (0, "p_enum"),  # we always use 'XYZ' order.
            b"RotationSpaceForLimitOnly": (False, "p_bool"),
            b"RotationStiffnessX": (0.0, "p_number"),
            b"RotationStiffnessY": (0.0, "p_number"),
            b"RotationStiffnessZ": (0.0, "p_number"),
            b"AxisLen": (10.0, "p_number"),
            b"PreRotation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"PostRotation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"RotationActive": (False, "p_bool"),
            b"RotationMin": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"RotationMax": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"RotationMinX": (False, "p_bool"),
            b"RotationMinY": (False, "p_bool"),
            b"RotationMinZ": (False, "p_bool"),
            b"RotationMaxX": (False, "p_bool"),
            b"RotationMaxY": (False, "p_bool"),
            b"RotationMaxZ": (False, "p_bool"),
            b"InheritType": (1, "p_enum"),  # RSrs
            b"ScalingActive": (False, "p_bool"),
            b"ScalingMin": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"),
            b"ScalingMax": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"),
            b"ScalingMinX": (False, "p_bool"),
            b"ScalingMinY": (False, "p_bool"),
            b"ScalingMinZ": (False, "p_bool"),
            b"ScalingMaxX": (False, "p_bool"),
            b"ScalingMaxY": (False, "p_bool"),
            b"ScalingMaxZ": (False, "p_bool"),
            b"GeometricTranslation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"GeometricRotation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"GeometricScaling": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"),
            b"MinDampRangeX": (0.0, "p_number"),
            b"MinDampRangeY": (0.0, "p_number"),
            b"MinDampRangeZ": (0.0, "p_number"),
            b"MaxDampRangeX": (0.0, "p_number"),
            b"MaxDampRangeY": (0.0, "p_number"),
            b"MaxDampRangeZ": (0.0, "p_number"),
            b"MinDampStrengthX": (0.0, "p_number"),
            b"MinDampStrengthY": (0.0, "p_number"),
            b"MinDampStrengthZ": (0.0, "p_number"),
            b"MaxDampStrengthX": (0.0, "p_number"),
            b"MaxDampStrengthY": (0.0, "p_number"),
            b"MaxDampStrengthZ": (0.0, "p_number"),
            b"PreferedAngleX": (0.0, "p_number"),
            b"PreferedAngleY": (0.0, "p_number"),
            b"PreferedAngleZ": (0.0, "p_number"),
            b"LookAtProperty": (None, "p_object"),
            b"UpVectorProperty": (None, "p_object"),
            b"Show": (True, "p_bool"),
            b"NegativePercentShapeSupport": (True, "p_bool"),
            b"DefaultAttributeIndex": (0, "p_integer"),
            b"Freeze": (False, "p_bool"),
            b"LODBox": (False, "p_bool"),
            b"Lcl Translation": ((0.0, 0.0, 0.0), "p_lcl_translation"),
            b"Lcl Rotation": ((0.0, 0.0, 0.0), "p_lcl_rotation"),
            b"Lcl Scaling": (Vector((1.0, 1.0, 1.0)) * gscale, "p_lcl_scaling"),
            b"Visibility": (1.0, "p_visibility"),
        }
        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 = {
            b"LightType": (0, "p_enum"),  # Point light.
            b"CastLight": (True, "p_bool"),
            b"Color": ((1.0, 1.0, 1.0), "p_color_rgb"),
            b"Intensity": (100.0, "p_number"),  # Times 100 compared to Blender values...
            b"DecayType": (2, "p_enum"),  # Quadratic.
            b"DecayStart": (30.0 * gscale, "p_number"),
            b"CastShadows": (True, "p_bool"),
            b"ShadowColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
            b"AreaLightShape": (0, "p_enum"),  # 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 = {}
        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 = {}
        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 = {
            b"Color": ((0.8, 0.8, 0.8), "p_color_rgb"),
            b"BBoxMin": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"BBoxMax": ((0.0, 0.0, 0.0), "p_vector_3d"),
        }
        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 = {
            b"ShadingModel": ("phong", "p_string"),
            b"MultiLayer": (False, "p_bool"),
            # Lambert-specific.
            b"EmissiveColor": ((0.8, 0.8, 0.8), "p_color_rgb"),  # Same as diffuse.
            b"EmissiveFactor": (0.0, "p_number"),
            b"AmbientColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
            b"AmbientFactor": (1.0, "p_number"),
            b"DiffuseColor": ((0.8, 0.8, 0.8), "p_color_rgb"),
            b"DiffuseFactor": (0.8, "p_number"),
            b"TransparentColor": ((0.8, 0.8, 0.8), "p_color_rgb"),  # Same as diffuse.
            b"TransparencyFactor": (0.0, "p_number"),
            b"NormalMap": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"BumpFactor": (1.0, "p_number"),
            b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
            b"DisplacementFactor": (0.0, "p_number"),
            # Phong-specific.
            b"SpecularColor": ((1.0, 1.0, 1.0), "p_color_rgb"),
            b"SpecularFactor": (0.5 / 2.0, "p_number"),
            # Not sure about the name, importer use 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"),
            b"ShininessExponent": ((50.0 - 1.0) / 5.10, "p_number"),
            b"ReflectionColor": ((1.0, 1.0, 1.0), "p_color_rgb"),
            b"ReflectionFactor": (0.0, "p_number"),
        }
        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 = {
            b"TextureTypeUse": (0, "p_enum"),  # Standard.
            b"AlphaSource": (2, "p_enum"),  # Black (i.e. texture's alpha), XXX name guessed!.
            b"Texture alpha": (1.0, "p_number"),
            b"PremultiplyAlpha": (False, "p_bool"),
            b"CurrentTextureBlendMode": (0, "p_enum"),  # Translucent, assuming this means "Alpha over"!
            b"CurrentMappingType": (1, "p_enum"),  # Planar.
            b"WrapModeU": (0, "p_enum"),  # Repeat.
            b"WrapModeV": (0, "p_enum"),  # Repeat.
            b"UVSwap": (False, "p_bool"),
            b"Translation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"Rotation": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"Scaling": ((1.0, 1.0, 1.0), "p_vector_3d"),
            b"TextureRotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d"),
            b"TextureScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d"),
            # Not sure about those two... At least, UseMaterial should always be ON imho.
            b"UseMaterial": (True, "p_bool"),
            b"UseMipMap": (False, "p_bool"),
        }
        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 = {
            # All pictures.
            b"Width": (0, "p_integer"),
            b"Height": (0, "p_integer"),
            b"Path": ("", "p_string_url"),
            b"AccessMode": (0, "p_enum"),  # Disk (0=Disk, 1=Mem, 2=DiskAsync).
            # All videos.
            b"StartFrame": (0, "p_integer"),
            b"StopFrame": (0, "p_integer"),
            b"Offset": (0, "p_timestamp"),
            b"PlaySpeed": (1.0, "p_number"),
            b"FreeRunning": (False, "p_bool"),
            b"Loop": (False, "p_bool"),
            b"InterlaceMode": (0, "p_enum"),  # None, i.e. progressive.
            # Image sequences.
            b"ImageSequence": (False, "p_bool"),
            b"ImageSequenceOffset": (0, "p_integer"),
            b"FrameRate": (scene.render.fps / scene.render.fps_base, "p_number"),
            b"LastFrame": (0, "p_integer"),
        }
        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 = {}
        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 = {}
        if override_defaults is not None:
            props.update(override_defaults)
        return FBXTemplate(b"Deformer", b"", props, nbr_users)
    
    
    ##### FBX objects generators. #####
    
    def object_matrix(scene_data, obj, armature=None, global_space=False):
        """
        Generate object transform matrix.
        If global_space is False, returned matrix is in parent space if parent exists and is exported, else in world space.
        If global_space is True, returned matrix is always in world 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!
        is_global = global_space or not (is_bone or (obj.parent and obj.parent in scene_data.objects))
    
        #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 = scene_data.settings.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 is_global:
            if obj.parent:
                matrix = obj.parent.matrix_world * matrix
            matrix = scene_data.settings.global_matrix * matrix
    
        return matrix
    
    
    def object_tx(scene_data, obj):
        """
        Generate object transform data (always in local space when possible).
        """
        matrix = 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(tmpl, 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_template_set(tmpl, props, "p_string", k.encode(), v)
            elif isinstance(v, int):
                elem_props_template_set(tmpl, props, "p_integer", k.encode(), v)
            if isinstance(v, float):
                elem_props_template_set(tmpl, props, "p_number", k.encode(), v)
    
    
    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(tmpl, 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 = 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(tmpl, 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.
        """
        # No gscale/gmat here, all data are supposed to be in object space.
        smooth_type = scene_data.settings.mesh_smooth_type
    
        me_key = scene_data.data_meshes[me]
        geom = elem_data_single_int64(root, b"Geometry", get_fbxuid_from_key(me_key))
        geom.add_string(fbx_name_class(me.name.encode(), b"Geometry"))
        geom.add_string(b"Mesh")
    
        tmpl = scene_data.templates[b"Geometry"]
        props = elem_properties(geom)
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
            fbx_data_element_custom_properties(tmpl, props, me)
    
        elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
    
        # Vertex cos.
    
        t_co = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
    
        me.vertices.foreach_get("co", t_co)
        elem_data_single_float64_array(geom, b"Vertices", t_co)
        del t_co
    
        # Polygon indices.
        #
        # We do loose edges as two-vertices faces, if enabled...
        #
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        # Note we have to process Edges in the same time, as they are based on poly's loops...
    
        loop_nbr = len(me.loops)
    
        t_pvi = array.array(data_types.ARRAY_INT32, (0,)) * loop_nbr
    
        t_ls = [None] * len(me.polygons)
    
        me.loops.foreach_get("vertex_index", t_pvi)
        me.polygons.foreach_get("loop_start", t_ls)
    
        # Add "fake" faces for loose edges.
        if scene_data.settings.use_mesh_edges:
            t_le = tuple(e.vertices for e in me.edges if e.is_loose)
            t_pvi.extend(chain(*t_le))
            t_ls.extend(range(loop_nbr, loop_nbr + len(t_le), 2))
            del t_le
    
        # Edges...
        # Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array.
        #       The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon.
        #       Advantage: Only one index per edge.
        #       Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons
        #                 for loose edges).
        #       We also have to store a mapping from real edges to their indices in this array, for edge-mapped data
        #       (like e.g. crease).
        t_eli = array.array(data_types.ARRAY_INT32)
        edges_map = {}
        edges_nbr = 0
        if t_ls and t_pvi:
            t_ls = set(t_ls)
            todo_edges = [None] * len(me.edges) * 2
            me.edges.foreach_get("vertices", todo_edges)
            todo_edges = set((v1, v2) if v1 < v2 else (v2, v1) for v1, v2 in zip(*(iter(todo_edges),) * 2))
    
            li = 0
            vi = vi_start = t_pvi[0]
            for li_next, vi_next in enumerate(t_pvi[1:] + t_pvi[:1], start=1):
                if li_next in t_ls:  # End of a poly's loop.
                    vi2 = vi_start
                    vi_start = vi_next
                else:
                    vi2 = vi_next
    
                e_key = (vi, vi2) if vi < vi2 else (vi2, vi)
                if e_key in todo_edges:
                    t_eli.append(li)
                    todo_edges.remove(e_key)
                    edges_map[e_key] = edges_nbr
                    edges_nbr += 1
    
                vi = vi_next
                li = li_next
        # End of edges!
    
        # We have to ^-1 last index of each loop.
        for ls in t_ls:
            t_pvi[ls - 1] ^= -1
    
        # And finally we can write data!
        elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi)
        elem_data_single_int32_array(geom, b"Edges", t_eli)
        del t_pvi
        del t_ls
        del t_eli
    
        # And now, layers!
    
        # Smoothing.
        if smooth_type in {'FACE', 'EDGE'}:
            t_ps = None
            _map = b""
            if smooth_type == 'FACE':
    
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
    
                me.polygons.foreach_get("use_smooth", t_ps)
                _map = b"ByPolygon"
            else:  # EDGE
                # Write Edge Smoothing.
    
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * edges_nbr
    
                for e in me.edges:
                    if e.key not in edges_map:
                        continue  # Only loose edges, in theory!
                    t_ps[edges_map[e.key]] = not e.use_edge_sharp
                _map = b"ByEdge"
            lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0)
            elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION)
            elem_data_single_string(lay_smooth, b"Name", b"")
            elem_data_single_string(lay_smooth, b"MappingInformationType", _map)
            elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct")
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps)  # Sight, int32 for bool...
    
            del t_ps
    
        # TODO: Edge crease (LayerElementCrease).
    
        # And we are done with edges!
        del edges_map
    
        # Loop normals.
        # NOTE: this is not supported by importer currently.
        # XXX Official docs says normals should use IndexToDirect,
        #     but this does not seem well supported by apps currently...
        me.calc_normals_split()
        if 0:
            def _nortuples_gen(raw_nors):
                return zip(*(iter(raw_nors),) * 3)