diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 6196e19c..70343a36 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -1904,90 +1904,101 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): if settings.use_custom_normals: mesh.free_normals_split() if not ok_smooth: sharp_face = MESH_ATTRIBUTE_SHARP_FACE.get(attributes) if sharp_face: attributes.remove(sharp_face) if settings.use_custom_props: blen_read_custom_properties(fbx_obj, mesh, settings) return mesh def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene): if not fbx_data: # No shape key data. Nothing to do. return me_vcos = MESH_ATTRIBUTE_POSITION.to_ndarray(me.attributes) me_vcos_vector_view = me_vcos.reshape(-1, 3) objects = list({node.bl_obj for node in objects}) assert(objects) # Blender has a hard minimum and maximum shape key Value. If an imported shape key has a value outside this range it # will be clamped, and we'll print a warning message to the console. shape_key_values_in_range = True bc_uuid_to_keyblocks = {} - for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data: + for k, v in fbx_data.items(): + bc_uuid = k[0] + fbx_sdata, fbx_bcdata = v + elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry') indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes')) dvcos = elem_prop_first(elem_find_first(fbx_sdata, b'Vertices')) indices = parray_as_ndarray(indices) if indices else np.empty(0, dtype=data_types.ARRAY_INT32) dvcos = parray_as_ndarray(dvcos) if dvcos else np.empty(0, dtype=data_types.ARRAY_FLOAT64) # If there's not a whole number of vectors, trim off the remainder. # 3 components per vector. remainder = len(dvcos) % 3 if remainder: dvcos = dvcos[:-remainder] dvcos = dvcos.reshape(-1, 3) # We completely ignore normals here! weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0 vgweights = elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights')) vgweights = parray_as_ndarray(vgweights) if vgweights else np.empty(0, dtype=data_types.ARRAY_FLOAT64) # Not doing the division in-place in-case it's possible for FBX shape keys to be used by more than one mesh. vgweights = vgweights / 100.0 create_vg = (vgweights != 1.0).any() # 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 = np.full_like(indices, vgweights[0], dtype=vgweights.dtype) + # Special case for trimmed weights with trailing zeroes + if len(vgweights) != nbr_indices: + # Pad with zeros + new_vgweights = np.zeros_like(indices, dtype=vgweights.dtype) + n = min(len(vgweights), nbr_indices) + new_vgweights[:n] = vgweights[:n] + vgweights = new_vgweights + assert(len(vgweights) == nbr_indices == len(dvcos)) # To add shape keys to the mesh, an Object using the mesh is needed. if me.shape_keys is None: objects[0].shape_key_add(name="Basis", from_mix=False) kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False) me.shape_keys.use_relative = True # Should already be set as such. # Only need to set the shape key co if there are any non-zero dvcos. if dvcos.any(): shape_cos = me_vcos_vector_view.copy() shape_cos[indices] += dvcos kb.data.foreach_set("co", shape_cos.ravel()) shape_key_values_in_range &= expand_shape_key_range(kb, weight) kb.value = weight # Add vgroup if necessary. if create_vg: # VertexGroup.add only allows sequences of int indices, but iterating the indices array directly would # produce numpy scalars of types such as np.int32. The underlying memoryview of the indices array, however, # does produce standard Python ints when iterated, so pass indices.data to add_vgroup_to_objects instead of # indices. # memoryviews tend to be faster to iterate than numpy arrays anyway, so vgweights.data is passed too. add_vgroup_to_objects(indices.data, vgweights.data, kb.name, objects) kb.vertex_group = kb.name bc_uuid_to_keyblocks.setdefault(bc_uuid, []).append(kb) @@ -3479,65 +3490,66 @@ def load(operator, context, filepath="", 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 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)) # Group shapes by mesh so that each mesh only needs to be processed once for all of its shape # keys. if bl_mdata not in mesh_to_shapes: # 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) - shapes_list = [] - mesh_to_shapes[bl_mdata] = (objects, shapes_list) + shapes = {} + mesh_to_shapes[bl_mdata] = (objects, shapes) else: - shapes_list = mesh_to_shapes[bl_mdata][1] - shapes_list.append((bc_uuid, fbx_sdata, fbx_bcdata)) + shapes = mesh_to_shapes[bl_mdata][1] + # Dict to avoid duplicates + shapes[(bc_uuid, id(fbx_sdata), id(fbx_bcdata))] = (fbx_sdata, fbx_bcdata) # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do. # Iterate through each mesh and create its shape keys for bl_mdata, (objects, shapes) in mesh_to_shapes.items(): for bc_uuid, keyblocks in blen_read_shapes(fbx_tmpl, shapes, objects, bl_mdata, scene).items(): # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation. blend_shape_channels.setdefault(bc_uuid, []).extend(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'))