Skip to content
Snippets Groups Projects
import_fbx.py 131 KiB
Newer Older
  • Learn to ignore specific revisions
  •             # ignore alpha layer (read 4 items into 3)
                blen_read_geom_array_mapped_polyloop(
                    mesh, blen_data, "color",
                    fbx_layer_data, fbx_layer_index,
                    fbx_layer_mapping, fbx_layer_ref,
                    4, 3, layer_id,
                    )
    
    
    def blen_read_geom_layer_smooth(fbx_obj, mesh):
        fbx_layer = elem_find_first(fbx_obj, b'LayerElementSmoothing')
    
        if fbx_layer is None:
            return False
    
        # all should be valid
        (fbx_layer_name,
         fbx_layer_mapping,
         fbx_layer_ref,
         ) = blen_read_geom_layerinfo(fbx_layer)
    
        layer_id = b'Smoothing'
        fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
    
    
        # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
        if fbx_layer_data is None:
            return False
    
    
        if fbx_layer_mapping == b'ByEdge':
    
            # some models have bad edge data, we cant use this info...
            if not mesh.edges:
    
                print("warning skipping sharp edges data, no valid edges...")
    
            blen_data = mesh.edges
    
                mesh, blen_data, "use_edge_sharp",
                fbx_layer_data, None,
                fbx_layer_mapping, fbx_layer_ref,
    
                1, 1, layer_id,
    
            # We only set sharp edges here, not face smoothing itself...
            mesh.use_auto_smooth = True
            mesh.show_edge_sharp = True
            return False
    
        elif fbx_layer_mapping == b'ByPolygon':
            blen_data = mesh.polygons
            return blen_read_geom_array_mapped_polygon(
                mesh, blen_data, "use_smooth",
                fbx_layer_data, None,
                fbx_layer_mapping, fbx_layer_ref,
    
                1, 1, layer_id,
    
                xform=lambda s: (s != 0),  # smoothgroup bitflags, treat as booleans for now
    
                )
        else:
            print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping))
            return False
    
    
    
    def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
    
        fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal')
    
        if fbx_layer is None:
            return False
    
        (fbx_layer_name,
         fbx_layer_mapping,
         fbx_layer_ref,
         ) = blen_read_geom_layerinfo(fbx_layer)
    
        layer_id = b'Normals'
        fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
    
        fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'NormalsIndex'))
    
        # try loops, then vertices.
    
        tries = ((mesh.loops, "Loops", False, blen_read_geom_array_mapped_polyloop),
                 (mesh.polygons, "Polygons", True, blen_read_geom_array_mapped_polygon),
                 (mesh.vertices, "Vertices", True, blen_read_geom_array_mapped_vert))
        for blen_data, blen_data_type, is_fake, func in tries:
    
            bdata = [None] * len(blen_data) if is_fake else blen_data
            if func(mesh, bdata, "normal",
    
                    fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True):
                if blen_data_type is "Polygons":
    
                    for pidx, p in enumerate(mesh.polygons):
                        for lidx in range(p.loop_start, p.loop_start + p.loop_total):
                            mesh.loops[lidx].normal[:] = bdata[pidx]
    
                elif blen_data_type is "Vertices":
    
                    # We have to copy vnors to lnors! Far from elegant, but simple.
                    for l in mesh.loops:
    
                        l.normal[:] = bdata[l.vertex_index]
    
    
        blen_read_geom_array_error_mapping("normal", fbx_layer_mapping)
        blen_read_geom_array_error_ref("normal", fbx_layer_ref)
    
    def blen_read_geom(fbx_tmpl, fbx_obj, settings):
    
        from itertools import chain
        import array
    
        # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
        # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
        geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
        # We need to apply the inverse transpose of the global matrix when transforming normals.
        geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None
        if geom_mat_no is not None:
            # Remove translation & scaling!
            geom_mat_no.translation = Vector()
            geom_mat_no.normalize()
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry')
    
    
        fbx_verts = elem_prop_first(elem_find_first(fbx_obj, b'Vertices'))
        fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex'))
    
        fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges'))
    
        if geom_mat_co is not None:
            def _vcos_transformed_gen(raw_cos, m=None):
                # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
                return chain(*(m * Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
            fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co))
    
    
        if fbx_verts is None:
            fbx_verts = ()
        if fbx_polys is None:
            fbx_polys = ()
    
    
        mesh = bpy.data.meshes.new(name=elem_name_utf8)
        mesh.vertices.add(len(fbx_verts) // 3)
        mesh.vertices.foreach_set("co", fbx_verts)
    
    
        if fbx_polys:
            mesh.loops.add(len(fbx_polys))
            poly_loop_starts = []
            poly_loop_totals = []
            poly_loop_prev = 0
            for i, l in enumerate(mesh.loops):
                index = fbx_polys[i]
                if index < 0:
                    poly_loop_starts.append(poly_loop_prev)
                    poly_loop_totals.append((i - poly_loop_prev) + 1)
                    poly_loop_prev = i + 1
    
    Campbell Barton's avatar
    Campbell Barton committed
                    index ^= -1
    
                l.vertex_index = index
    
            mesh.polygons.add(len(poly_loop_starts))
            mesh.polygons.foreach_set("loop_start", poly_loop_starts)
            mesh.polygons.foreach_set("loop_total", poly_loop_totals)
    
    
            blen_read_geom_layer_material(fbx_obj, mesh)
    
            blen_read_geom_layer_uv(fbx_obj, mesh)
    
            blen_read_geom_layer_color(fbx_obj, mesh)
    
            # edges in fact index the polygons (NOT the vertices)
            import array
            tot_edges = len(fbx_edges)
            edges_conv = array.array('i', [0]) * (tot_edges * 2)
    
            edge_index = 0
            for i in fbx_edges:
    
    Campbell Barton's avatar
    Campbell Barton committed
                e_a = fbx_polys[i]
                if e_a >= 0:
                    e_b = fbx_polys[i + 1]
    
    Campbell Barton's avatar
    Campbell Barton committed
                        e_b ^= -1
    
                else:
                    # Last index of polygon, wrap back to the start.
    
                    # ideally we wouldn't have to search back,
                    # but it should only be 2-3 iterations.
                    j = i - 1
                    while j >= 0 and fbx_polys[j] >= 0:
                        j -= 1
    
    Campbell Barton's avatar
    Campbell Barton committed
                    e_a ^= -1
                    e_b = fbx_polys[j + 1]
    
    
                edges_conv[edge_index] = e_a
                edges_conv[edge_index + 1] = e_b
                edge_index += 2
    
            mesh.edges.add(tot_edges)
            mesh.edges.foreach_set("vertices", edges_conv)
    
    
        # must be after edge, face loading.
        ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh)
    
    
        ok_normals = False
        if settings.use_custom_normals:
            # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
            #       we can only set custom lnors *after* calling it.
            mesh.create_normals_split()
            if geom_mat_no is None:
                ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh)
            else:
                def nortrans(v):
                    return geom_mat_no * Vector(v)
                ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans)
    
        mesh.validate(clean_customdata=False)  # *Very* important to not remove lnors here!
    
    
        if ok_normals:
            clnors = array.array('f', [0.0] * (len(mesh.loops) * 3))
            mesh.loops.foreach_get("normal", clnors)
    
            if not ok_smooth:
                mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
                ok_smooth = True
    
            mesh.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3)))
            mesh.use_auto_smooth = True
            mesh.show_edge_sharp = True
        else:
    
            mesh.calc_normals()
    
        if settings.use_custom_normals:
            mesh.free_normals_split()
    
            mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, mesh, settings)
    
    
    def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
        indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
        dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
        # We completely ignore normals here!
        weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
        vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
    
    
        # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
        nbr_indices = len(indices)
        if len(vgweights) == 1 and nbr_indices > 1:
            vgweights = (vgweights[0],) * nbr_indices
    
        assert(len(vgweights) == nbr_indices == len(dvcos))
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        create_vg = bool(set(vgweights) - {1.0})
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        for me, objects in meshes:
            vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
    
            objects = list({node.bl_obj for node in objects})
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            assert(objects)
    
            if me.shape_keys is None:
                objects[0].shape_key_add(name="Basis", from_mix=False)
            objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
            me.shape_keys.use_relative = True  # Should already be set as such.
    
            kb = me.shape_keys.key_blocks[elem_name_utf8]
            for idx, co in vcos:
                kb.data[idx].co[:] = co
            kb.value = weight
    
            # Add vgroup if necessary.
            if create_vg:
                add_vgroup_to_objects(indices, vgweights, elem_name_utf8, objects)
                kb.vertex_group = elem_name_utf8
    
    
    def blen_read_material(fbx_tmpl, fbx_obj, settings):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Material')
    
        cycles_material_wrap_map = settings.cycles_material_wrap_map
    
        ma = bpy.data.materials.new(name=elem_name_utf8)
    
        const_color_white = 1.0, 1.0, 1.0
    
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
        #~ assert(fbx_props[0] is not None)  # Some Material may be missing that one, it seems... see T50566.
    
    
        ma_diff = elem_props_get_color_rgb(fbx_props, b'DiffuseColor', const_color_white)
        ma_spec = elem_props_get_color_rgb(fbx_props, b'SpecularColor', const_color_white)
        ma_alpha = elem_props_get_number(fbx_props, b'Opacity', 1.0)
        ma_spec_intensity = ma.specular_intensity = elem_props_get_number(fbx_props, b'SpecularFactor', 0.25) * 2.0
        ma_spec_hardness = elem_props_get_number(fbx_props, b'Shininess', 9.6)
        ma_refl_factor = elem_props_get_number(fbx_props, b'ReflectionFactor', 0.0)
        ma_refl_color = elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
    
    
            from modules import cycles_shader_compat
    
            # viewport color
            ma.diffuse_color = ma_diff
    
            ma_wrap = cycles_shader_compat.CyclesShaderWrapper(ma)
            ma_wrap.diffuse_color_set(ma_diff)
            ma_wrap.specular_color_set([c * ma_spec_intensity for c in ma_spec])
    
            ma_wrap.hardness_value_set(((ma_spec_hardness + 3.0) / 5.0) - 0.65)
    
            ma_wrap.alpha_value_set(ma_alpha)
            ma_wrap.reflect_factor_set(ma_refl_factor)
            ma_wrap.reflect_color_set(ma_refl_color)
    
            cycles_material_wrap_map[ma] = ma_wrap
        else:
            # TODO, number BumpFactor isnt used yet
            ma.diffuse_color = ma_diff
            ma.specular_color = ma_spec
            ma.alpha = ma_alpha
    
            if ma_alpha < 1.0:
                ma.use_transparency = True
                ma.transparency_method = 'RAYTRACE'
    
            ma.specular_intensity = ma_spec_intensity
            ma.specular_hardness = ma_spec_hardness * 5.10 + 1.0
    
            if ma_refl_factor != 0.0:
                ma.raytrace_mirror.use = True
                ma.raytrace_mirror.reflect_factor = ma_refl_factor
                ma.mirror_color = ma_refl_color
    
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, ma, settings)
    
    
    def blen_read_texture_image(fbx_tmpl, fbx_obj, basedir, settings):
    
        import os
        from bpy_extras import image_utils
    
    
        def pack_data_from_content(image, fbx_obj):
            data = elem_find_first_bytes(fbx_obj, b'Content')
            if (data):
                data_len = len(data)
                if (data_len):
                    image.pack(data=data, data_len=data_len)
    
    
        elem_name_utf8 = elem_name_ensure_classes(fbx_obj, {b'Texture', b'Video'})
    
        # Yet another beautiful logic demonstration by Master FBX:
        # * RelativeFilename in both Video and Texture nodes.
        # * FileName in texture nodes.
        # * Filename in video nodes.
        # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
        filepath = elem_find_first_string(fbx_obj, b'RelativeFilename')
    
        if filepath:
            filepath = os.path.join(basedir, filepath)
    
            filepath = elem_find_first_string(fbx_obj, b'FileName')
    
        if not filepath:
            filepath = elem_find_first_string(fbx_obj, b'Filename')
        if not filepath:
            print("Error, could not find any file path in ", fbx_obj)
    
            print("       Falling back to: ", elem_name_utf8)
            filepath = elem_name_utf8
    
        else :
            filepath = filepath.replace('\\', '/') if (os.sep == '/') else filepath.replace('/', '\\')
    
    
        image = image_cache.get(filepath)
        if image is not None:
    
            # Data is only embedded once, we may have already created the image but still be missing its data!
            if not image.has_data:
                pack_data_from_content(image, fbx_obj)
    
        image = image_utils.load_image(
            filepath,
            dirname=basedir,
            place_holder=True,
    
        pack_data_from_content(image, fbx_obj)
    
        image_cache[filepath] = image
        # name can be ../a/b/c
        image.name = os.path.basename(elem_name_utf8)
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, image, settings)
    
    
    def blen_read_camera(fbx_tmpl, fbx_obj, global_scale):
    
        # meters to inches
        M2I = 0.0393700787
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
        assert(fbx_props[0] is not None)
    
    
        camera = bpy.data.cameras.new(name=elem_name_utf8)
    
    
        camera.type = 'ORTHO' if elem_props_get_enum(fbx_props, b'CameraProjectionType', 0) == 1 else 'PERSP'
    
    
        camera.lens = elem_props_get_number(fbx_props, b'FocalLength', 35.0)
        camera.sensor_width = elem_props_get_number(fbx_props, b'FilmWidth', 32.0 * M2I) / M2I
        camera.sensor_height = elem_props_get_number(fbx_props, b'FilmHeight', 32.0 * M2I) / M2I
    
    
        camera.ortho_scale = elem_props_get_number(fbx_props, b'OrthoZoom', 1.0)
    
    
        filmaspect = camera.sensor_width / camera.sensor_height
        # film offset
        camera.shift_x = elem_props_get_number(fbx_props, b'FilmOffsetX', 0.0) / (M2I * camera.sensor_width)
        camera.shift_y = elem_props_get_number(fbx_props, b'FilmOffsetY', 0.0) / (M2I * camera.sensor_height * filmaspect)
    
    
        camera.clip_start = elem_props_get_number(fbx_props, b'NearPlane', 0.01) * global_scale
        camera.clip_end = elem_props_get_number(fbx_props, b'FarPlane', 100.0) * global_scale
    
    def blen_read_light(fbx_tmpl, fbx_obj, global_scale):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
        # rare
        if fbx_props[0] is None:
            lamp = bpy.data.lamps.new(name=elem_name_utf8, type='POINT')
            return lamp
    
    
        light_type = {
            0: 'POINT',
            1: 'SUN',
            2: 'SPOT'}.get(elem_props_get_enum(fbx_props, b'LightType', 0), 'POINT')
    
        lamp = bpy.data.lamps.new(name=elem_name_utf8, type=light_type)
    
    
        if light_type == 'SPOT':
    
            spot_size = elem_props_get_number(fbx_props, b'OuterAngle', None)
            if spot_size is None:
                # Deprecated.
                spot_size = elem_props_get_number(fbx_props, b'Cone angle', 45.0)
            lamp.spot_size = math.radians(spot_size)
    
            spot_blend = elem_props_get_number(fbx_props, b'InnerAngle', None)
            if spot_blend is None:
                # Deprecated.
                spot_blend = elem_props_get_number(fbx_props, b'HotSpot', 45.0)
            lamp.spot_blend = 1.0 - (spot_blend / spot_size)
    
        lamp.color = elem_props_get_color_rgb(fbx_props, b'Color', (1.0, 1.0, 1.0))
    
        lamp.energy = elem_props_get_number(fbx_props, b'Intensity', 100.0) / 100.0
        lamp.distance = elem_props_get_number(fbx_props, b'DecayStart', 25.0) * global_scale
        lamp.shadow_method = ('RAY_SHADOW' if elem_props_get_bool(fbx_props, b'CastShadow', True) else 'NOSHADOW')
        lamp.shadow_color = elem_props_get_color_rgb(fbx_props, b'ShadowColor', (0.0, 0.0, 0.0))
    
    # ### Import Utility class
    class FbxImportHelperNode:
        """
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
        It tries to keep the correction data in one place so it can be applied consistently to the imported data.
    
            '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
    
            'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
            'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
            'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
            'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
    
    
        def __init__(self, fbx_elem, bl_data, fbx_transform_data, is_bone):
            self.fbx_name = elem_name_ensure_class(fbx_elem, b'Model') if fbx_elem else 'Unknown'
            self.fbx_type = fbx_elem.props[2] if fbx_elem else None
            self.fbx_elem = fbx_elem
            self.bl_obj = None
            self.bl_data = bl_data
            self.bl_bone = None                     # Name of bone if this is a bone (this may be different to fbx_name if there was a name conflict in Blender!)
            self.fbx_transform_data = fbx_transform_data
            self.is_root = False
            self.is_bone = is_bone
            self.is_armature = False
    
            self.armature = None                    # For bones only, relevant armature node.
    
            self.has_bone_children = False          # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
    
            self.is_leaf = False                    # True for leaf-bones added to the end of some bone chains to set the lengths.
    
            self.pre_matrix = None                  # correction matrix that needs to be applied before the FBX transform
            self.bind_matrix = None                 # for bones this is the matrix used to bind to the skin
    
            if fbx_transform_data:
                self.matrix, self.matrix_as_parent, self.matrix_geom = blen_read_object_transform_do(fbx_transform_data)
            else:
                self.matrix, self.matrix_as_parent, self.matrix_geom = (None, None, None)
    
            self.post_matrix = None                 # correction matrix that needs to be applied after the FBX transform
            self.bone_child_matrix = None           # Objects attached to a bone end not the beginning, this matrix corrects for that
    
    
            # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
            #     that their animation is in global space (afaik...).
            #     This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
            #     itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
            #     and for each armature action... beyond being an insane work).
            #     Solution for now: do not read rigged meshes animations at all! sic...
    
            self.anim_compensation_matrix = None    # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
    
    
            self.meshes = None                      # List of meshes influenced by this bone.
            self.clusters = []                      # Deformer Cluster nodes
    
            self.armature_setup = {}                # mesh and armature matrix when the mesh was bound
    
    
            self._parent = None
            self.children = []
    
        @property
        def parent(self):
            return self._parent
    
        @parent.setter
        def parent(self, value):
            if self._parent is not None:
                self._parent.children.remove(self)
            self._parent = value
            if self._parent is not None:
                self._parent.children.append(self)
    
    
        @property
        def ignore(self):
            # Separating leaf status from ignore status itself.
            # Currently they are equivalent, but this may change in future.
            return self.is_leaf
    
    
        def __repr__(self):
            if self.fbx_elem:
                return self.fbx_elem.props[1].decode()
            else:
                return "None"
    
    
        def print_info(self, indent=0):
            print(" " * indent + (self.fbx_name if self.fbx_name else "(Null)")
                  + ("[root]" if self.is_root else "")
    
                  + ("[ignore]" if self.ignore else "")
                  + ("[armature]" if self.is_armature else "")
                  + ("[bone]" if self.is_bone else "")
                  + ("[HBC]" if self.has_bone_children else "")
                  )
            for c in self.children:
                c.print_info(indent + 1)
    
        def mark_leaf_bones(self):
            if self.is_bone and len(self.children) == 1:
                child = self.children[0]
                if child.is_bone and len(child.children) == 0:
    
            for child in self.children:
                child.mark_leaf_bones()
    
        def do_bake_transform(self, settings):
            return (settings.bake_space_transform and self.fbx_type in (b'Mesh', b'Null') and
                    not self.is_armature and not self.is_bone)
    
        def find_correction_matrix(self, settings, parent_correction_inv=None):
            from bpy_extras.io_utils import axis_conversion
    
            if self.parent and (self.parent.is_root or self.parent.do_bake_transform(settings)):
                self.pre_matrix = settings.global_matrix
    
            if parent_correction_inv:
                self.pre_matrix = parent_correction_inv * (self.pre_matrix if self.pre_matrix else Matrix())
    
            correction_matrix = None
    
            if self.is_bone:
                if settings.automatic_bone_orientation:
                    # find best orientation to align bone with
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                    bone_children = tuple(child for child in self.children if child.is_bone)
    
                    if len(bone_children) == 0:
                        # no children, inherit the correction from parent (if possible)
                        if self.parent and self.parent.is_bone:
                            correction_matrix = parent_correction_inv.inverted() if parent_correction_inv else None
                    else:
                        # else find how best to rotate the bone to align the Y axis with the children
                        best_axis = (1, 0, 0)
                        if len(bone_children) == 1:
    
                            vec = bone_children[0].get_bind_matrix().to_translation()
    
                            best_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
                            if abs(vec[0]) > abs(vec[1]):
                                if abs(vec[0]) > abs(vec[2]):
                                    best_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
                            elif abs(vec[1]) > abs(vec[2]):
                                best_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
                        else:
                            # get the child directions once because they may be checked several times
    
                            child_locs = (child.get_bind_matrix().to_translation() for child in bone_children)
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                            child_locs = tuple(loc.normalized() for loc in child_locs if loc.magnitude > 0.0)
    
    
                            # I'm not sure which one I like better...
                            if False:
                                best_angle = -1.0
                                for i in range(6):
                                    a = i // 2
                                    s = -1 if i % 2 == 1 else 1
                                    test_axis = Vector((s if a == 0 else 0, s if a == 1 else 0, s if a == 2 else 0))
    
                                    # find max angle to children
                                    max_angle = 1.0
                                    for loc in child_locs:
                                        max_angle = min(max_angle, test_axis.dot(loc))
    
                                    # is it better than the last one?
                                    if best_angle < max_angle:
                                        best_angle = max_angle
                                        best_axis = test_axis
                            else:
                                best_angle = -1.0
                                for vec in child_locs:
                                    test_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
                                    if abs(vec[0]) > abs(vec[1]):
                                        if abs(vec[0]) > abs(vec[2]):
                                            test_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
                                    elif abs(vec[1]) > abs(vec[2]):
                                        test_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
    
                                    # find max angle to children
                                    max_angle = 1.0
                                    for loc in child_locs:
                                        max_angle = min(max_angle, test_axis.dot(loc))
    
                                    # is it better than the last one?
                                    if best_angle < max_angle:
                                        best_angle = max_angle
                                        best_axis = test_axis
    
                        # convert best_axis to axis string
                        to_up = 'Z' if best_axis[2] >= 0 else '-Z'
                        if abs(best_axis[0]) > abs(best_axis[1]):
                            if abs(best_axis[0]) > abs(best_axis[2]):
                                to_up = 'X' if best_axis[0] >= 0 else '-X'
                        elif abs(best_axis[1]) > abs(best_axis[2]):
                            to_up = 'Y' if best_axis[1] >= 0 else '-Y'
                        to_forward = 'X' if to_up not in {'X', '-X'} else 'Y'
    
                        # Build correction matrix
                        if (to_up, to_forward) != ('Y', 'X'):
                            correction_matrix = axis_conversion(from_forward='X',
                                                                from_up='Y',
                                                                to_forward=to_forward,
                                                                to_up=to_up,
                                                                ).to_4x4()
                else:
                    correction_matrix = settings.bone_correction_matrix
            else:
                # camera and light can be hard wired
                if self.fbx_type == b'Camera':
                    correction_matrix = MAT_CONVERT_CAMERA
                elif self.fbx_type == b'Light':
                    correction_matrix = MAT_CONVERT_LAMP
    
            self.post_matrix = correction_matrix
    
            if self.do_bake_transform(settings):
                self.post_matrix = settings.global_matrix_inv * (self.post_matrix if self.post_matrix else Matrix())
    
            # process children
    
            correction_matrix_inv = correction_matrix.inverted_safe() if correction_matrix else None
    
            for child in self.children:
                child.find_correction_matrix(settings, correction_matrix_inv)
    
    
        def find_armature_bones(self, armature):
            for child in self.children:
                if child.is_bone:
                    child.armature = armature
                    child.find_armature_bones(armature)
    
    
        def find_armatures(self):
            needs_armature = False
            for child in self.children:
                if child.is_bone:
                    needs_armature = True
                    break
            if needs_armature:
    
                if self.fbx_type in {b'Null', b'Root'}:
    
                    # if empty then convert into armature
                    self.is_armature = True
    
                    # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
    
                    armature = FbxImportHelperNode(None, None, None, False)
                    armature.fbx_name = "Armature"
                    armature.is_armature = True
    
    
                    for child in tuple(self.children):
    
                        if child.is_bone:
                            child.parent = armature
    
                    armature.parent = self
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                if child.is_armature or child.is_bone:
    
                    continue
                child.find_armatures()
    
        def find_bone_children(self):
            has_bone_children = False
            for child in self.children:
                has_bone_children |= child.find_bone_children()
            self.has_bone_children = has_bone_children
            return self.is_bone or has_bone_children
    
        def find_fake_bones(self, in_armature=False):
            if in_armature and not self.is_bone and self.has_bone_children:
                self.is_bone = True
                # if we are not a null node we need an intermediate node for the data
    
                if self.fbx_type not in {b'Null', b'Root'}:
    
                    node = FbxImportHelperNode(self.fbx_elem, self.bl_data, None, False)
                    self.fbx_elem = None
                    self.bl_data = None
    
                    # transfer children
                    for child in self.children:
                        if child.is_bone or child.has_bone_children:
                            continue
                        child.parent = node
    
                    # attach to parent
                    node.parent = self
    
            if self.is_armature:
                in_armature = True
            for child in self.children:
                child.find_fake_bones(in_armature)
    
    
        def get_world_matrix_as_parent(self):
            matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
            if self.matrix_as_parent:
                matrix = matrix * self.matrix_as_parent
            return matrix
    
    
            matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
    
            if self.matrix:
                matrix = matrix * self.matrix
            return matrix
    
        def get_matrix(self):
            matrix = self.matrix if self.matrix else Matrix()
            if self.pre_matrix:
                matrix = self.pre_matrix * matrix
            if self.post_matrix:
                matrix = matrix * self.post_matrix
            return matrix
    
        def get_bind_matrix(self):
            matrix = self.bind_matrix if self.bind_matrix else Matrix()
            if self.pre_matrix:
                matrix = self.pre_matrix * matrix
            if self.post_matrix:
                matrix = matrix * self.post_matrix
            return matrix
    
        def make_bind_pose_local(self, parent_matrix=None):
            if parent_matrix is None:
                parent_matrix = Matrix()
    
            if self.bind_matrix:
    
                bind_matrix = parent_matrix.inverted_safe() * self.bind_matrix
    
            else:
                bind_matrix = self.matrix.copy() if self.matrix else None
    
            self.bind_matrix = bind_matrix
            if bind_matrix:
                parent_matrix = parent_matrix * bind_matrix
    
            for child in self.children:
                child.make_bind_pose_local(parent_matrix)
    
        def collect_skeleton_meshes(self, meshes):
            for _, m in self.clusters:
                meshes.update(m)
            for child in self.children:
                child.collect_skeleton_meshes(meshes)
    
        def collect_armature_meshes(self):
            if self.is_armature:
    
                armature_matrix_inv = self.get_world_matrix().inverted_safe()
    
    
                meshes = set()
                for child in self.children:
                    child.collect_skeleton_meshes(meshes)
                for m in meshes:
                    old_matrix = m.matrix
                    m.matrix = armature_matrix_inv * m.get_world_matrix()
    
                    m.anim_compensation_matrix = old_matrix.inverted_safe() * m.matrix
    
                    m.parent = self
                self.meshes = meshes
            else:
                for child in self.children:
                    child.collect_armature_meshes()
    
    
        def build_skeleton(self, arm, parent_matrix, parent_bone_size=1, force_connect_children=False):
    
            def child_connect(par_bone, child_bone, child_head, connect_ctx):
                # child_bone or child_head may be None.
                force_connect_children, connected = connect_ctx
                if child_bone is not None:
                    child_bone.parent = par_bone
                    child_head = child_bone.head
    
                if similar_values_iter(par_bone.tail, child_head):
                    if child_bone is not None:
                        child_bone.use_connect = True
                    # Disallow any force-connection at this level from now on, since that child was 'really'
                    # connected, we do not want to move current bone's tail anymore!
                    connected = None
                elif force_connect_children and connected is not None:
                    # We only store position where tail of par_bone should be in the end.
                    # Actual tail moving and force connection of compatible child bones will happen
                    # once all have been checked.
                    if connected is ...:
                        connected = ([child_head.copy(), 1], [child_bone] if child_bone is not None else [])
                    else:
                        connected[0][0] += child_head
                        connected[0][1] += 1
                        if child_bone is not None:
                            connected[1].append(child_bone)
                connect_ctx[1] = connected
    
            def child_connect_finalize(par_bone, connect_ctx):
                force_connect_children, connected = connect_ctx
                # Do nothing if force connection is not enabled!
                if force_connect_children and connected is not None and connected is not ...:
    
                    # Here again we have to be wary about zero-length bones!!!
                    par_tail = connected[0][0] / connected[0][1]
                    if (par_tail - par_bone.head).magnitude < 1e-2:
                        par_bone_vec = (par_bone.tail - par_bone.head).normalized()
                        par_tail = par_bone.head + par_bone_vec * 0.01
                    par_bone.tail = par_tail
    
                    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()
    
            bone.matrix = bone_matrix
    
    
    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))
            assert(fbx_props[0] is not None)
    
            # ----
            # Misc Attributes
    
            obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8))
            obj.hide = 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)
    
    
            return obj
    
        def build_skeleton_children(self, fbx_tmpl, settings, scene):
            if self.is_bone:
                for child in self.children:
                    if child.ignore:
                        continue
    
                    child.build_skeleton_children(fbx_tmpl, settings, scene)
                return None
            else:
                # child is not a bone
                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)
    
                # instance in scene
                obj_base = scene.objects.link(obj)
                obj_base.select = 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.
    
                        if child.pre_matrix:
                            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_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: