Skip to content
Snippets Groups Projects
import_fbx.py 132 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) Blender Foundation
    
    
    # FBX 7.1.0 -> 7.4.0 loader for Blender
    
    # Not totally pep8 compliant.
    #   pep8 import_fbx.py --ignore=E501,E123,E702,E125
    
    
    if "bpy" in locals():
        import importlib
        if "parse_fbx" in locals():
            importlib.reload(parse_fbx)
    
        if "fbx_utils" in locals():
            importlib.reload(fbx_utils)
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    from mathutils import Matrix, Euler, Vector
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    from . import parse_fbx, fbx_utils
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    from .fbx_utils import (
    
        units_blender_to_fbx_factor,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        units_convertor_iter,
        array_to_matrix4,
        similar_values,
        similar_values_iter,
    
    # global singleton, assign on execution
    fbx_elem_nil = None
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    # Units convertors...
    convert_deg_to_rad_iter = units_convertor_iter("degree", "radian")
    
    MAT_CONVERT_BONE = fbx_utils.MAT_CONVERT_BONE.inverted()
    MAT_CONVERT_LAMP = fbx_utils.MAT_CONVERT_LAMP.inverted()
    MAT_CONVERT_CAMERA = fbx_utils.MAT_CONVERT_CAMERA.inverted()
    
    
    def elem_find_first(elem, id_search, default=None):
    
        for fbx_item in elem.elems:
            if fbx_item.id == id_search:
                return fbx_item
    
    def elem_find_iter(elem, id_search):
        for fbx_item in elem.elems:
            if fbx_item.id == id_search:
                yield fbx_item
    
    
    
    def elem_find_first_string(elem, id_search):
        fbx_item = elem_find_first(elem, id_search)
    
        if fbx_item is not None and fbx_item.props:  # Do not error on complete empty properties (see T45291).
    
            assert(len(fbx_item.props) == 1)
            assert(fbx_item.props_type[0] == data_types.STRING)
            return fbx_item.props[0].decode('utf-8')
        return None
    
    
    
    def elem_find_first_string_as_bytes(elem, id_search):
        fbx_item = elem_find_first(elem, id_search)
    
        if fbx_item is not None and fbx_item.props:  # Do not error on complete empty properties (see T45291).
    
            assert(len(fbx_item.props) == 1)
            assert(fbx_item.props_type[0] == data_types.STRING)
            return fbx_item.props[0]  # Keep it as bytes as requested...
        return None
    
    
    
    def elem_find_first_bytes(elem, id_search, decode=True):
        fbx_item = elem_find_first(elem, id_search)
    
        if fbx_item is not None and fbx_item.props:  # Do not error on complete empty properties (see T45291).
    
            assert(len(fbx_item.props) == 1)
    
            assert(fbx_item.props_type[0] == data_types.BYTES)
    
            return fbx_item.props[0]
        return None
    
    
    def elem_repr(elem):
        return "%s: props[%d=%r], elems=(%r)" % (
            elem.id,
            len(elem.props),
            ", ".join([repr(p) for p in elem.props]),
            # elem.props_type,
            b", ".join([e.id for e in elem.elems]),
            )
    
    
    def elem_split_name_class(elem):
        assert(elem.props_type[-2] == data_types.STRING)
        elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
        return elem_name, elem_class
    
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    def elem_name_ensure_class(elem, clss=...):
        elem_name, elem_class = elem_split_name_class(elem)
        if clss is not ...:
            assert(elem_class == clss)
        return elem_name.decode('utf-8')
    
    
    
    def elem_name_ensure_classes(elem, clss=...):
        elem_name, elem_class = elem_split_name_class(elem)
        if clss is not ...:
            assert(elem_class in clss)
        return elem_name.decode('utf-8')
    
    
    
    def elem_split_name_class_nodeattr(elem):
        assert(elem.props_type[-2] == data_types.STRING)
        elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
        assert(elem_class == b'NodeAttribute')
        assert(elem.props_type[-1] == data_types.STRING)
        elem_class = elem.props[-1]
        return elem_name, elem_class
    
    
    
    def elem_uuid(elem):
        assert(elem.props_type[0] == data_types.INT64)
        return elem.props[0]
    
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    def elem_prop_first(elem, default=None):
        return elem.props[0] if (elem is not None) and elem.props else default
    
    
    
    # ----
    # Support for
    # Properties70: { ... P:
    def elem_props_find_first(elem, elem_prop_id):
    
        if elem is None:
            # When properties are not found... Should never happen, but happens - as usual.
            return None
    
        # support for templates (tuple of elems)
        if type(elem) is not FBXElem:
            assert(type(elem) is tuple)
            for e in elem:
                result = elem_props_find_first(e, elem_prop_id)
                if result is not None:
                    return result
            assert(len(elem) > 0)
            return None
    
    
        for subelem in elem.elems:
            assert(subelem.id == b'P')
            if subelem.props[0] == elem_prop_id:
                return subelem
        return None
    
    
    def elem_props_get_color_rgb(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            if elem_prop.props[1] == b'Color':
                # FBX version 7300
                assert(elem_prop.props[1] == b'Color')
                assert(elem_prop.props[2] == b'')
            else:
                assert(elem_prop.props[1] == b'ColorRGB')
                assert(elem_prop.props[2] == b'Color')
            assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
            return elem_prop.props[4:7]
        return default
    
    
    
    def elem_props_get_vector_3d(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
            return elem_prop.props[4:7]
        return default
    
    
    
    def elem_props_get_number(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            if elem_prop.props[1] == b'double':
                assert(elem_prop.props[1] == b'double')
                assert(elem_prop.props[2] == b'Number')
            else:
                assert(elem_prop.props[1] == b'Number')
                assert(elem_prop.props[2] == b'')
    
            # we could allow other number types
            assert(elem_prop.props_type[4] == data_types.FLOAT64)
    
            return elem_prop.props[4]
        return default
    
    
    
    def elem_props_get_integer(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            if elem_prop.props[1] == b'int':
                assert(elem_prop.props[1] == b'int')
                assert(elem_prop.props[2] == b'Integer')
            elif elem_prop.props[1] == b'ULongLong':
                assert(elem_prop.props[1] == b'ULongLong')
                assert(elem_prop.props[2] == b'')
    
            # we could allow other number types
            assert(elem_prop.props_type[4] in {data_types.INT32, data_types.INT64})
    
            return elem_prop.props[4]
        return default
    
    
    
    def elem_props_get_bool(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            assert(elem_prop.props[1] == b'bool')
            assert(elem_prop.props[2] == b'')
            assert(elem_prop.props[3] == b'')
    
            # we could allow other number types
            assert(elem_prop.props_type[4] == data_types.INT32)
    
            assert(elem_prop.props[4] in {0, 1})
    
    def elem_props_get_enum(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            assert(elem_prop.props[1] == b'enum')
            assert(elem_prop.props[2] == b'')
            assert(elem_prop.props[3] == b'')
    
            # we could allow other number types
            assert(elem_prop.props_type[4] == data_types.INT32)
    
            return elem_prop.props[4]
        return default
    
    
    
    def elem_props_get_visibility(elem, elem_prop_id, default=None):
        elem_prop = elem_props_find_first(elem, elem_prop_id)
        if elem_prop is not None:
            assert(elem_prop.props[0] == elem_prop_id)
            assert(elem_prop.props[1] == b'Visibility')
            assert(elem_prop.props[2] == b'')
    
            # we could allow other number types
            assert(elem_prop.props_type[4] == data_types.FLOAT64)
    
            return elem_prop.props[4]
        return default
    
    
    
    # ----------------------------------------------------------------------------
    # Blender
    
    # ------
    # Object
    
    FBXTransformData = namedtuple("FBXTransformData", (
    
        "loc", "geom_loc",
    
        "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
        "sca", "sca_ofs", "sca_piv", "geom_sca",
    
    def blen_read_custom_properties(fbx_obj, blen_obj, settings):
        # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
        fbx_obj_props = elem_find_first(fbx_obj, b'Properties70')
        if fbx_obj_props:
            for fbx_prop in fbx_obj_props.elems:
                assert(fbx_prop.id == b'P')
    
                if b'U' in fbx_prop.props[3]:
                    if fbx_prop.props[0] == b'UDP3DSMAX':
                        # Special case for 3DS Max user properties:
                        assert(fbx_prop.props[1] == b'KString')
                        assert(fbx_prop.props_type[4] == data_types.STRING)
                        items = fbx_prop.props[4].decode('utf-8')
                        for item in items.split('\r\n'):
                            if item:
                                prop_name, prop_value = item.split('=', 1)
                                blen_obj[prop_name.strip()] = prop_value.strip()
                    else:
                        prop_name = fbx_prop.props[0].decode('utf-8')
                        prop_type = fbx_prop.props[1]
                        if prop_type in {b'Vector', b'Vector3D', b'Color', b'ColorRGB'}:
                            assert(fbx_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
                            blen_obj[prop_name] = fbx_prop.props[4:7]
                        elif prop_type in {b'Vector4', b'ColorRGBA'}:
                            assert(fbx_prop.props_type[4:8] == bytes((data_types.FLOAT64,)) * 4)
                            blen_obj[prop_name] = fbx_prop.props[4:8]
                        elif prop_type == b'Vector2D':
                            assert(fbx_prop.props_type[4:6] == bytes((data_types.FLOAT64,)) * 2)
                            blen_obj[prop_name] = fbx_prop.props[4:6]
                        elif prop_type in {b'Integer', b'int'}:
                            assert(fbx_prop.props_type[4] == data_types.INT32)
                            blen_obj[prop_name] = fbx_prop.props[4]
                        elif prop_type == b'KString':
                            assert(fbx_prop.props_type[4] == data_types.STRING)
                            blen_obj[prop_name] = fbx_prop.props[4].decode('utf-8')
                        elif prop_type in {b'Number', b'double', b'Double'}:
                            assert(fbx_prop.props_type[4] == data_types.FLOAT64)
                            blen_obj[prop_name] = fbx_prop.props[4]
                        elif prop_type in {b'Float', b'float'}:
                            assert(fbx_prop.props_type[4] == data_types.FLOAT32)
                            blen_obj[prop_name] = fbx_prop.props[4]
                        elif prop_type in {b'Bool', b'bool'}:
                            assert(fbx_prop.props_type[4] == data_types.INT32)
                            blen_obj[prop_name] = fbx_prop.props[4] != 0
                        elif prop_type in {b'Enum', b'enum'}:
                            assert(fbx_prop.props_type[4:6] == bytes((data_types.INT32, data_types.STRING)))
                            val = fbx_prop.props[4]
    
                            if settings.use_custom_props_enum_as_string and fbx_prop.props[5]:
    
                                enum_items = fbx_prop.props[5].decode('utf-8').split('~')
                                assert(val >= 0 and val < len(enum_items))
                                blen_obj[prop_name] = enum_items[val]
                            else:
                                blen_obj[prop_name] = val
                        else:
                            print ("WARNING: User property type '%s' is not supported" % prop_type.decode('utf-8'))
    
    
    
    def blen_read_object_transform_do(transform_data):
    
        # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
        #
        #     WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1
        #
        # Where all those terms are 4 x 4 matrices that contain:
        #     WorldTransform: Transformation matrix of the node in global space.
        #     ParentWorldTransform: Transformation matrix of the parent node in global space.
        #     T: Translation
        #     Roff: Rotation offset
        #     Rp: Rotation pivot
        #     Rpre: Pre-rotation
        #     R: Rotation
        #     Rpost: Post-rotation
        #     Rp-1: Inverse of the rotation pivot
        #     Soff: Scaling offset
        #     Sp: Scaling pivot
        #     S: Scaling
        #     Sp-1: Inverse of the scaling pivot
        #
        # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
        # support 3DSMax way:
        #
        #     WorldTransform = ParentWorldTransform * T * R * S * OT * OR * OS
        #
        # Where all those terms are 4 x 4 matrices that contain:
        #     WorldTransform: Transformation matrix of the node in global space
        #     ParentWorldTransform: Transformation matrix of the parent node in global space
        #     T: Translation
        #     R: Rotation
        #     S: Scaling
        #     OT: Geometric transform translation
        #     OR: Geometric transform rotation
        #     OS: Geometric transform translation
        #
        # Notes:
        #     Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
        #     of WorldTransform's parent node.
        #
        # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
        #            index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
    
    
        # translation
        lcl_translation = Matrix.Translation(transform_data.loc)
    
        geom_loc = Matrix.Translation(transform_data.geom_loc)
    
    
        # rotation
        to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
        lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) * transform_data.rot_alt_mat
        pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord)
        pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord)
    
        geom_rot = to_rot(transform_data.geom_rot, transform_data.rot_ord)
    
    
        rot_ofs = Matrix.Translation(transform_data.rot_ofs)
        rot_piv = Matrix.Translation(transform_data.rot_piv)
        sca_ofs = Matrix.Translation(transform_data.sca_ofs)
        sca_piv = Matrix.Translation(transform_data.sca_piv)
    
        # scale
        lcl_scale = Matrix()
        lcl_scale[0][0], lcl_scale[1][1], lcl_scale[2][2] = transform_data.sca
    
        geom_scale = Matrix();
        geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca
    
        geom_mat = geom_loc * geom_rot * geom_scale
        # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
        return (base_mat * geom_mat, base_mat, geom_mat)
    
    # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
    #     more likely, will have to make this more robust!!!
    def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
        assert(len(vg_indices) == len(vg_weights))
        if vg_indices:
            for obj in objects:
                # We replace/override here...
                vg = obj.vertex_groups.get(vg_name)
                if vg is None:
                    vg = obj.vertex_groups.new(vg_name)
                for i, w in zip(vg_indices, vg_weights):
                    vg.add((i,), w, 'REPLACE')
    
    
    
    def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
    
        # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
    
        const_vector_zero_3d = 0.0, 0.0, 0.0
        const_vector_one_3d = 1.0, 1.0, 1.0
    
        loc = list(elem_props_get_vector_3d(fbx_props, b'Lcl Translation', const_vector_zero_3d))
        rot = list(elem_props_get_vector_3d(fbx_props, b'Lcl Rotation', const_vector_zero_3d))
        sca = list(elem_props_get_vector_3d(fbx_props, b'Lcl Scaling', const_vector_one_3d))
    
        geom_loc = list(elem_props_get_vector_3d(fbx_props, b'GeometricTranslation', const_vector_zero_3d))
        geom_rot = list(elem_props_get_vector_3d(fbx_props, b'GeometricRotation', const_vector_zero_3d))
        geom_sca = list(elem_props_get_vector_3d(fbx_props, b'GeometricScaling', const_vector_one_3d))
    
    
        rot_ofs = elem_props_get_vector_3d(fbx_props, b'RotationOffset', const_vector_zero_3d)
        rot_piv = elem_props_get_vector_3d(fbx_props, b'RotationPivot', const_vector_zero_3d)
        sca_ofs = elem_props_get_vector_3d(fbx_props, b'ScalingOffset', const_vector_zero_3d)
        sca_piv = elem_props_get_vector_3d(fbx_props, b'ScalingPivot', const_vector_zero_3d)
    
        is_rot_act = elem_props_get_bool(fbx_props, b'RotationActive', False)
    
        if is_rot_act:
    
            if use_prepost_rot:
                pre_rot = elem_props_get_vector_3d(fbx_props, b'PreRotation', const_vector_zero_3d)
                pst_rot = elem_props_get_vector_3d(fbx_props, b'PostRotation', const_vector_zero_3d)
            else:
                pre_rot = const_vector_zero_3d
                pst_rot = const_vector_zero_3d
    
                1: 'XZY',
                2: 'YZX',
                3: 'YXZ',
                4: 'ZXY',
                5: 'ZYX',
                6: 'XYZ',  # XXX eSphericXYZ, not really supported...
    
                }.get(elem_props_get_enum(fbx_props, b'RotationOrder', 0))
    
        else:
            pre_rot = const_vector_zero_3d
            pst_rot = const_vector_zero_3d
            rot_ord = 'XYZ'
    
    
        return FBXTransformData(loc, geom_loc,
                                rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat, geom_rot,
                                sca, sca_ofs, sca_piv, geom_sca)
    
    # ---------
    # Animation
    def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
        """
        Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
        together with (blender) timing, in frames.
        blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
        """
        # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
        # of FBX curves later.
        from .fbx_utils import FBX_KTIME
        timefac = fps / FBX_KTIME
    
        curves = tuple([0,
                        elem_prop_first(elem_find_first(c[2], b'KeyTime')),
                        elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
                        c]
    
                       for c in fbx_curves)
    
        allkeys = sorted({item for sublist in curves for item in sublist[1]})
        for curr_fbxktime in allkeys:
    
            curr_values = []
            for item in curves:
                idx, times, values, fbx_curve = item
    
                    if idx >= 0:
                        idx += 1
                        if idx >= len(times):
                            # We have reached our last element for this curve, stay on it from now on...
                            idx = -1
                        item[0] = idx
    
    
                if times[idx] >= curr_fbxktime:
                    if idx == 0:
                        curr_values.append((values[idx], fbx_curve))
                    else:
                        # Interpolate between this key and the previous one.
                        ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
                        curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
    
            curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
            yield (curr_blenkframe, curr_values)
    
    
    
    def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        'Bake' loc/rot/scale into the action,
        taking any pre_ and post_ matrix into account to transform from fbx into blender space.
    
        from bpy.types import Object, PoseBone, ShapeKey, Material
    
        from itertools import chain
    
        fbx_curves = []
    
        for curves, fbxprop in cnodes.values():
            for (fbx_acdata, _blen_data), channel in curves.values():
                fbx_curves.append((fbxprop, channel, fbx_acdata))
    
        # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
        if len(fbx_curves) == 0:
            return
    
        blen_curves = []
    
        if isinstance(item, Material):
            grpname = item.name
            props = [("diffuse_color", 3, grpname or "Diffuse Color")]
        elif isinstance(item, ShapeKey):
    
            props = [(item.path_from_id("value"), 1, "Key")]
        else:  # Object or PoseBone:
    
            if item.is_bone:
                bl_obj = item.bl_obj.pose.bones[item.bl_bone]
            else:
                bl_obj = item.bl_obj
    
    
            # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
    
    
            # Since we might get other channels animated in the end, due to all FBX transform magic,
            # we need to add curves for whole loc/rot/scale in any case.
    
            props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
    
                     (bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
            rot_mode = bl_obj.rotation_mode
    
            if rot_mode == 'QUATERNION':
    
                props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
    
            elif rot_mode == 'AXIS_ANGLE':
    
                props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
    
                props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
    
    
        blen_curves = [action.fcurves.new(prop, channel, grpname)
                       for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
    
    
        if isinstance(item, Material):
            for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
                value = [0,0,0]
                for v, (fbxprop, channel, _fbx_acdata) in values:
                    assert(fbxprop == b'DiffuseColor')
                    assert(channel in {0, 1, 2})
                    value[channel] = v
    
                for fc, v in zip(blen_curves, value):
                    fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
    
        elif isinstance(item, ShapeKey):
    
            for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
    
                value = 0.0
                for v, (fbxprop, channel, _fbx_acdata) in values:
                    assert(fbxprop == b'DeformPercent')
                    assert(channel == 0)
                    value = v / 100.0
    
                for fc, v in zip(blen_curves, (value,)):
                    fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
    
        else:  # Object or PoseBone:
    
            if item.is_bone:
                bl_obj = item.bl_obj.pose.bones[item.bl_bone]
            else:
                bl_obj = item.bl_obj
    
            transform_data = item.fbx_transform_data
            rot_prev = bl_obj.rotation_euler.copy()
    
            # Pre-compute inverted local rest matrix of the bone, if relevant.
    
            restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
    
            for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
    
                for v, (fbxprop, channel, _fbx_acdata) in values:
                    if fbxprop == b'Lcl Translation':
                        transform_data.loc[channel] = v
                    elif fbxprop == b'Lcl Rotation':
                        transform_data.rot[channel] = v
                    elif fbxprop == b'Lcl Scaling':
                        transform_data.sca[channel] = v
    
                mat, _, _ = blen_read_object_transform_do(transform_data)
    
    
                # compensate for changes in the local matrix during processing
                if item.anim_compensation_matrix:
                    mat = mat * item.anim_compensation_matrix
    
                # apply pre- and post matrix
                # post-matrix will contain any correction for lights, camera and bone orientation
                # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
                if item.pre_matrix:
                    mat = item.pre_matrix * mat
                if item.post_matrix:
                    mat = mat * item.post_matrix
    
                # And now, remove that rest pose matrix from current mat (also in parent space).
                if restmat_inv:
    
                    mat = restmat_inv * mat
    
    
                # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
                loc, rot, sca = mat.decompose()
                if rot_mode == 'QUATERNION':
                    pass  # nothing to do!
                elif rot_mode == 'AXIS_ANGLE':
                    vec, ang = rot.to_axis_angle()
                    rot = ang, vec.x, vec.y, vec.z
                else:  # Euler
                    rot = rot.to_euler(rot_mode, rot_prev)
                    rot_prev = rot
                for fc, value in zip(blen_curves, chain(loc, rot, sca)):
                    fc.keyframe_points.insert(frame, value, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
    
        # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
        for fc in blen_curves:
            fc.update()
    
    
    
    def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset):
    
        """
        Recreate an action per stack/layer/object combinations.
    
        Only the first found action is linked to objects, more complex setups are not handled,
        it's up to user to reproduce them!
    
        from bpy.types import ShapeKey, Material
    
        actions = {}
        for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
            stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
            for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items():
                layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer')
                for item, cnodes in items.items():
    
                    if isinstance(item, Material):
                        id_data = item
                    elif isinstance(item, ShapeKey):
    
                        # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
                        #     FbxImportHelperNode class definition.
                        if id_data.type == 'MESH' and id_data.parent and id_data.parent.type == 'ARMATURE':
                            continue
    
                    # Create new action if needed (should always be needed!
    
                    key = (as_uuid, al_uuid, id_data)
                    action = actions.get(key)
                    if action is None:
                        action_name = "|".join((id_data.name, stack_name, layer_name))
                        actions[key] = action = bpy.data.actions.new(action_name)
                        action.use_fake_user = True
    
                    # If none yet assigned, assign this action to id_data.
                    if not id_data.animation_data:
                        id_data.animation_data_create()
                    if not id_data.animation_data.action:
                        id_data.animation_data.action = action
                    # And actually populate the action!
    
                    blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset)
    
    # ----
    # Mesh
    
    def blen_read_geom_layerinfo(fbx_layer):
        return (
            elem_find_first_string(fbx_layer, b'Name'),
    
            elem_find_first_string_as_bytes(fbx_layer, b'MappingInformationType'),
            elem_find_first_string_as_bytes(fbx_layer, b'ReferenceInformationType'),
    
    def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform):
        """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
    
        max_idx = len(blen_data) - 1
        print_error = True
    
        def check_skip(blen_idx, fbx_idx):
            nonlocal print_error
    
            if fbx_idx < 0:  # Negative values mean 'skip'.
    
                return True
            if blen_idx > max_idx:
                if print_error:
                    print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
                    print_error = False
                return True
            return False
    
    
            if isinstance(blen_data, list):
                if item_size == 1:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        blen_data[blen_idx] = xform(fbx_data[fbx_idx])
                else:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size])
            else:
                if item_size == 1:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx]))
                else:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size]))
    
            if isinstance(blen_data, list):
                if item_size == 1:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        blen_data[blen_idx] = fbx_data[fbx_idx]
                else:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size]
            else:
                if item_size == 1:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx])
                else:
    
                    def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
    
                        setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx:fbx_idx + item_size])
    
        for blen_idx, fbx_idx in generator:
            if check_skip(blen_idx, fbx_idx):
                continue
    
            _process(blen_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx)
    
    def blen_read_geom_array_gen_allsame(data_len):
        return zip(*(range(data_len), (0,) * data_len))
    
    
    
    def blen_read_geom_array_gen_direct(fbx_data, stride):
        fbx_data_len = len(fbx_data)
        return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride)))
    
    
    def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride):
        return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
    
    
    def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
        fbx_data_len = len(fbx_data) // stride
        loops = mesh.loops
        for p in mesh.polygons:
            for lidx in p.loop_indices:
                vidx = loops[lidx].vertex_index
                if vidx < fbx_data_len:
                    yield lidx, vidx * stride
    
    
    # generic error printers.
    
    def blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet=False):
        if not quiet:
            print("warning layer %r mapping type unsupported: %r" % (descr, fbx_layer_mapping))
    
    def blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet=False):
        if not quiet:
            print("warning layer %r ref type unsupported: %r" % (descr, fbx_layer_ref))
    
    def blen_read_geom_array_mapped_vert(
    
            mesh, blen_data, blen_attr,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            fbx_layer_data, fbx_layer_index,
            fbx_layer_mapping, fbx_layer_ref,
            stride, item_size, descr,
    
        if fbx_layer_mapping == b'ByVertice':
            if fbx_layer_ref == b'Direct':
                assert(fbx_layer_index is None)
    
                blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        elif fbx_layer_mapping == b'AllSame':
            if fbx_layer_ref == b'IndexToDirect':
                assert(fbx_layer_index is None)
                blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        else:
    
            blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
    
    def blen_read_geom_array_mapped_edge(
    
            mesh, blen_data, blen_attr,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            fbx_layer_data, fbx_layer_index,
            fbx_layer_mapping, fbx_layer_ref,
            stride, item_size, descr,
    
        if fbx_layer_mapping == b'ByEdge':
            if fbx_layer_ref == b'Direct':
    
                blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        elif fbx_layer_mapping == b'AllSame':
            if fbx_layer_ref == b'IndexToDirect':
                assert(fbx_layer_index is None)
                blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
            blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
    
    def blen_read_geom_array_mapped_polygon(
    
            mesh, blen_data, blen_attr,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            fbx_layer_data, fbx_layer_index,
            fbx_layer_mapping, fbx_layer_ref,
            stride, item_size, descr,
    
        if fbx_layer_mapping == b'ByPolygon':
    
                # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
                #     We fallback to 'Direct' mapping in this case.
                #~ assert(fbx_layer_index is not None)
                if fbx_layer_index is None:
                    blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
    
                                                 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
                else:
    
                    blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
                                                 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
            elif fbx_layer_ref == b'Direct':
    
                blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        elif fbx_layer_mapping == b'AllSame':
            if fbx_layer_ref == b'IndexToDirect':
                assert(fbx_layer_index is None)
                blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
            blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
    
    def blen_read_geom_array_mapped_polyloop(
    
            mesh, blen_data, blen_attr,
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            fbx_layer_data, fbx_layer_index,
            fbx_layer_mapping, fbx_layer_ref,
            stride, item_size, descr,
    
        if fbx_layer_mapping == b'ByPolygonVertex':
            if fbx_layer_ref == b'IndexToDirect':
    
                # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
                #     We fallback to 'Direct' mapping in this case.
                #~ assert(fbx_layer_index is not None)
                if fbx_layer_index is None:
                    blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
                                                 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                else:
                    blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
                                                 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
            elif fbx_layer_ref == b'Direct':
                blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
    
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        elif fbx_layer_mapping == b'ByVertice':
            if fbx_layer_ref == b'Direct':
                assert(fbx_layer_index is None)
    
                blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
        elif fbx_layer_mapping == b'AllSame':
            if fbx_layer_ref == b'IndexToDirect':
                assert(fbx_layer_index is None)
                blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
                                             blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
                return True
    
            blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
    
            blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
    
    def blen_read_geom_layer_material(fbx_obj, mesh):
    
        fbx_layer = elem_find_first(fbx_obj, b'LayerElementMaterial')
    
        if fbx_layer is None:
            return
    
        (fbx_layer_name,
         fbx_layer_mapping,
         fbx_layer_ref,
         ) = blen_read_geom_layerinfo(fbx_layer)
    
        layer_id = b'Materials'
        fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
    
    
        blen_data = mesh.polygons
    
        blen_read_geom_array_mapped_polygon(
    
            mesh, blen_data, "material_index",
    
            fbx_layer_data, None,
            fbx_layer_mapping, fbx_layer_ref,
    
            1, 1, layer_id,
    
    def blen_read_geom_layer_uv(fbx_obj, mesh):
        for layer_id in (b'LayerElementUV',):
            for fbx_layer in elem_find_iter(fbx_obj, layer_id):
                # all should be valid
                (fbx_layer_name,
                 fbx_layer_mapping,
                 fbx_layer_ref,
                 ) = blen_read_geom_layerinfo(fbx_layer)
    
                fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'UV'))
                fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'UVIndex'))
    
                uv_tex = mesh.uv_textures.new(name=fbx_layer_name)
    
                uv_lay = mesh.uv_layers[-1]
    
                # some valid files omit this data
    
                if fbx_layer_data is None:
    
                    print("%r %r missing data" % (layer_id, fbx_layer_name))
                    continue
    
    
                blen_read_geom_array_mapped_polyloop(
                    mesh, blen_data, "uv",
                    fbx_layer_data, fbx_layer_index,
                    fbx_layer_mapping, fbx_layer_ref,
    
                    2, 2, layer_id,
    
    def blen_read_geom_layer_color(fbx_obj, mesh):
        # almost same as UV's
        for layer_id in (b'LayerElementColor',):