Skip to content
Snippets Groups Projects
import_fbx.py 136 KiB
Newer Older
  • Learn to ignore specific revisions
  •                 for child_bone in connected[1]:
                        if similar_values_iter(par_tail, child_bone.head):
                            child_bone.use_connect = True
    
            # Create the (edit)bone.
    
            bone = arm.bl_data.edit_bones.new(name=self.fbx_name)
            bone.select = True
            self.bl_obj = arm.bl_obj
            self.bl_data = arm.bl_data
            self.bl_bone = bone.name  # Could be different from the FBX name!
    
            # get average distance to children
            bone_size = 0.0
            bone_count = 0
            for child in self.children:
                if child.is_bone:
    
                    bone_size += child.get_bind_matrix().to_translation().magnitude
    
                    bone_count += 1
            if bone_count > 0:
                bone_size /= bone_count
            else:
                bone_size = parent_bone_size
    
            # So that our bone gets its final length, but still Y-aligned in armature space.
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # 0-length bones are automatically collapsed into their parent when you leave edit mode,
            # so this enforces a minimum length.
    
            bone_tail = Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size)
            bone.tail = bone_tail
    
            # And rotate/move it to its final "rest pose".
    
            bone_matrix = parent_matrix @ self.get_bind_matrix().normalized()
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
            # while Blender attaches to the tail.
    
            self.bone_child_matrix = Matrix.Translation(-bone_tail)
    
    
            connect_ctx = [force_connect_children, ...]
    
                if child.is_leaf and force_connect_children:
                    # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
                    # to orient current one!!!
    
                    child_head = (bone_matrix @ child.get_bind_matrix().normalized()).translation
    
                    child_connect(bone, None, child_head, connect_ctx)
                elif child.is_bone and not child.ignore:
    
                    child_bone = child.build_skeleton(arm, bone_matrix, bone_size,
                                                      force_connect_children=force_connect_children)
    
                    child_connect(bone, child_bone, None, connect_ctx)
    
        def build_node_obj(self, fbx_tmpl, settings):
    
            if self.bl_obj:
                return self.bl_obj
    
            if self.is_bone or not self.fbx_elem:
                return None
    
    
            # create when linking since we need object data
            elem_name_utf8 = self.fbx_name
    
            # Object data must be created already
            self.bl_obj = obj = bpy.data.objects.new(name=elem_name_utf8, object_data=self.bl_data)
    
            fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
                         elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
            # ----
            # Misc Attributes
    
            obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8))
    
            obj.hide_viewport = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0))
    
    
            obj.matrix_basis = self.get_matrix()
    
            if settings.use_custom_props:
    
                blen_read_custom_properties(self.fbx_elem, obj, settings)
    
        def build_skeleton_children(self, fbx_tmpl, settings, scene, view_layer):
    
            if self.is_bone:
                for child in self.children:
                    if child.ignore:
                        continue
    
                    child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
    
                obj = self.build_node_obj(fbx_tmpl, settings)
    
                for child in self.children:
                    if child.ignore:
                        continue
    
                    child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
    
                # instance in scene
                view_layer.active_layer_collection.collection.objects.link(obj)
                obj.select_set(True)
    
    
                return obj
    
        def link_skeleton_children(self, fbx_tmpl, settings, scene):
            if self.is_bone:
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.bl_obj
    
                    if child_obj and child_obj != self.bl_obj:
    
                        child_obj.parent = self.bl_obj  # get the armature the bone belongs to
                        child_obj.parent_bone = self.bl_bone
                        child_obj.parent_type = 'BONE'
    
                        child_obj.matrix_parent_inverse = Matrix()
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                        # Blender attaches to the end of a bone, while FBX attaches to the start.
                        # bone_child_matrix corrects for that.
    
                            child.pre_matrix = self.bone_child_matrix @ child.pre_matrix
    
                        else:
                            child.pre_matrix = self.bone_child_matrix
    
                        child_obj.matrix_basis = child.get_matrix()
    
                    child.link_skeleton_children(fbx_tmpl, settings, scene)
    
                    child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
    
                    if child_obj:
                        child_obj.parent = obj
    
                return obj
    
        def set_pose_matrix(self, arm):
            pose_bone = arm.bl_obj.pose.bones[self.bl_bone]
    
            pose_bone.matrix_basis = self.get_bind_matrix().inverted_safe() @ self.get_matrix()
    
    
            for child in self.children:
                if child.ignore:
                    continue
                if child.is_bone:
                    child.set_pose_matrix(arm)
    
        def merge_weights(self, combined_weights, fbx_cluster):
            indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
            weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
    
            for index, weight in zip(indices, weights):
                w = combined_weights.get(index)
                if w is None:
                    combined_weights[index] = [weight]
                else:
                    w.append(weight)
    
        def set_bone_weights(self):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            ignored_children = tuple(child for child in self.children
                                           if child.is_bone and child.ignore and len(child.clusters) > 0)
    
    
            if len(ignored_children) > 0:
                # If we have an ignored child bone we need to merge their weights into the current bone weights.
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                # This can happen both intentionally and accidentally when skinning a model. Either way, they
                # need to be moved into a parent bone or they cause animation glitches.
    
                for fbx_cluster, meshes in self.clusters:
                    combined_weights = {}
                    self.merge_weights(combined_weights, fbx_cluster)
    
                    for child in ignored_children:
                        for child_cluster, child_meshes in child.clusters:
                            if not meshes.isdisjoint(child_meshes):
                                self.merge_weights(combined_weights, child_cluster)
    
                    # combine child weights
                    indices = []
                    weights = []
                    for i, w in combined_weights.items():
                        indices.append(i)
                        if len(w) > 1:
                            weights.append(sum(w) / len(w))
                        else:
                            weights.append(w[0])
    
                    add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
    
                # clusters that drive meshes not included in a parent don't need to be merged
                all_meshes = set().union(*[meshes for _, meshes in self.clusters])
                for child in ignored_children:
                    for child_cluster, child_meshes in child.clusters:
                        if all_meshes.isdisjoint(child_meshes):
                            indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=())
                            weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=())
                            add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes])
            else:
                # set the vertex weights on meshes
                for fbx_cluster, meshes in self.clusters:
                    indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
                    weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
                    add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
    
            for child in self.children:
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                if child.is_bone and not child.ignore:
    
        def build_hierarchy(self, fbx_tmpl, settings, scene, view_layer):
    
            if self.is_armature:
                # create when linking since we need object data
                elem_name_utf8 = self.fbx_name
    
                self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8)
    
                # Object data must be created already
                self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data)
    
                arm.matrix_basis = self.get_matrix()
    
                if self.fbx_elem:
                    fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
                                 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
                    if settings.use_custom_props:
    
                        blen_read_custom_properties(self.fbx_elem, arm, settings)
    
                view_layer.active_layer_collection.collection.objects.link(arm)
    
                arm.select_set(True)
    
                view_layer.objects.active = arm
    
                is_hidden = arm.hide_viewport
                arm.hide_viewport = False  # Can't switch to Edit mode hidden objects...
    
                bpy.ops.object.mode_set(mode='EDIT')
    
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
    
                        child.build_skeleton(self, Matrix(), force_connect_children=settings.force_connect_children)
    
    
                # Set pose matrix
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
                        child.set_pose_matrix(self)
    
                # Add bone children:
                for child in self.children:
                    if child.ignore:
                        continue
    
                    child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
    
                obj = self.build_node_obj(fbx_tmpl, settings)
    
    
                # walk through children
                for child in self.children:
    
                    child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
    
                # instance in scene
                view_layer.active_layer_collection.collection.objects.link(obj)
                obj.select_set(True)
    
    
                    child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
    
    
                return None
    
        def link_hierarchy(self, fbx_tmpl, settings, scene):
            if self.is_armature:
                arm = self.bl_obj
    
                # Link bone children:
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
    
                    if child_obj:
                        child_obj.parent = arm
    
                # Add armature modifiers to the meshes
                if self.meshes:
                    for mesh in self.meshes:
    
                        (mmat, amat) = mesh.armature_setup[self]
    
    
                        # bring global armature & mesh matrices into *Blender* global space.
    
                        # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
                        #       Among other things, why in hell isn't it taken into account by bindpose & co???
                        #       Probably because org app (max) handles it completely aside from any parenting stuff,
                        #       which we obviously cannot do in Blender. :/
    
                        amat = settings.global_matrix @ (Matrix() if amat is None else amat)
    
                            amat = amat @ self.matrix_geom
                        mmat = settings.global_matrix @ mmat
    
                            mmat = mmat @ mesh.matrix_geom
    
                        # Now that we have armature and mesh in there (global) bind 'state' (matrix),
                        # we can compute inverse parenting matrix of the mesh.
    
                        me_obj.matrix_parent_inverse = amat.inverted_safe() @ mmat @ me_obj.matrix_basis.inverted_safe()
    
                        mod = mesh.bl_obj.modifiers.new(arm.name, 'ARMATURE')
    
                        mod.object = arm
    
                # Add bone weights to the deformers
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
                        child.set_bone_weights()
    
                return arm
    
                    child_obj = child.link_hierarchy(fbx_tmpl, settings, scene)
    
                    child.link_hierarchy(fbx_tmpl, settings, scene)
    
                return None
    
    def load(operator, context, filepath="",
    
             use_manual_orientation=False,
             axis_forward='-Z',
             axis_up='Y',
             global_scale=1.0,
    
             use_image_search=False,
             use_alpha_decals=False,
    
             use_custom_props=True,
    
             use_custom_props_enum_as_string=True,
             ignore_leaf_bones=False,
    
             automatic_bone_orientation=False,
             primary_bone_axis='Y',
    
        global fbx_elem_nil
        fbx_elem_nil = FBXElem('', (), (), ())
    
    
        import os
        import time
    
        from bpy_extras.io_utils import axis_conversion
    
    
        from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
    
        start_time_proc = time.process_time()
        start_time_sys = time.time()
    
        perfmon = PerfMon()
        perfmon.level_up()
        perfmon.step("FBX Import: start importing %s" % filepath)
        perfmon.level_up()
    
    
        # Detect ASCII files.
    
        # Typically it's bad practice to fail silently on any error,
        # however the file may fail to read for many reasons,
        # and this situation is handled later in the code,
        # right now we only want to know if the file successfully reads as ascii.
        try:
            with open(filepath, 'r', encoding="utf-8") as fh:
                fh.read(24)
            is_ascii = True
        except Exception:
            is_ascii = False
    
        if is_ascii:
    
            operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
            return {'CANCELLED'}
    
        del is_ascii
        # End ascii detection.
    
        try:
            elem_root, version = parse_fbx.parse(filepath)
    
            import traceback
            traceback.print_exc()
    
    
            operator.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath, e))
    
            return {'CANCELLED'}
    
        if version < 7100:
            operator.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version, 7100))
            return {'CANCELLED'}
    
    
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
    
    
        # deselect all
        if bpy.ops.object.select_all.poll():
            bpy.ops.object.select_all(action='DESELECT')
    
        basedir = os.path.dirname(filepath)
    
    
        nodal_material_wrap_map = {}
    
        image_cache = {}
    
        # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
        fbx_table_nodes = {}
    
    
        if use_alpha_decals:
            material_decals = set()
        else:
            material_decals = None
    
    
        view_layer = context.view_layer
    
        # #### Get some info from GlobalSettings.
    
        fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
        fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
        if fbx_settings is None or fbx_settings_props is None:
            operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
            return {'CANCELLED'}
    
    
        # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
    
        unit_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 1.0)
        unit_scale_org = elem_props_get_number(fbx_settings_props, b'OriginalUnitScaleFactor', 1.0)
    
        global_scale *= (unit_scale / units_blender_to_fbx_factor(context.scene))
    
        # Compute global matrix and scale.
        if not use_manual_orientation:
            axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
                            elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
            axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
                       elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
            axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
                          elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
    
            axis_key = (axis_up, axis_forward, axis_coord)
            axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
    
        global_matrix = (Matrix.Scale(global_scale, 4) @
    
                         axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
    
    
        # To cancel out unwanted rotation/scale on nodes.
        global_matrix_inv = global_matrix.inverted()
        # For transforming mesh normals.
        global_matrix_inv_transposed = global_matrix_inv.transposed()
    
        # Compute bone correction matrix
        bone_correction_matrix = None  # None means no correction/identity
        if not automatic_bone_orientation:
            if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
                bone_correction_matrix = axis_conversion(from_forward='X',
                                                         from_up='Y',
                                                         to_forward=secondary_bone_axis,
                                                         to_up=primary_bone_axis,
                                                         ).to_4x4()
    
    
        # Compute framerate settings.
        custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
        time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
        real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
    
            real_fps = 25.0
        scene.render.fps = round(real_fps)
        scene.render.fps_base = scene.render.fps / real_fps
    
    
        # store global settings that need to be accessed during conversion
        settings = FBXImportSettings(
            operator.report, (axis_up, axis_forward), global_matrix, global_scale,
    
            bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
    
            use_custom_normals, use_image_search,
    
            use_custom_props, use_custom_props_enum_as_string,
    
            nodal_material_wrap_map, image_cache,
    
            ignore_leaf_bones, force_connect_children, automatic_bone_orientation, bone_correction_matrix,
    
        # #### And now, the "real" data.
    
        fbx_defs = elem_find_first(elem_root, b'Definitions')  # can be None
    
        fbx_nodes = elem_find_first(elem_root, b'Objects')
        fbx_connections = elem_find_first(elem_root, b'Connections')
    
        if fbx_nodes is None:
    
            operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
            return {'CANCELLED'}
    
        if fbx_connections is None:
    
            operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
            return {'CANCELLED'}
    
        # ----
        # First load property templates
        # Load 'PropertyTemplate' values.
        # Key is a tuple, (ObjectType, FBXNodeType)
        # eg, (b'Texture', b'KFbxFileTexture')
        #     (b'Geometry', b'KFbxMesh')
        fbx_templates = {}
    
        def _():
            if fbx_defs is not None:
                for fbx_def in fbx_defs.elems:
                    if fbx_def.id == b'ObjectType':
                        for fbx_subdef in fbx_def.elems:
                            if fbx_subdef.id == b'PropertyTemplate':
                                assert(fbx_def.props_type == b'S')
                                assert(fbx_subdef.props_type == b'S')
                                # (b'Texture', b'KFbxFileTexture') - eg.
                                key = fbx_def.props[0], fbx_subdef.props[0]
                                fbx_templates[key] = fbx_subdef
        _(); del _
    
        def fbx_template_get(key):
    
                # Newest FBX (7.4 and above) use no more 'K' in their type names...
                key = (key[0], key[1][1:])
                return fbx_templates.get(key, fbx_elem_nil)
            return ret
    
        def _():
            for fbx_obj in fbx_nodes.elems:
    
                # TODO, investigate what other items after first 3 may be
                assert(fbx_obj.props_type[:3] == b'LSS')
    
                fbx_uuid = elem_uuid(fbx_obj)
                fbx_table_nodes[fbx_uuid] = [fbx_obj, None]
        _(); del _
    
        # ----
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
        #        WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
    
        fbx_connection_map = {}
        fbx_connection_map_reverse = {}
    
        def _():
            for fbx_link in fbx_connections.elems:
                c_type = fbx_link.props[0]
    
                if fbx_link.props_type[1:3] == b'LL':
                    c_src, c_dst = fbx_link.props[1:3]
                    fbx_connection_map.setdefault(c_src, []).append((c_dst, fbx_link))
                    fbx_connection_map_reverse.setdefault(c_dst, []).append((c_src, fbx_link))
    
            fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxMesh'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Geometry':
                    continue
                if fbx_obj.props[-1] == b'Mesh':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_geom(fbx_tmpl, fbx_obj, settings)
    
        # ----
        # Load material data
        def _():
    
            fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
            # b'KFbxSurfaceLambert'
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Material':
                    continue
                assert(blen_data is None)
    
                fbx_item[1] = blen_read_material(fbx_tmpl, fbx_obj, settings)
    
            fbx_tmpl_tex = fbx_template_get((b'Texture', b'KFbxFileTexture'))
            fbx_tmpl_img = fbx_template_get((b'Video', b'KFbxVideo'))
    
            # Important to run all 'Video' ones first, embedded images are stored in those nodes.
            # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
            #     this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Video':
                    continue
                fbx_item[1] = blen_read_texture_image(fbx_tmpl_img, fbx_obj, basedir, settings)
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Texture':
                    continue
    
                fbx_item[1] = blen_read_texture_image(fbx_tmpl_tex, fbx_obj, basedir, settings)
    
        # ----
        # Load camera data
        def _():
    
            fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxCamera'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'NodeAttribute':
                    continue
                if fbx_obj.props[-1] == b'Camera':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_camera(fbx_tmpl, fbx_obj, global_scale)
    
        _(); del _
    
        # ----
        # Load lamp data
        def _():
    
            fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxLight'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'NodeAttribute':
                    continue
                if fbx_obj.props[-1] == b'Light':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_light(fbx_tmpl, fbx_obj, global_scale)
    
        # ----
        # Connections
        def connection_filter_ex(fbx_uuid, fbx_id, dct):
            return [(c_found[0], c_found[1], c_type)
                    for (c_uuid, c_type) in dct.get(fbx_uuid, ())
    
                    # 0 is used for the root node, which isnt in fbx_table_nodes
    
                    for c_found in (() if c_uuid == 0 else (fbx_table_nodes.get(c_uuid, (None, None)),))
    
                    if (fbx_id is None) or (c_found[0] and c_found[0].id == fbx_id)]
    
    
        def connection_filter_forward(fbx_uuid, fbx_id):
            return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map)
    
        def connection_filter_reverse(fbx_uuid, fbx_id):
            return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
    
    
        # -- temporary helper hierarchy to build armatures and objects from
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
        fbx_helper_nodes = {}
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # We build an intermediate hierarchy used to:
            # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
            # - Find/insert armature nodes.
            # - Filter leaf bones.
    
    
            # create scene root
            fbx_helper_nodes[0] = root_helper = FbxImportHelperNode(None, None, None, False)
            root_helper.is_root = True
    
            # add fbx nodes
            fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
    
            for a_uuid, a_item in fbx_table_nodes.items():
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                fbx_obj, bl_data = a_item
    
                if fbx_obj is None or fbx_obj.id != b'Model':
    
    
                fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                             elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
    
                transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, Matrix(), use_prepost_rot)
    
                # Note: 'Root' "bones" are handled as (armature) objects.
                # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
                is_bone = fbx_obj.props[2] in {b'LimbNode', b'Limb'}
    
                fbx_helper_nodes[a_uuid] = FbxImportHelperNode(fbx_obj, bl_data, transform_data, is_bone)
    
            # add parent-child relations and add blender data to the node
            for fbx_link in fbx_connections.elems:
                if fbx_link.props[0] != b'OO':
                    continue
                if fbx_link.props_type[1:3] == b'LL':
                    c_src, c_dst = fbx_link.props[1:3]
                    parent = fbx_helper_nodes.get(c_dst)
                    if parent is None:
    
    
                    child = fbx_helper_nodes.get(c_src)
                    if child is None:
                        # add blender data (meshes, lights, cameras, etc.) to a helper node
                        fbx_sdata, bl_data = p_item = fbx_table_nodes.get(c_src, (None, None))
                        if fbx_sdata is None:
    
                        if fbx_sdata.id not in {b'Geometry', b'NodeAttribute'}:
    
                        parent.bl_data = bl_data
                    else:
                        # set parent
                        child.parent = parent
    
            # find armatures (either an empty below a bone or a new node inserted at the bone
            root_helper.find_armatures()
    
            # mark nodes that have bone children
            root_helper.find_bone_children()
    
            # mark nodes that need a bone to attach child-bones to
            root_helper.find_fake_bones()
    
            # mark leaf nodes that are only required to mark the end of their parent bone
            if settings.ignore_leaf_bones:
                root_helper.mark_leaf_bones()
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
            # and you can have several clusters per bone!
    
            # Maybe some conversion can be applied to put them all into the same frame of reference?
    
            # get the bind pose from pose elements
            for a_uuid, a_item in fbx_table_nodes.items():
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                fbx_obj, bl_data = a_item
    
                if fbx_obj.id != b'Pose':
                    continue
                if fbx_obj.props[2] != b'BindPose':
                    continue
                for fbx_pose_node in fbx_obj.elems:
                    if fbx_pose_node.id != b'PoseNode':
                        continue
                    node_elem = elem_find_first(fbx_pose_node, b'Node')
                    node = elem_uuid(node_elem)
                    matrix_elem = elem_find_first(fbx_pose_node, b'Matrix')
                    matrix = array_to_matrix4(matrix_elem.props[0]) if matrix_elem else None
                    bone = fbx_helper_nodes.get(node)
                    if bone and matrix:
                        # Store the matrix in the helper node.
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                        # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
    
                        bone.bind_matrix = matrix  # global space
    
            # get clusters and bind pose
            for helper_uuid, helper_node in fbx_helper_nodes.items():
                if not helper_node.is_bone:
    
                for cluster_uuid, cluster_link in fbx_connection_map.get(helper_uuid, ()):
                    if cluster_link.props[0] != b'OO':
                        continue
                    fbx_cluster, _ = fbx_table_nodes.get(cluster_uuid, (None, None))
                    if fbx_cluster is None or fbx_cluster.id != b'Deformer' or fbx_cluster.props[2] != b'Cluster':
                        continue
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    tx_mesh_elem = elem_find_first(fbx_cluster, b'Transform', default=None)
                    tx_mesh = array_to_matrix4(tx_mesh_elem.props[0]) if tx_mesh_elem else Matrix()
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    tx_bone_elem = elem_find_first(fbx_cluster, b'TransformLink', default=None)
                    tx_bone = array_to_matrix4(tx_bone_elem.props[0]) if tx_bone_elem else None
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    tx_arm_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None)
    
                    tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else None
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    mesh_matrix = tx_mesh
                    armature_matrix = tx_arm
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    if tx_bone:
    
                        mesh_matrix = tx_bone @ mesh_matrix
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                        helper_node.bind_matrix = tx_bone  # overwrite the bind matrix
    
    
                    # Get the meshes driven by this cluster: (Shouldn't that be only one?)
                    meshes = set()
                    for skin_uuid, skin_link in fbx_connection_map.get(cluster_uuid):
                        if skin_link.props[0] != b'OO':
    
                            continue
    
                        fbx_skin, _ = fbx_table_nodes.get(skin_uuid, (None, None))
                        if fbx_skin is None or fbx_skin.id != b'Deformer' or fbx_skin.props[2] != b'Skin':
    
                        for mesh_uuid, mesh_link in fbx_connection_map.get(skin_uuid):
                            if mesh_link.props[0] != b'OO':
                                continue
                            fbx_mesh, _ = fbx_table_nodes.get(mesh_uuid, (None, None))
                            if fbx_mesh is None or fbx_mesh.id != b'Geometry' or fbx_mesh.props[2] != b'Mesh':
                                continue
                            for object_uuid, object_link in fbx_connection_map.get(mesh_uuid):
                                if object_link.props[0] != b'OO':
                                    continue
                                mesh_node = fbx_helper_nodes[object_uuid]
                                if mesh_node:
                                    # ----
    
                                    # If we get a valid mesh matrix (in bone space), store armature and
                                    # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
                                    # when actually binding them via the modifier.
                                    # Note we assume all bones were bound with the same mesh/armature (global) matrix,
                                    # we do not support otherwise in Blender anyway!
    
                                    mesh_node.armature_setup[helper_node.armature] = (mesh_matrix, armature_matrix)
    
                                    meshes.add(mesh_node)
    
                    helper_node.clusters.append((fbx_cluster, meshes))
    
            # convert bind poses from global space into local space
            root_helper.make_bind_pose_local()
    
            # collect armature meshes
            root_helper.collect_armature_meshes()
    
            # find the correction matrices to align FBX objects with their Blender equivalent
            root_helper.find_correction_matrix(settings)
    
            root_helper.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
    
            # Link the Object/Armature/Bone hierarchy
            root_helper.link_hierarchy(fbx_tmpl, settings, scene)
    
    
        blend_shape_channels = {}  # We do not need Shapes themselves, but keyblocks, for anim.
    
        def _():
            fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
    
            for s_uuid, s_item in fbx_table_nodes.items():
                fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
                if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
                    continue
    
                # shape -> blendshapechannel -> blendshape -> mesh.
                for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()):
                    if bc_ctype.props[0] != b'OO':
                        continue
                    fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
                    if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
                        continue
                    meshes = []
                    objects = []
                    for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
                        if bs_ctype.props[0] != b'OO':
                            continue
                        fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None))
                        if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape':
                            continue
                        for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()):
                            if m_ctype.props[0] != b'OO':
                                continue
                            fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
                            if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
                                continue
                            # Blenmeshes are assumed already created at that time!
                            assert(isinstance(bl_mdata, bpy.types.Mesh))
                            # And we have to find all objects using this mesh!
                            objects = []
                            for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
                                if o_ctype.props[0] != b'OO':
                                    continue
    
                                node = fbx_helper_nodes[o_uuid]
                                if node:
                                    objects.append(node)
    
                            meshes.append((bl_mdata, objects))
                        # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
    
                    # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
    
                    keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene)
    
                    blend_shape_channels[bc_uuid] = keyblocks
        _(); del _
    
    
        if settings.use_subsurf:
            perfmon.step("FBX import: Subdivision surfaces")
    
            # Look through connections for subsurf in meshes and add it to the parent object
            def _():
                for fbx_link in fbx_connections.elems:
                    if fbx_link.props[0] != b'OO':
                        continue
                    if fbx_link.props_type[1:3] == b'LL':
                        c_src, c_dst = fbx_link.props[1:3]
                        parent = fbx_helper_nodes.get(c_dst)
                        if parent is None:
                            continue
    
                        child = fbx_helper_nodes.get(c_src)
                        if child is None:
                            fbx_sdata, bl_data = fbx_table_nodes.get(c_src, (None, None))
                            if fbx_sdata.id != b'Geometry':
                                continue
    
                            preview_levels = elem_prop_first(elem_find_first(fbx_sdata, b'PreviewDivisionLevels'))
                            render_levels = elem_prop_first(elem_find_first(fbx_sdata, b'RenderDivisionLevels'))
                            if isinstance(preview_levels, int) and isinstance(render_levels, int):
                                mod = parent.bl_obj.modifiers.new('subsurf', 'SUBSURF')
                                mod.levels = preview_levels
                                mod.render_levels = render_levels
    
                                boundary_rule = elem_prop_first(elem_find_first(fbx_sdata, b'BoundaryRule'), default=1)
                                if boundary_rule == 2:
                                    mod.boundary_smooth = "PRESERVE_CORNERS"
                                else:
                                    mod.boundary_smooth = "ALL"
    
        if use_anim:
            perfmon.step("FBX import: Animations...")
    
            # Animation!
            def _():
                fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
                fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
                stacks = {}
    
                # AnimationStacks.
                for as_uuid, fbx_asitem in fbx_table_nodes.items():
                    fbx_asdata, _blen_data = fbx_asitem
                    if fbx_asdata.id != b'AnimationStack' or fbx_asdata.props[2] != b'':
    
                    stacks[as_uuid] = (fbx_asitem, {})
    
                # AnimationLayers
                # (mixing is completely ignored for now, each layer results in an independent set of actions).
                def get_astacks_from_alayer(al_uuid):
                    for as_uuid, as_ctype in fbx_connection_map.get(al_uuid, ()):
                        if as_ctype.props[0] != b'OO':
    
                        fbx_asdata, _bl_asdata = fbx_table_nodes.get(as_uuid, (None, None))
                        if (fbx_asdata is None or fbx_asdata.id != b'AnimationStack' or
                            fbx_asdata.props[2] != b'' or as_uuid not in stacks):
    
                        yield as_uuid
                for al_uuid, fbx_alitem in fbx_table_nodes.items():
                    fbx_aldata, _blen_data = fbx_alitem
                    if fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
    
                        continue
                    for as_uuid in get_astacks_from_alayer(al_uuid):
    
                        _fbx_asitem, alayers = stacks[as_uuid]
                        alayers[al_uuid] = (fbx_alitem, {})
    
                # AnimationCurveNodes (also the ones linked to actual animated data!).
                curvenodes = {}
                for acn_uuid, fbx_acnitem in fbx_table_nodes.items():
                    fbx_acndata, _blen_data = fbx_acnitem
                    if fbx_acndata.id != b'AnimationCurveNode' or fbx_acndata.props[2] != b'':
    
                    cnode = curvenodes[acn_uuid] = {}
                    items = []
                    for n_uuid, n_ctype in fbx_connection_map.get(acn_uuid, ()):
                        if n_ctype.props[0] != b'OP':
                            continue
                        lnk_prop = n_ctype.props[3]
                        if lnk_prop in {b'Lcl Translation', b'Lcl Rotation', b'Lcl Scaling'}:
                            # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
                            ob = fbx_helper_nodes.get(n_uuid, None)
    
                                continue
                            items.append((ob, lnk_prop))
                        elif lnk_prop == b'DeformPercent':  # Shape keys.
    
                            keyblocks = blend_shape_channels.get(n_uuid, None)
    
                            if keyblocks is None:
                                continue
                            items += [(kb, lnk_prop) for kb in keyblocks]
    
                        elif lnk_prop == b'FocalLength':  # Camera lens.
                            from bpy.types import Camera
                            fbx_item = fbx_table_nodes.get(n_uuid, None)