Skip to content
Snippets Groups Projects
import_fbx.py 109 KiB
Newer Older
  • Learn to ignore specific revisions
  • 
                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)
    
    
        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)
    
    
        if not ok_normals:
            mesh.calc_normals()
    
        if not ok_smooth:
            for p in mesh.polygons:
                p.use_smooth = True
    
    
        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
        from mathutils import Vector
    
        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)
    
    
        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 . 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
            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(fbx_tmpl, fbx_obj, basedir, settings):
    
        import os
        from bpy_extras import image_utils
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Texture')
    
        filepath = elem_find_first_string(fbx_obj, b'FileName')
        if os.sep == '/':
            filepath = filepath.replace('\\', '/')
        else:
            filepath = filepath.replace('/', '\\')
    
        image = image_cache.get(filepath)
        if image is not None:
            return image
    
    
        image = image_utils.load_image(
            filepath,
            dirname=basedir,
            place_holder=True,
    
        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:
        """
        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.
        """
    
        __slots__ = ('_parent', 'anim_compensation_matrix', 'armature_setup', 'bind_matrix', 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix',
                     'children', 'clusters', 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type', 'has_bone_children', 'ignore', 'is_armature',
                     'is_bone', 'is_root', 'matrix', '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.has_bone_children = False          # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
            self.ignore = 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
            self.matrix = blen_read_object_transform_do(fbx_transform_data) if fbx_transform_data else 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
            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 = None              # 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)
    
    
        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:
                    child.ignore = True  # Ignore leaf bone at end of chain
            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
            from mathutils import Matrix, Vector
    
            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
                    bone_children = [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].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 = [loc.normalized() for loc in [bind_matrix.to_translation() for bind_matrix in [child.bind_matrix for child in bone_children]] 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_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 == b'Null':
                    # if empty then convert into armature
                    self.is_armature = True
                else:
                    # otherwise insert a new node
                    armature = FbxImportHelperNode(None, None, None, False)
                    armature.fbx_name = "Armature"
                    armature.is_armature = True
    
                    for child in self.children[:]:
                        if child.is_bone:
                            child.parent = armature
    
                    armature.parent = self
    
            for child in self.children:
                if child.is_armature:
                    continue
                if 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 != b'Null':
                    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(self):
            from mathutils import Matrix
    
            matrix = self.parent.get_world_matrix() if self.parent else Matrix()
            if self.matrix:
                matrix = matrix * self.matrix
            return matrix
    
        def get_matrix(self):
            from mathutils import Matrix
    
            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):
            from mathutils import Matrix
    
            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):
            from mathutils import Matrix
    
            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):
            from mathutils import Vector, Matrix
    
            # ----
            # Now, 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.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.
            # 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
    
            # 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)
    
            for child in self.children:
                if child.ignore:
                    continue
                if child.is_bone:
                    child_bone = child.build_skeleton(arm, bone_matrix, bone_size)
                    # Connection to parent.
                    child_bone.parent = bone
                    if similar_values_iter(bone.tail, child_bone.head):
                        child_bone.use_connect = True
    
            return bone
    
        def build_node(self, fbx_tmpl, settings):
            # 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(fbx_props[0], 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_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
                    if child_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'
    
                        # 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()
    
            else:
                # child is not a bone
                obj = self.build_node(fbx_tmpl, settings)
    
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
                    if child_obj:
                        child_obj.parent = obj
    
                # instance in scene
                obj_base = scene.objects.link(obj)
                obj_base.select = True
    
                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):
            ignored_children = [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.
                # (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:
                if child.ignore:
                    continue
                if child.is_bone:
                    child.set_bone_weights()
    
        def build_hierarchy(self, fbx_tmpl, settings, scene):
            from mathutils import Matrix
    
            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))
                    assert(fbx_props[0] is not None)
    
                    if settings.use_custom_props:
                        blen_read_custom_properties(fbx_props[0], arm, settings)
    
                # instance in scene
                obj_base = scene.objects.link(arm)
                obj_base.select = True
    
                # Add bones:
    
                # Switch to Edit mode.
                scene.objects.active = arm
                is_hidden = arm.hide
                arm.hide = 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_obj = child.build_skeleton(self, Matrix())
    
                bpy.ops.object.mode_set(mode='OBJECT')
    
                arm.hide = is_hidden
    
                # 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)
                    if child_obj:
                        child_obj.parent = arm
    
                # Add armature modifiers to the meshes
                if self.meshes:
                    arm_mat_back = arm.matrix_basis.copy()
                    for mesh in self.meshes:
                        (amat, mmat) = mesh.armature_setup
    
                        # bring global armature & mesh matrices into *Blender* global space.
                        amat = settings.global_matrix * amat
                        mmat = settings.global_matrix * mmat
    
                        arm.matrix_basis = amat
                        me_mat_back = mesh.bl_obj.matrix_basis.copy()
                        mesh.bl_obj.matrix_basis = mmat
    
                        mod = mesh.bl_obj.modifiers.new(elem_name_utf8, 'ARMATURE')
                        mod.object = arm
    
                        mesh.bl_obj.matrix_basis = me_mat_back
                    arm.matrix_basis = arm_mat_back
    
                # 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
            elif self.fbx_elem:
                obj = self.build_node(fbx_tmpl, settings)
    
                # walk through children
                for child in self.children:
                    child_obj = child.build_hierarchy(fbx_tmpl, settings, scene)
                    child_obj.parent = obj
    
                # instance in scene
                obj_base = scene.objects.link(obj)
                obj_base.select = True
    
                return obj
            else:
                for child in self.children:
                    child_obj = child.build_hierarchy(fbx_tmpl, settings, scene)
    
    
    
    def is_ascii(filepath, size):
        with open(filepath, 'r', encoding="utf-8") as f:
            try:
                f.read(size)
                return True
            except UnicodeDecodeError:
                pass
    
        return False
    
    
    
    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,
    
             decal_offset=0.0,
             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 mathutils import Matrix
    
    
        from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        start_time = time.process_time()
    
    
        # detect ascii files
        if is_ascii(filepath, 24):
            operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
            return {'CANCELLED'}
    
    
        try:
            elem_root, version = parse_fbx.parse(filepath)
        except:
            import traceback
            traceback.print_exc()
    
            operator.report({'ERROR'}, "Couldn't open file %r" % filepath)
            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)
    
        cycles_material_wrap_map = {}
        image_cache = {}
        if not use_cycles:
            texture_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
    
    
        # #### 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...
        global_scale *= elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 100.0) / 100.0
    
        # 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)
        if real_fps < 0.0:
            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_cycles, use_image_search,
            use_alpha_decals, decal_offset,
    
            use_custom_props, use_custom_props_enum_as_string,
    
            cycles_material_wrap_map, image_cache,
            ignore_leaf_bones, 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'}