Skip to content
Snippets Groups Projects
import_fbx.py 136 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
    
    
    # Units converters...
    
    Bastien Montagne's avatar
    Bastien Montagne committed
    convert_deg_to_rad_iter = units_convertor_iter("degree", "radian")
    
    MAT_CONVERT_BONE = fbx_utils.MAT_CONVERT_BONE.inverted()
    
    MAT_CONVERT_LIGHT = fbx_utils.MAT_CONVERT_LIGHT.inverted()
    
    MAT_CONVERT_CAMERA = fbx_utils.MAT_CONVERT_CAMERA.inverted()
    
    def validate_blend_names(name):
        assert(type(name) == bytes)
        # Blender typically does not accept names over 63 bytes...
        if len(name) > 63:
            import hashlib
            h = hashlib.sha1(name).hexdigest()
    
            n = 55
            name_utf8 = name[:n].decode('utf-8', 'replace') + "_" + h[:7]
            while len(name_utf8.encode()) > 63:
                n -= 1
                name_utf8 = name[:n].decode('utf-8', 'replace') + "_" + h[:7]
            return name_utf8
    
        else:
            # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
            return name.decode('utf-8', 'replace')
    
    
    
    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', 'replace')
    
    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 validate_blend_names(elem_name)
    
    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 validate_blend_names(elem_name)
    
    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)
    
            # b'Bool' with a capital seems to be used for animated property... go figure...
            assert(elem_prop.props[1] in {b'bool', b'Bool'})
    
            assert(elem_prop.props[2] == 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', 'replace')
    
                        for item in items.split('\r\n'):
                            if item:
    
                                split_item = item.split('=', 1)
                                if len(split_item) != 2:
                                    split_item = item.split(':', 1)
                                if len(split_item) != 2:
                                    print("cannot parse UDP3DSMAX custom property '%s', ignoring..." % item)
                                else:
                                    prop_name, prop_value = split_item
                                    prop_name = validate_blend_names(prop_name.strip().encode('utf-8'))
                                    blen_obj[prop_name] = prop_value.strip()
    
                        prop_name = validate_blend_names(fbx_prop.props[0])
    
                        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', 'replace')
    
                        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', 'replace').split('~')
    
                                if val >= 0 and val < len(enum_items):
                                    blen_obj[prop_name] = enum_items[val]
                                else:
                                    print ("WARNING: User property '%s' has wrong enum value, skipped" % prop_name)
    
                            else:
                                blen_obj[prop_name] = val
                        else:
    
                            print ("WARNING: User property type '%s' is not supported" % prop_type.decode('utf-8', 'replace'))
    
    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
    
            lcl_translation @
            rot_ofs @
            rot_piv @
            pre_rot @
            lcl_rot @
            pst_rot @
            rot_piv.inverted_safe() @
            sca_ofs @
            sca_piv @
            lcl_scale @
    
        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(name=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, Camera
    
        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 = []
    
        keyframes = {}
    
        # Add each keyframe to the keyframe dict
        def store_keyframe(fc, frame, value):
            fc_key = (fc.data_path, fc.array_index)
            if not keyframes.get(fc_key):
                keyframes[fc_key] = []
    
            keyframes[fc_key].extend((frame, value))
    
        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")]
    
        elif isinstance(item, Camera):
            props = [(item.path_from_id("lens"), 1, "Camera")]
    
        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, index=channel, action_group=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):
    
                    store_keyframe(fc, frame, v)
    
            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,)):
    
                    store_keyframe(fc, frame, v)
    
        elif isinstance(item, Camera):
            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'FocalLength')
                    assert(channel == 0)
                    value = v
    
                for fc, v in zip(blen_curves, (value,)):
    
                    store_keyframe(fc, frame, v)
    
        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_eul_prev = bl_obj.rotation_euler.copy()
            rot_quat_prev = bl_obj.rotation_quaternion.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
    
                    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':
    
                    if rot_quat_prev.dot(rot) < 0.0:
                        rot = -rot
                    rot_quat_prev = rot
    
                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_eul_prev)
                    rot_eul_prev = rot
    
    
                # Add each keyframe and its value to the keyframe dict
    
                for fc, value in zip(blen_curves, chain(loc, rot, sca)):
    
                    store_keyframe(fc, frame, value)
    
        # Add all keyframe points to the fcurves at once and modify them after
        for fc_key, key_values in keyframes.items():
            data_path, index = fc_key
    
            # Add all keyframe points at once
            fcurve = action.fcurves.find(data_path=data_path, index=index)
    
            num_keys = len(key_values) // 2
    
            fcurve.keyframe_points.add(num_keys)
    
            fcurve.keyframe_points.foreach_set('co', key_values)
            linear_enum_value = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value
            fcurve.keyframe_points.foreach_set('interpolation', (linear_enum_value,) * num_keys)
    
        # Since we inserted our keyframes in 'ultra-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, Camera
    
        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):
    
                    elif isinstance(item, Camera):
                        id_data = item
    
                        # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
                        #     FbxImportHelperNode class definition.
    
                        if id_data and id_data.type == 'MESH' and id_data.parent and id_data.parent.type == 'ARMATURE':
    
                    # Create new action if needed (should always be needed, except for keyblocks from shapekeys cases).
    
                    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 (
    
            validate_blend_names(elem_find_first_string_as_bytes(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':