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
Example #2
0
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