def apply_morph_remapping(pmx: pmxstruct.Pmx, morph_dellist, morph_shiftmap): # actually delete the morphs from the list for f in reversed(morph_dellist): pmx.morphs.pop(f) # frames: for d, frame in enumerate(pmx.frames): i = 0 while i < len(frame.items): item = frame.items[i] # if this item is a bone, skip it if not item.is_morph: i += 1 else: # if this is one of the morphs being deleted, delete it here too. otherwise remap. if core.binary_search_isin(item.idx, morph_dellist): frame.items.pop(i) else: item.idx = newval_from_range_map(item.idx, morph_shiftmap) i += 1 # group/flip morphs: for d, morph in enumerate(pmx.morphs): # group/flip = 0/9 if morph.morphtype not in (pmxstruct.MorphType.GROUP, pmxstruct.MorphType.FLIP): continue i = 0 while i < len(morph.items): it = morph.items[i] it: pmxstruct.PmxMorphItemGroup # if this is one of the morphs being deleted, delete it here too. otherwise remap. if core.binary_search_isin(it.morph_idx, morph_dellist): morph.items.pop(i) else: it.morph_idx = newval_from_range_map(it.morph_idx, morph_shiftmap) i += 1 return pmx
def prune_unused_vertices(pmx: pmxstruct.Pmx, moreinfo=False): ############################# # ready for logic # vertices are referenced in faces, morphs (uv and vertex morphs), and soft bodies (should be handled just for completeness' sake) # find individual vertices to delete # build set of vertices used in faces # build set of all vertices (just a range) # subtract # convert to sorted list # convert to list of [begin, length] # iterate over delvertlist, identify contiguous blocks # convert to list of [begin, cumulative size] # build set of USED vertices used_verts = set() for face in pmx.faces: used_verts.add(face[0]) used_verts.add(face[1]) used_verts.add(face[2]) # build set of ALL vertices all_verts = set(list(range(len(pmx.verts)))) # derive set of UNUSED vertices unused_verts = all_verts.difference(used_verts) # convert to ordered list delme_verts = sorted(list(unused_verts)) numdeleted = len(delme_verts) prevtotal = len(pmx.verts) if numdeleted == 0: core.MY_PRINT_FUNC("No changes are required") return pmx, False delme_range = delme_list_to_rangemap(delme_verts) if moreinfo: core.MY_PRINT_FUNC( "Detected %d orphan vertices arranged in %d contiguous blocks" % (len(delme_verts), len(delme_range[0]))) # need to update places that reference vertices: faces, morphs, softbody # first get the total # of iterations I need to do, for progress purposes: #faces + sum of len of all UV and vert morphs totalwork = len(pmx.faces) + sum([ len(m.items) for m in pmx.morphs if (m.morphtype in (1, 3, 4, 5, 6, 7)) ]) # faces: d = 0 for d, face in enumerate(pmx.faces): # vertices in a face are not guaranteed sorted, and sorting them is a Very Bad Idea # therefore they must be remapped individually face[0] = newval_from_range_map(face[0], delme_range) face[1] = newval_from_range_map(face[1], delme_range) face[2] = newval_from_range_map(face[2], delme_range) # display progress printouts core.print_progress_oneline(d / totalwork) # core.MY_PRINT_FUNC("Done updating vertex references in faces") # morphs: orphan_vertex_references = 0 for morph in pmx.morphs: # if not a vertex morph or UV morph, skip it if not morph.morphtype in (1, 3, 4, 5, 6, 7): continue lenbefore = len(morph.items) # it is plausible that vertex/uv morphs could reference orphan vertices, so I should check for and delete those i = 0 while i < len(morph.items): # if the vertex being manipulated is in the list of verts being deleted, if core.binary_search_isin(morph.items[i].vert_idx, delme_verts): # delete it here too morph.items.pop(i) orphan_vertex_references += 1 else: # otherwise, remap it # but don't remap it here, wait until I'm done deleting vertices and then tackle them all at once i += 1 # morphs usually contain vertexes in sorted order, but not guaranteed!!! MAKE it sorted, nobody will mind morph.items.sort(key=lambda x: x.vert_idx) # separate the vertices from the morph entries into a list of their own, for more efficient remapping vertlist = [x.vert_idx for x in morph.items] # remap remappedlist = newval_from_range_map(vertlist, delme_range) # write the remapped values back into where they came from for x, newval in zip(morph.items, remappedlist): x.vert_idx = newval # display progress printouts d += lenbefore core.print_progress_oneline(d / totalwork) # core.MY_PRINT_FUNC("Done updating vertex references in morphs") # softbody: probably not relevant but eh for soft in pmx.softbodies: # anchors # first, delete any references to delme verts in the anchors i = 0 while i < len(soft.anchors_list): # if the vertex referenced is in the list of verts being deleted, if core.binary_search_isin(soft.anchors_list[i][1], delme_verts): # delete it here too soft.anchors_list.pop(i) else: # otherwise, remap it # but don't remap it here, wait until I'm done deleting vertices and then tackle them all at once i += 1 # MAKE it sorted, nobody will mind soft.anchors_list.sort(key=lambda x: x[1]) # extract the vert indices into a list of their town anchorlist = [x[1] for x in soft.anchors_list] # remap newanchorlist = newval_from_range_map(anchorlist, delme_range) # write the remapped values back into where they came from for x, newval in zip(soft.anchors_list, newanchorlist): x[1] = newval # vertex pins # first, delete any references to delme verts i = 0 while i < len(soft.vertex_pin_list): # if the vertex referenced is in the list of verts being deleted, if core.binary_search_isin(soft.vertex_pin_list[i], delme_verts): # delete it here too soft.vertex_pin_list.pop(i) else: # otherwise, remap it # but don't remap it here, wait until I'm done deleting vertices and then tackle them all at once i += 1 # MAKE it sorted, nobody will mind soft.anchors_list.sort() # remap soft.vertex_pin_list = newval_from_range_map(soft.vertex_pin_list, delme_range) # done with softbodies! # now, finally, actually delete the vertices from the vertex list delme_verts.reverse() for f in delme_verts: pmx.verts.pop(f) core.MY_PRINT_FUNC( "Identified and deleted {} / {} = {:.1%} vertices for being unused". format(numdeleted, prevtotal, numdeleted / prevtotal)) return pmx, True
def apply_bone_remapping(pmx: pmxstruct.Pmx, bone_dellist: List[int], bone_shiftmap: Tuple[List[int], List[int]]): """ Given a list of bones to delete, delete them, and update the indices for all references to all remaining bones. PMX is modified in-place. Behavior is undefined if the dellist bones are still in use somewhere! References include: vertex weight, bone morph, display frame, rigidbody anchor, bone tail, bone partial inherit, bone IK target, bone IK link. :param pmx: PMX object :param bone_dellist: list of bone indices to delete :param bone_shiftmap: created by delme_list_to_rangemap() before calling """ core.print_progress_oneline(0 / 5) # VERTICES: # just remap the bones that have weight # any references to bones being deleted will definitely have 0 weight, and therefore it doesn't matter what they reference afterwards for d, vert in enumerate(pmx.verts): for pair in vert.weight: pair[0] = newval_from_range_map(int(pair[0]), bone_shiftmap) # done with verts core.print_progress_oneline(1 / 5) # MORPHS: for d, morph in enumerate(pmx.morphs): # only operate on bone morphs if morph.morphtype != pmxstruct.MorphType.BONE: continue # first, it is plausible that bone morphs could reference otherwise unused bones, so I should check for and delete those i = 0 while i < len(morph.items): it = morph.items[i] it: pmxstruct.PmxMorphItemBone # if the bone being manipulated is in the list of bones being deleted, delete it here too. otherwise remap. if core.binary_search_isin(it.bone_idx, bone_dellist): morph.items.pop(i) else: it.bone_idx = newval_from_range_map(it.bone_idx, bone_shiftmap) i += 1 # done with morphs core.print_progress_oneline(2 / 5) # DISPLAY FRAMES for d, frame in enumerate(pmx.frames): i = 0 while i < len(frame.items): item = frame.items[i] # if this item is a morph, skip it if item.is_morph: i += 1 else: # if this is one of the bones being deleted, delete it here too. otherwise remap. if core.binary_search_isin(item.idx, bone_dellist): frame.items.pop(i) else: item.idx = newval_from_range_map(item.idx, bone_shiftmap) i += 1 # done with frames core.print_progress_oneline(3 / 5) # RIGIDBODY for d, body in enumerate(pmx.rigidbodies): # only remap, no possibility of one of these bones being deleted body.bone_idx = newval_from_range_map(body.bone_idx, bone_shiftmap) # done with bodies core.print_progress_oneline(4 / 5) # BONES: point-at target, true parent, external parent, partial append, ik stuff for d, bone in enumerate(pmx.bones): # point-at link: if bone.tail_usebonelink: if core.binary_search_isin(bone.tail, bone_dellist): # if pointing at a bone that will be deleted, instead change to offset with offset 0,0,0 bone.tail_usebonelink = False bone.tail = [0, 0, 0] else: # otherwise, remap bone.tail = newval_from_range_map(bone.tail, bone_shiftmap) # other 4 categories only need remapping # true parent: bone.parent_idx = newval_from_range_map(bone.parent_idx, bone_shiftmap) # partial append: if (bone.inherit_rot or bone.inherit_trans) and bone.inherit_parent_idx != -1: if core.binary_search_isin(bone.inherit_parent_idx, bone_dellist): # if a bone is getting partial append from a bone getting deleted, break that relationship # shouldn't be possible but whatever i'll support the case bone.inherit_rot = False bone.inherit_trans = False bone.inherit_parent_idx = -1 else: bone.inherit_parent_idx = newval_from_range_map( bone.inherit_parent_idx, bone_shiftmap) # ik stuff: if bone.has_ik: bone.ik_target_idx = newval_from_range_map(bone.ik_target_idx, bone_shiftmap) for link in bone.ik_links: link.idx = newval_from_range_map(link.idx, bone_shiftmap) # done with bones return