def make_zipfile_backup(startpath: str, backup_suffix: str) -> bool: """ Make a .zip backup of the folder 'startpath' and all its contents. Returns True if all goes well, False if it should abort. Resulting zip will be adjacent to the folder it is backing up with a slightly different name. :param startpath: absolute path of the folder you want to zip :param backup_suffix: segment inserted between the foldername and .zip extension :return: true if things are good, False if i should abort """ # need to add .zip for checking against already-exising files and for printing zipname = startpath + "." + backup_suffix + ".zip" zipname = core.get_unused_file_name(zipname) core.MY_PRINT_FUNC("...making backup archive:") core.MY_PRINT_FUNC(zipname) try: root_dir = os.path.dirname(startpath) base_dir = os.path.basename(startpath) # need to remove .zip suffix because zipper forcefully adds .zip whether its already on the name or not shutil.make_archive(zipname[:-4], 'zip', root_dir, base_dir) except Exception as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) info = [ "ERROR3! Unable to create zipfile for backup.", "Do you want to continue without a zipfile backup?", "1 = Yes, 2 = No (abort)" ] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 2: return False return True
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("L upper arm...") make_autotwist_segment(pmx, jp_l, jp_arm, jp_armtwist, jp_elbow, moreinfo) core.MY_PRINT_FUNC("R upper arm...") make_autotwist_segment(pmx, jp_r, jp_arm, jp_armtwist, jp_elbow, moreinfo) core.MY_PRINT_FUNC("L lower arm...") make_autotwist_segment(pmx, jp_l, jp_elbow, jp_wristtwist, jp_wrist, moreinfo) core.MY_PRINT_FUNC("R lower arm...") make_autotwist_segment(pmx, jp_r, jp_elbow, jp_wristtwist, jp_wrist, moreinfo) # if i want to, set elbowD parent to armT...? # if i want to, set wrist parent to elbowT...? # that's what the original does, but why? why would I want that? # armT should have exactly the same deformations as armtwist # it's better to have the twist-rigs be isolated from eachother # TODO: examine leg system! not universal because nobody has legtwist bones to hijack but worth understanding # write out output_filename_pmx = input_filename_pmx[0:-4] + "_autotwist.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # to shift the model by a set amount: # first, ask user for X Y Z # create the prompt popup shift_str = core.MY_GENERAL_INPUT_FUNC( lambda x: (is_3float(x) is not None), [ "Enter the X,Y,Z amount to shift this model by:", "Three decimal values separated by commas.", "Empty input will quit the script." ]) # if empty, quit if shift_str == "": core.MY_PRINT_FUNC("quitting") return None # use the same func to convert the input string shift = is_3float(shift_str) #################### # then execute the shift: for v in pmx.verts: # every vertex position for i in range(3): v.pos[i] += shift[i] # c, r0, r1 params of every SDEF vertex # these correspond to real positions in 3d space so they need to be modified if v.weighttype == pmxstruct.WeightMode.SDEF: for param in v.weight_sdef: for i in range(3): param[i] += shift[i] # bone position for b in pmx.bones: for i in range(3): b.pos[i] += shift[i] # rigid body position for rb in pmx.rigidbodies: for i in range(3): rb.pos[i] += shift[i] # joint position for j in pmx.joints: for i in range(3): j.pos[i] += shift[i] # that's it? that's it! # write out output_filename_pmx = input_filename_pmx[0:-4] + "_shift.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # usually want to hide many morphs at a time, so put all this in a loop num_hidden = 0 while True: core.MY_PRINT_FUNC("") # valid input is any string that can matched aginst a morph idx s = core.MY_GENERAL_INPUT_FUNC(lambda x: (morph_scale.get_idx_in_pmxsublist(x, pmx.morphs) is not None), ["Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).", "Empty input will quit the script."]) # do it again, cuz the lambda only returns true/false target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.morphs) # when given empty text, done! if target_index == -1 or target_index is None: core.MY_PRINT_FUNC("quitting") break # determine the morph type morphtype = pmx.morphs[target_index].morphtype core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format( morphtype, target_index, pmx.morphs[target_index].name_jp, pmx.morphs[target_index].name_en)) core.MY_PRINT_FUNC("Was group {}, now group {}".format( pmx.morphs[target_index].panel, pmxstruct.MorphPanel.HIDDEN)) # make the actual change pmx.morphs[target_index].panel = pmxstruct.MorphPanel.HIDDEN num_hidden += 1 pass if num_hidden == 0: core.MY_PRINT_FUNC("Nothing was changed") return None # last step: remove all invalid morphs from all display panels for d, frame in enumerate(pmx.frames): # for each display group, i = 0 while i < len(frame.items): # for each item in that display group, item = frame.items[i] if item.is_morph: # if it is a morph # look up the morph morph = pmx.morphs[item.idx] # figure out what panel of this morph is # if it has an invalid panel #, delete it here if morph.panel == pmxstruct.MorphPanel.HIDDEN: frame.items.pop(i) else: i += 1 else: i += 1 # write out output_filename_pmx = input_filename_pmx[0:-4] + "_morphhide.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def convert_vmd_to_txt(input_filename: str, moreinfo=True): """ Read a VMD motion file from disk, convert it, and write to disk as a text file. The output will have the same path and basename, but the opposite file extension. See 'README.txt' for more details about VMD-as-text output format. :param input_filename: filepath to input vmd, absolute or relative to CWD :param moreinfo: default false. if true, get extra printouts with more info about stuff. """ # read the entire VMD, all in this one function # also create the bonedict & morphdict vmd_nicelist = vmdlib.read_vmd(input_filename, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # identify an unused filename for writing the output dumpname = core.get_unused_file_name(input_filename[0:-4] + filestr_txt) # write the output VMD-as-text file write_vmdtext(dumpname, vmd_nicelist) # ##################################### # # summary file: # # # if there are no bones and no morphs, there is no need for a summary file... just return early # if len(bonedict) == 0 and len(morphdict) == 0: # return None # # if the user doesn't want a summary, dont bother # elif not PRINT_BONE_MORPH_SUMMARY_FILE: # return None # else: # # identify an unused filename for writing the output # summname = core.get_unused_file_name(core.get_clean_basename(dumpname) + "_summary" + filestr_txt) # write_summary_dicts(bonedict, morphdict, summname) # done! return
def end(pmx, input_filename_pmx): # write out # output_filename_pmx = "%s_winnow.pmx" % core.get_clean_basename(input_filename_pmx) output_filename_pmx = input_filename_pmx[0:-4] + "_winnow.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=True) return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) realbones = pmx.bones # get bones realmorphs = pmx.morphs # get morphs modelname_jp = pmx.header.name_jp modelname_en = pmx.header.name_en bonelist_out = [["modelname_jp", "'" + modelname_jp + "'"], ["modelname_en", "'" + modelname_en + "'"], ["bonename_jp", "bonename_en"]] morphlist_out = [["modelname_jp", "'" + modelname_jp + "'"], ["modelname_en", "'" + modelname_en + "'"], ["morphname_jp", "morphname_en"]] # in both lists, idx0 = name_jp, idx1 = name_en bonelist_pairs = [[a.name_jp, a.name_en] for a in realbones] morphlist_pairs = [[a.name_jp, a.name_en] for a in realmorphs] bonelist_out += bonelist_pairs morphlist_out += morphlist_pairs # write out output_filename_bone = "%s_bone_names.txt" % input_filename_pmx[0:-4] # output_filename_bone = output_filename_bone.replace(" ", "_") output_filename_bone = core.get_unused_file_name(output_filename_bone) core.MY_PRINT_FUNC("...writing result to file '%s'..." % output_filename_bone) core.write_csvlist_to_file(output_filename_bone, bonelist_out, use_jis_encoding=False) output_filename_morph = "%s_morph_names.txt" % input_filename_pmx[0:-4] output_filename_morph = core.get_unused_file_name(output_filename_morph) core.MY_PRINT_FUNC("...writing result to file '%s'..." % output_filename_morph) core.write_csvlist_to_file(output_filename_morph, morphlist_out, use_jis_encoding=False) core.MY_PRINT_FUNC("Done!") return None
def convert_txt_to_vmd(input_filename, moreinfo=True): """ Read a VMD-as-text file from disk, convert it, and write to disk as a VMD motion file. The output will have the same path and basename, but the opposite file extension. :param input_filename: filepath to input vmd, absolute or relative to CWD :param moreinfo: default false. if true, get extra printouts with more info about stuff. """ # read the VMD-as-text into the nicelist format, all in one function vmd_nicelist = read_vmdtext(input_filename) core.MY_PRINT_FUNC("") # identify an unused filename for writing the output dumpname = core.get_unused_file_name(input_filename[0:-4] + ".vmd") # write the output VMD file vmdlib.write_vmd(dumpname, vmd_nicelist, moreinfo=moreinfo) # done! return
def convert_vmd_to_vpd(vmd_path: str, moreinfo=True): """ Read a VMD motion file from disk, convert it, and write to disk as a VPD pose file. All frames of the VMD are ignored except for frames at time=0. The output will have the same path and basename, but the opposite file extension. :param vmd_path: filepath to input vmd, absolute or relative to CWD :param moreinfo: default false. if true, get extra printouts with more info about stuff. """ # read the entire VMD, all in this one function vmd = vmdlib.read_vmd(vmd_path, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # identify an unused filename for writing the output vpd_outpath = core.get_unused_file_name(vmd_path[0:-4] + ".vpd") # write the output VPD file vpdlib.write_vpd(vpd_outpath, vmd, moreinfo=moreinfo) # done! return
def convert_vpd_to_vmd(vpd_path: str, moreinfo=True): """ Read a VPD pose file from disk, convert it, and write to disk as a VMD motion file. The resulting VMD will be empty except for bone/morph frames at time=0. The output will have the same path and basename, but the opposite file extension. :param vpd_path: filepath to input vpd, absolute or relative to CWD :param moreinfo: default false. if true, get extra printouts with more info about stuff. """ # read the VPD into memory as a VMD object vmd = vpdlib.read_vpd(vpd_path, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # identify an unused filename for writing the output vmd_outpath = core.get_unused_file_name(vpd_path[0:-4] + ".vmd") # write the output VMD file vmdlib.write_vmd(vmd_outpath, vmd, moreinfo=moreinfo) # done! return
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # valid input is any string that can matched aginst a morph idx s = core.MY_GENERAL_INPUT_FUNC( lambda x: get_idx_in_pmxsublist(x, pmx.morphs) is not None, [ "Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).", "Empty input will quit the script." ]) # do it again, cuz the lambda only returns true/false target_index = get_idx_in_pmxsublist(s, pmx.morphs) # when given empty text, done! if target_index == -1 or target_index is None: core.MY_PRINT_FUNC("quitting") return None # determine the morph type morphtype = pmx.morphs[target_index].morphtype core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format( morphtype, target_index, pmx.morphs[target_index].name_jp, pmx.morphs[target_index].name_en)) # if it is a bone morph, ask for translation/rotation/both bone_mode = 0 if morphtype == pmxstruct.MorphType.BONE: bone_mode = core.MY_SIMPLECHOICE_FUNC((1, 2, 3), [ "Bone morph detected: do you want to scale the motion(translation), rotation, or both?", "1 = motion(translation), 2 = rotation, 3 = both" ]) # ask for factor: keep looping this prompt until getting a valid float def is_float(x): try: v = float(x) return True except ValueError: core.MY_PRINT_FUNC("Please enter a decimal number") return False factor_str = core.MY_GENERAL_INPUT_FUNC( is_float, "Enter the factor that you want to scale this morph by:") if factor_str == "": core.MY_PRINT_FUNC("quitting") return None factor = float(factor_str) # important values: target_index, factor, morphtype, bone_mode # first create the new morph that is a copy of current if SCALE_MORPH_IN_PLACE: newmorph = pmx.morphs[target_index] else: newmorph = copy.deepcopy(pmx.morphs[target_index]) # then modify the names name_suffix = "*" + (str(factor)[0:6]) newmorph.name_jp += name_suffix newmorph.name_en += name_suffix # now scale the actual values r = morph_scale(newmorph, factor, bone_mode) if not r: core.MY_PRINT_FUNC("quitting") return None pmx.morphs.append(newmorph) # write out output_filename_pmx = input_filename_pmx[0:-4] + ("_%dscal.pmx" % target_index) output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=False): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) #### how should these operations be ordered? # faces before verts, because faces define what verts are used # verts before weights, so i operate on fewer vertices & run faster # weights before bones, because weights determine what bones are used # verts before morph winnow, so i operate on fewer vertices & run faster # translate after bones/disp groups/morph winnow because they reduce the # of things to translate # uniquify after translate, because translate can map multiple different JP to same EN names # alphamorphs after translate, so it uses post-translate names for printing # deform order after translate, so it uses post-translate names for printing # if ANY stage returns True then it has made changes # final file-write is skipped only if NO stage has made changes is_changed = False core.MY_PRINT_FUNC("\n>>>> Deleting invalid & duplicate faces <<<<") pmx, is_changed_t = _prune_invalid_faces.prune_invalid_faces(pmx, moreinfo) is_changed |= is_changed_t # or-equals: if any component returns true, then ultimately this func returns true core.MY_PRINT_FUNC("\n>>>> Deleting orphaned/unused vertices <<<<") pmx, is_changed_t = _prune_unused_vertices.prune_unused_vertices(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Deleting unused bones <<<<") pmx, is_changed_t = _prune_unused_bones.prune_unused_bones(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Normalizing vertex weights & normals <<<<") pmx, is_changed_t = _weight_cleanup.weight_cleanup(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Pruning imperceptible vertex morphs <<<<") pmx, is_changed_t = _morph_winnow.morph_winnow(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Fixing display groups: duplicates, empty groups, missing items <<<<") pmx, is_changed_t = _dispframe_fix.dispframe_fix(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Adding missing English names <<<<") pmx, is_changed_t = _translate_to_english.translate_to_english(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Ensuring all names in the model are unique <<<<") pmx, is_changed_t = _uniquify_names.uniquify_names(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Fixing bone deform order <<<<") pmx, is_changed_t = _bonedeform_fix.bonedeform_fix(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("\n>>>> Standardizing alphamorphs and accounting for edging <<<<") pmx, is_changed_t = _alphamorph_correct.alphamorph_correct(pmx, moreinfo) is_changed |= is_changed_t core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("++++++++++++++++++++++++++++++++++++++++++++++++++++++++") core.MY_PRINT_FUNC("++++ Scanning for other potential issues ++++") longbone, longmorph = find_toolong_bonemorph(pmx) # also checks that bone/morph names can be stored in shift_jis for VMD usage if longmorph or longbone: core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Minor warning: this model contains bones/morphs with JP names that are too long (>15 bytes)") core.MY_PRINT_FUNC("These will work just fine in MMD but will not properly save/load in VMD motion files") if longbone: ss = "[" + ", ".join(longbone[0:MAX_WARNING_LIST]) + "]" if len(longbone) > MAX_WARNING_LIST: ss = ss[0:-1] + ", ...]" core.MY_PRINT_FUNC("These %d bones are too long (index[length]): %s" % (len(longbone), ss)) if longmorph: ss = "[" + ", ".join(longmorph[0:MAX_WARNING_LIST]) + "]" if len(longmorph) > MAX_WARNING_LIST: ss = ss[0:-1] + ", ...]" core.MY_PRINT_FUNC("These %d morphs are too long (index[length]): %s" % (len(longmorph), ss)) shadowy_mats = find_shadowy_materials(pmx) if shadowy_mats: core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Minor warning: this model contains transparent materials with visible edging") core.MY_PRINT_FUNC("Edging is visible even if the material is transparent, so this will look like an ugly silhouette") core.MY_PRINT_FUNC("Either disable edging in MMD when using this model, or reduce the edge parameters to 0 and re-add them in the morph that restores its opacity") ss = str(shadowy_mats[0:MAX_WARNING_LIST]) if len(shadowy_mats) > MAX_WARNING_LIST: ss = ss[0:-1] + ", ...]" core.MY_PRINT_FUNC("These %d materials need edging disabled (index): %s" % (len(shadowy_mats), ss)) boneless_bodies = find_boneless_bonebodies(pmx) if boneless_bodies: core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("WARNING: this model has bone-type rigidbodies that aren't anchored to any bones") core.MY_PRINT_FUNC("This won't crash MMD but it is probably a mistake that needs corrected") ss = str(boneless_bodies[0:MAX_WARNING_LIST]) if len(boneless_bodies) > MAX_WARNING_LIST: ss = ss[0:-1] + ", ...]" core.MY_PRINT_FUNC("These %d bodies are boneless (index): %s" % (len(boneless_bodies), ss)) jointless_bodies = find_jointless_physbodies(pmx) if jointless_bodies: core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("WARNING: this model has physics-type rigidbodies that aren't constrained by joints") core.MY_PRINT_FUNC("These will just roll around on the floor wasting processing power in MMD") ss = str(jointless_bodies[0:MAX_WARNING_LIST]) if len(jointless_bodies) > MAX_WARNING_LIST: ss = ss[0:-1] + ", ...]" core.MY_PRINT_FUNC("These %d bodies are jointless (index): %s" % (len(jointless_bodies), ss)) crashing_joints = find_crashing_joints(pmx) if crashing_joints: # make the biggest f*****g alert i can cuz this is a critical issue core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ") core.MY_PRINT_FUNC("CRITICAL WARNING: this model contains invalid joints which WILL cause MMD to crash!") core.MY_PRINT_FUNC("These must be manually deleted or repaired using PMXE") core.MY_PRINT_FUNC("These %d joints are invalid (index): %s" % (len(crashing_joints), crashing_joints)) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("++++++++++++++++++++++++++++++++++++++++++++++++++++++++") if not is_changed: core.MY_PRINT_FUNC("++++ No writeback required ++++") core.MY_PRINT_FUNC("Done!") return core.MY_PRINT_FUNC("++++ Done with cleanup, saving improvements to file ++++") # write out # output_filename_pmx = "%s_better.pmx" % core.get_clean_basename(input_filename_pmx) output_filename_pmx = input_filename_pmx[0:-4] + "_better.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # usually want to add/remove endpoints for many bones at once, so put all this in a loop num_changed = 0 while True: core.MY_PRINT_FUNC("") # valid input is any string that can matched aginst a bone idx s = core.MY_GENERAL_INPUT_FUNC( lambda x: (morph_scale.get_idx_in_pmxsublist(x, pmx.bones) is not None), [ "Please specify the target bone: bone #, JP name, or EN name (names are not case sensitive).", "Empty input will quit the script." ]) # do it again, cuz the lambda only returns true/false target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.bones) # when given empty text, done! if target_index == -1 or target_index is None: core.MY_PRINT_FUNC("quitting") break target_bone = pmx.bones[target_index] # print the bone it found core.MY_PRINT_FUNC("Found bone #{}: '{}' / '{}'".format( target_index, target_bone.name_jp, target_bone.name_en)) if target_bone.tail_type: core.MY_PRINT_FUNC( "Was tailmode 'bonelink', changing to mode 'offset'") if target_bone.tail == -1: core.MY_PRINT_FUNC( "Error: bone is not linked to anything, skipping") continue # find the location of the bone currently pointing at endpos = pmx.bones[target_bone.tail].pos # determine the equivalent offset vector offset = [endpos[i] - target_bone.pos[i] for i in range(3)] # write it into the bone target_bone.tail_type = False target_bone.tail = offset # done unlinking endpoint! pass else: core.MY_PRINT_FUNC( "Was tailmode 'offset', changing to mode 'bonelink' and adding new endpoint bone" ) if target_bone.tail == [0, 0, 0]: core.MY_PRINT_FUNC( "Error: bone has offset of [0,0,0], skipping") continue # determine the position of the new endpoint bone endpos = [ target_bone.pos[i] + target_bone.tail[i] for i in range(3) ] # create the new bone newbone = pmxstruct.PmxBone( name_jp=target_bone.name_jp + endpoint_suffix_jp, name_en=target_bone.name_en + endpoint_suffix_en, pos=endpos, parent_idx=target_index, deform_layer=target_bone.deform_layer, deform_after_phys=target_bone.deform_after_phys, has_rotate=False, has_translate=False, has_visible=False, has_enabled=True, has_ik=False, has_localaxis=False, has_fixedaxis=False, has_externalparent=False, inherit_rot=False, inherit_trans=False, tail_type=True, tail=-1) # set the target to point at the new bone target_bone.tail_type = True target_bone.tail = len(pmx.bones) # append the new bone pmx.bones.append(newbone) # done adding endpoint! pass num_changed += 1 pass if num_changed == 0: core.MY_PRINT_FUNC("Nothing was changed") return None core.MY_PRINT_FUNC("") # write out output_filename_pmx = input_filename_pmx[0:-4] + "_endpoints.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=False): core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # step zero: set up the translator thingy translate_to_english.init_googletrans() # texture sorting plan: # 1. get startpath = basepath of input PMX # 2. get lists of relevant files # 2a. extract top-level 'neighbor' pmx files from all-set # 3. ask about modifying neighbor PMX # 4. read PMX: either target or target+all neighbor # 5. "categorize files & normalize usages within PMX", NEW FUNC!!! # 6. translate all names via Google Trans, don't even bother with local dict # 7. mask out invalid windows filepath chars just to be safe # 8. print proposed names & other findings # for unused files under a folder, combine & replace with *** # 9. ask for confirmation # 10. zip backup (NEW FUNC!) # 11. apply renaming, NEW FUNC! rename all including old PMXes on disk # 12. get new names for PMXes, write PMX from mem to disk if any of its contents changed # i.e. of all FileRecord with a new name, create a set of all the PMX that use them # absolute path to directory holding the pmx input_filename_pmx_abs = os.path.normpath( os.path.abspath(input_filename_pmx)) startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files relative_all_exist_files = file_sort_textures.walk_filetree_from_root( startpath) core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files)) # now fill "neighbor_pmx" by finding files without path separator that end in PMX # these are relative paths tho neighbor_pmx = [ f for f in relative_all_exist_files if (f.lower().endswith(".pmx")) and ( os.path.sep not in f) and f != input_filename_pmx_rel ] # no filtering, all files are relevant relevant_exist_files = relative_all_exist_files core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx)) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # now ask if I care about the neighbors and read the PMXes into memory pmx_filenames = [input_filename_pmx_rel] if neighbor_pmx: core.MY_PRINT_FUNC("") info = [ "Detected %d top-level neighboring PMX files, these probably share the same filebase as the target." % len(neighbor_pmx), "If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.", "Do you want to process all neighbors in addition to the target? (highly recommended)", "1 = Yes, 2 = No" ] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 1: core.MY_PRINT_FUNC("Processing target + all neighbor files") # append neighbor PMX files onto the list of files to be processed pmx_filenames += neighbor_pmx else: core.MY_PRINT_FUNC( "WARNING: Processing only target, ignoring %d neighbor PMX files" % len(neighbor_pmx)) # now read all the PMX objects & store in dict alongside the relative name # dictionary where keys are filename and values are resulting pmx objects all_pmx_obj = {} for this_pmx_name in pmx_filenames: this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name), moreinfo=moreinfo) all_pmx_obj[this_pmx_name] = this_pmx_obj # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk # also fill out how much and how each file is used, and unify dupes between files, all that good stuff filerecord_list = file_sort_textures.categorize_files( all_pmx_obj, relevant_exist_files, moreinfo) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # DETERMINE NEW NAMES FOR FILES # how to remap: build a list of all destinations (lowercase) to see if any proposed change would lead to collision all_new_names = set() # get new names via google # force it to use chunk-wise translate newname_list = translate_to_english.google_translate( [p.name for p in filerecord_list], strategy=1) # now repair any windows-forbidden symbols that might have shown up after translation newname_list = [ n.translate(invalid_windows_chars_ord) for n in newname_list ] # iterate over the results in parallel with the FileRecord items for p, newname in zip(filerecord_list, newname_list): if newname != p.name: # resolve potential collisions by adding numbers suffix to file names # first need to make path absolute so get_unused_file_name can check the disk. # then check uniqueness against files on disk and files in namelist (files that WILL be on disk) newname = core.get_unused_file_name(os.path.join( startpath, newname), namelist=all_new_names) # now dest path is guaranteed unique against other existing files & other proposed name changes all_new_names.add(newname.lower()) # make the path no longer absolute: undo adding "startpath" above newname = os.path.relpath(newname, startpath) p.newname = newname # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # NOW PRINT MY PROPOSED RENAMINGS and other findings # isolate the ones with proposed renaming translated_file = [u for u in filerecord_list if u.newname is not None] if translated_file: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC("Found %d JP filenames to be translated:" % len(translated_file)) oldname_list = core.MY_JUSTIFY_STRINGLIST( [p.name for p in translated_file]) newname_list = [p.newname for p in translated_file] zipped = list(zip(oldname_list, newname_list)) zipped_and_sorted = sorted( zipped, key=lambda y: file_sort_textures.sortbydirdepth(y[0])) for o, n in zipped_and_sorted: # print 'from' with the case/separator it uses in the PMX core.MY_PRINT_FUNC(" {:s} --> {:s}".format(o, n)) core.MY_PRINT_FUNC("=" * 60) else: core.MY_PRINT_FUNC("No proposed file changes") core.MY_PRINT_FUNC("Aborting: no files were changed") return None info = [ "Do you accept these new names/locations?", "1 = Yes, 2 = No (abort)" ] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 2: core.MY_PRINT_FUNC("Aborting: no files were changed") return None # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # finally, do the actual renaming: # first, create a backup of the folder if MAKE_BACKUP_BEFORE_RENAMES: r = file_sort_textures.make_zipfile_backup(startpath, BACKUP_SUFFIX) if not r: # this happens if the backup failed somehow AND the user decided to quit core.MY_PRINT_FUNC("Aborting: no files were changed") return None # do all renaming on disk and in PMXes, and also handle the print statements file_sort_textures.apply_file_renaming(all_pmx_obj, filerecord_list, startpath) # write out for this_pmx_name, this_pmx_obj in all_pmx_obj.items(): # what name do i write this pmx to? it may be different now! find it in the FileRecord! # this script does not filter filerecord_list so it is guaranteed to hae a record rec = None for r in filerecord_list: if r.name == this_pmx_name: rec = r break if rec.newname is None: # if there is no new name, write back to the name it had previously new_pmx_name = rec.name else: # if there is a new name, write to the new name new_pmx_name = rec.newname # make the name absolute output_filename_pmx = os.path.join(startpath, new_pmx_name) # write it, overwriting the existing file at that name pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # input_filename_pmx = "../../python_scripts/grasstest_better.pmx" pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) ################################## # user flow: # first ask whether they want to add armtwist, yes/no # second ask whether they want to add legtwist, yes/no # then do it # then write out to file ################################## working_queue = [] s = core.MY_SIMPLECHOICE_FUNC((1, 2), [ "Do you wish to add magic twistbones to the ARMS?", "1 = Yes, 2 = No" ]) if s == 1: # add upperarm set and lowerarm set to the queue working_queue.append(armset) working_queue.append(wristset) pass s = core.MY_SIMPLECHOICE_FUNC((1, 2), [ "Do you wish to add magic twistbones to the LEGS?", "1 = Yes, 2 = No" ]) if s == 1: # TODO detect whether d-bones exist or not # add legs or d-legs set to the queue pass if not working_queue: core.MY_PRINT_FUNC("Nothing was changed") core.MY_PRINT_FUNC("Done") return None # for each set in the queue, for boneset in working_queue: # boneset = (start, end, preferred, oldrigs, bezier) for side in [jp_l, jp_r]: # print(side) # print(boneset) # 1. first, validate that start/end exist, these are required # NOTE: remember to prepend 'side' before all jp names! start_jp = side + boneset[0] start_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == start_jp) if start_idx is None: core.MY_PRINT_FUNC( "ERROR: standard bone '%s' not found in model, this is required!" % start_jp) continue end_jp = side + boneset[1] end_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == end_jp) if end_idx is None: core.MY_PRINT_FUNC( "ERROR: standard bone '%s' not found in model, this is required!" % end_jp) continue # 2. determine whether the 'preferredparent' exists and therefore what to acutally use as the parent parent_jp = side + boneset[2] parent_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == parent_jp) if parent_idx is None: parent_idx = start_idx # 3. attempt to collapse known armtwist rig names onto 'parent' so that the base case is further automated # for each bonename in boneset[3], if it exists, collapse onto boneidx parent_idx for bname in boneset[3]: rig_idx = core.my_list_search( pmx.bones, lambda x: x.name_jp == side + bname) if rig_idx is None: continue # if not found, try the next # when it is found, what 'factor' do i use? # print(side+bname) if pmx.bones[rig_idx].inherit_rot and pmx.bones[ rig_idx].inherit_parent_idx == parent_idx and pmx.bones[ rig_idx].inherit_ratio != 0: # if using partial rot inherit AND inheriting from parent_idx AND ratio != 0, use that # think this is good, if twistbones exist they should be children of preferred f = pmx.bones[rig_idx].inherit_ratio elif pmx.bones[rig_idx].parent_idx == parent_idx: # this should be just in case? f = 1 elif pmx.bones[rig_idx].parent_idx == start_idx: # this should catch magic armtwist bones i previously created f = 1 else: core.MY_PRINT_FUNC( "Warning, found unusual relationship when collapsing old armtwist rig, assuming ratio=1" ) f = 1 transfer_bone_weights(pmx, parent_idx, rig_idx, f) pass # also collapse 'start' onto 'preferredparent' if it exists... want to transfer weight from 'arm' to 'armtwist' # if start == preferredparent this does nothing, no harm done transfer_bone_weights(pmx, parent_idx, start_idx, scalefactor=1) # 4. run the weight-cleanup function normalize_weights(pmx) # 5. append 3 new bones to end of bonelist # armYZ gets pos = start pos & parent = start parent basename_jp = pmx.bones[start_idx].name_jp armYZ_new_idx = len(pmx.bones) # armYZ = [basename_jp + yz_suffix, local_translate(basename_jp + yz_suffix)] # name_jp,en # armYZ += pmx[5][start_idx][2:] # copy the whole rest of the bone # armYZ[10:12] = [False, False] # visible=false, enabled=false # armYZ[12:14] = [True, [armYZ_new_idx + 1]] # tail type = tail, tail pointat = armYZend # armYZ[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZ[21:25] = [False, [], False, []] # disable ext parent + ik armYZ = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix, name_en=local_translate(basename_jp + yz_suffix), pos=pmx.bones[start_idx].pos, parent_idx=pmx.bones[start_idx].parent_idx, deform_layer=pmx.bones[start_idx].deform_layer, deform_after_phys=pmx.bones[start_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[start_idx].localaxis_x, localaxis_z=pmx.bones[start_idx].localaxis_z, tail_type=True, tail=armYZ_new_idx + 1, has_rotate=True, has_translate=True, has_visible=False, has_enabled=True, has_ik=False, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_externalparent=False, ) # armYZend gets pos = end pos & parent = armYZ # armYZend = [basename_jp + yz_suffix + "å…ˆ", local_translate(basename_jp + yz_suffix + "å…ˆ")] # name_jp,en # armYZend += pmx[5][end_idx][2:] # copy the whole rest of the bone # armYZend[5] = armYZ_new_idx # parent = armYZ # armYZend[10:12] = [False, False] # visible=false, enabled=false # armYZend[12:14] = [True, [-1]] # tail type = tail, tail pointat = none # armYZend[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZend[21:25] = [False, [], False, []] # disable ext parent + ik armYZend = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix + "å…ˆ", name_en=local_translate(basename_jp + yz_suffix + "å…ˆ"), pos=pmx.bones[end_idx].pos, parent_idx=armYZ_new_idx, deform_layer=pmx.bones[end_idx].deform_layer, deform_after_phys=pmx.bones[end_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[end_idx].localaxis_x, localaxis_z=pmx.bones[end_idx].localaxis_z, tail_type=True, tail=-1, has_rotate=True, has_translate=True, has_visible=False, has_enabled=True, has_ik=False, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_externalparent=False, ) # # elbowIK gets pos = end pos & parent = end parent # armYZIK = [basename_jp + yz_suffix + "IK", local_translate(basename_jp + yz_suffix + "IK")] # name_jp,en # armYZIK += pmx[5][end_idx][2:] # copy the whole rest of the bone # armYZIK[10:12] = [False, False] # visible=false, enabled=false # armYZIK[12:14] = [True, [-1]] # tail type = tail, tail pointat = none # armYZIK[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZIK[21:23] = [False, []] # disable ext parent # armYZIK[23] = True # ik=true # # add the ik info: [target, loops, anglelimit, [[link_idx, []], [link_idx, []]] ] # armYZIK[24] = [armYZ_new_idx+1, newik_loops, newik_angle, [[armYZ_new_idx, []]]] armYZIK = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix + "IK", name_en=local_translate(basename_jp + yz_suffix + "IK"), pos=pmx.bones[end_idx].pos, parent_idx=pmx.bones[end_idx].parent_idx, deform_layer=pmx.bones[end_idx].deform_layer, deform_after_phys=pmx.bones[end_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[end_idx].localaxis_x, localaxis_z=pmx.bones[end_idx].localaxis_z, tail_type=True, tail=-1, has_rotate=True, has_translate=True, has_visible=False, has_externalparent=False, has_enabled=True, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_ik=True, ik_target_idx=armYZ_new_idx + 1, ik_numloops=newik_loops, ik_angle=newik_angle, ik_links=[pmxstruct.PmxBoneIkLink(idx=armYZ_new_idx)]) # now append them to the bonelist pmx.bones.append(armYZ) pmx.bones.append(armYZend) pmx.bones.append(armYZIK) # 6. build the bezier curve bezier_curve = core.MyBezier(boneset[4][0], boneset[4][1], resolution=50) # 7. find relevant verts & determine unbounded percentile for each (verts, percentiles, centers) = calculate_percentiles(pmx, start_idx, end_idx, parent_idx) if moreinfo: core.MY_PRINT_FUNC( "Blending between bones '{}'/'{}'=ZEROtwist and '{}'/'{}'=FULLtwist" .format(armYZ.name_jp, armYZ.name_en, pmx.bones[parent_idx].name_jp, pmx.bones[parent_idx].name_en)) core.MY_PRINT_FUNC( " Found %d potentially relevant vertices" % len(verts)) # 8. use X or Y to choose border points, print for debugging, also scale the percentiles # first sort ascending by percentile value vert_zip = list(zip(verts, percentiles, centers)) vert_zip.sort(key=lambda x: x[1]) verts, percentiles, centers = zip(*vert_zip) # unzip # X. highest point mode # "liberal" endpoints: extend as far as i can, include all good stuff even if i include some bad stuff with it # start at each end and work inward until i find a vert controlled by only parent_idx i_min_liberal = 0 i_max_liberal = len(verts) - 1 i_min_conserv = -1 i_max_conserv = len(verts) for i_min_liberal in range( 0, len(verts)): # start at head and work down, if pmx.verts[verts[ i_min_liberal]].weighttype == 0: # if the vertex is BDEF1 type, break # then stop looking, p_min_liberal = percentiles[ i_min_liberal] # and save the percentile it found. for i_max_liberal in reversed(range( 0, len(verts))): # start at tail and work up, if pmx.verts[verts[ i_max_liberal]].weighttype == 0: # if the vertex is BDEF1 type, break # then stop looking, p_max_liberal = percentiles[ i_max_liberal] # and save the percentile it found. # Y. lowest highest point mode # "conservative" endpoints: define ends such that no bad stuff exists within bounds, even if i miss some good stuff # start in the middle and work outward until i find a vert NOT controlled by only parent_idx, then back off 1 # where is the middle? use "bisect_left" middle = core.bisect_left(percentiles, 0.5) for i_min_conserv in reversed( range(middle - 1)): # start in middle, work toward head, if pmx.verts[verts[ i_min_conserv]].weighttype != 0: # if the vertex is NOT BDEF1 type, break # then stop looking, i_min_conserv += 1 # and step back 1 to find the last vert that was good BDEF1, p_min_conserv = percentiles[ i_min_conserv] # and save the percentile it found. for i_max_conserv in range( middle + 1, len(verts)): # start in middle, work toward tail, if pmx.verts[verts[ i_max_conserv]].weighttype != 0: # if the vertex is NOT BDEF1 type, break # then stop looking, i_max_conserv -= 1 # and step back 1 to find the last vert that was good BDEF1, p_max_conserv = percentiles[ i_max_conserv] # and save the percentile it found. foobar = False if not (i_min_liberal <= i_min_conserv <= i_max_conserv <= i_max_liberal): core.MY_PRINT_FUNC( "ERROR: bounding indexes do not follow the expected relationship, results may be bad!" ) foobar = True if foobar or moreinfo: core.MY_PRINT_FUNC( " Max liberal bounds: idx = %d to %d, %% = %f to %f" % (i_min_liberal, i_max_liberal, p_min_liberal, p_max_liberal)) core.MY_PRINT_FUNC( " Max conservative bounds: idx = %d to %d, %% = %f to %f" % (i_min_conserv, i_max_conserv, p_min_conserv, p_max_conserv)) # IDEA: WEIGHTED BLEND! sliding scale! avg_factor = core.clamp(ENDPOINT_AVERAGE_FACTOR, 0.0, 1.0) if p_min_liberal != p_min_conserv: p_min = (p_min_liberal * avg_factor) + (p_min_conserv * (1 - avg_factor)) else: p_min = p_min_liberal if p_max_liberal != p_max_conserv: p_max = (p_max_liberal * avg_factor) + (p_max_conserv * (1 - avg_factor)) else: p_max = p_max_liberal # clamp just in case p_min = core.clamp(p_min, 0.0, 1.0) p_max = core.clamp(p_max, 0.0, 1.0) if moreinfo: i_min = core.bisect_left(percentiles, p_min) i_max = core.bisect_left(percentiles, p_max) core.MY_PRINT_FUNC( " Compromise bounds: idx = %d to %d, %% = %f to %f" % (i_min, i_max, p_min, p_max)) # now normalize the percentiles to these endpoints p_len = p_max - p_min percentiles = [(p - p_min) / p_len for p in percentiles] # 9. divide weight between preferredparent (or parent) and armYZ vert_zip = list(zip(verts, percentiles, centers)) num_modified, num_bleeders = divvy_weights( pmx=pmx, vert_zip=vert_zip, axis_limits=(pmx.bones[start_idx].pos, pmx.bones[end_idx].pos), bone_hasweight=parent_idx, bone_getsweight=armYZ_new_idx, bezier=bezier_curve) if moreinfo: core.MY_PRINT_FUNC( " Modified %d verts to use blending, %d are questionable 'bleeding' points" % (num_modified, num_bleeders)) pass pass # 10. run final weight-cleanup normalize_weights(pmx) # 11. write out output_filename_pmx = input_filename_pmx[0:-4] + "_magictwist.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # detect whether arm ik exists r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik) if r is None: r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik2) l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik) if l is None: l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik2) # decide whether to create or remove arm ik if r is None and l is None: # add IK branch core.MY_PRINT_FUNC(">>>> Adding arm IK <<<") # set output name if input_filename_pmx.lower().endswith(pmx_noik_suffix.lower()): output_filename = input_filename_pmx[0:-(len(pmx_noik_suffix))] + pmx_yesik_suffix else: output_filename = input_filename_pmx[0:-4] + pmx_yesik_suffix for side in [jp_l, jp_r]: # first find all 3 arm bones # even if i insert into the list, this will still be a valid reference i think bones = [] bones: List[pmxstruct.PmxBone] for n in [jp_arm, jp_elbow, jp_wrist]: i = core.my_list_search(pmx.bones, lambda x: x.name_jp == side + n, getitem=True) if i is None: core.MY_PRINT_FUNC("ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % (side + n)) raise RuntimeError() bones.append(i) # get parent of arm bone (shoulder bone), new bones will be inserted after this shoulder_idx = bones[0].parent_idx # new bones will be inserted AFTER shoulder_idx # newarm_idx = shoulder_idx+1 # newelbow_idx = shoulder_idx+2 # newwrist_idx = shoulder_idx+3 # newik_idx = shoulder_idx+4 # make copies of the 3 armchain bones # arm: parent is shoulder newarm = pmxstruct.PmxBone( name_jp=bones[0].name_jp + jp_ikchainsuffix, name_en=bones[0].name_en + jp_ikchainsuffix, pos=bones[0].pos, parent_idx=shoulder_idx, deform_layer=bones[0].deform_layer, deform_after_phys=bones[0].deform_after_phys, has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False, tail_usebonelink=True, tail=0, # want arm tail to point at the elbow, can't set it until elbow is created inherit_rot=False, inherit_trans=False, has_localaxis=bones[0].has_localaxis, localaxis_x=bones[0].localaxis_x, localaxis_z=bones[0].localaxis_z, has_externalparent=False, has_fixedaxis=False, ) insert_single_bone(pmx, newarm, shoulder_idx + 1) # change existing arm to inherit rot from this bones[0].inherit_rot = True bones[0].inherit_parent_idx = shoulder_idx + 1 bones[0].inherit_ratio = 1 # elbow: parent is newarm newelbow = pmxstruct.PmxBone( name_jp=bones[1].name_jp + jp_ikchainsuffix, name_en=bones[1].name_en + jp_ikchainsuffix, pos=bones[1].pos, parent_idx=shoulder_idx+1, deform_layer=bones[1].deform_layer, deform_after_phys=bones[1].deform_after_phys, has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False, tail_usebonelink=True, tail=0, # want elbow tail to point at the wrist, can't set it until wrist is created inherit_rot=False, inherit_trans=False, has_localaxis=bones[1].has_localaxis, localaxis_x=bones[1].localaxis_x, localaxis_z=bones[1].localaxis_z, has_externalparent=False, has_fixedaxis=False, ) insert_single_bone(pmx, newelbow, shoulder_idx + 2) # change existing elbow to inherit rot from this bones[1].inherit_rot = True bones[1].inherit_parent_idx = shoulder_idx + 2 bones[1].inherit_ratio = 1 # now that newelbow exists, change newarm tail to point to this newarm.tail = shoulder_idx + 2 # wrist: parent is newelbow newwrist = pmxstruct.PmxBone( name_jp=bones[2].name_jp + jp_ikchainsuffix, name_en=bones[2].name_en + jp_ikchainsuffix, pos=bones[2].pos, parent_idx=shoulder_idx+2, deform_layer=bones[2].deform_layer, deform_after_phys=bones[2].deform_after_phys, has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False, tail_usebonelink=True, tail=-1, # newwrist has no tail inherit_rot=False, inherit_trans=False, has_localaxis=bones[2].has_localaxis, localaxis_x=bones[2].localaxis_x, localaxis_z=bones[2].localaxis_z, has_externalparent=False, has_fixedaxis=False, ) insert_single_bone(pmx, newwrist, shoulder_idx + 3) # now that newwrist exists, change newelbow tail to point to this newelbow.tail = shoulder_idx + 3 # copy the wrist to make the IK bone en_suffix = "_L" if side == jp_l else "_R" # get index of "upperbody" to use as parent of hand IK bone ikpar = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_upperbody) if ikpar is None: core.MY_PRINT_FUNC("ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % jp_upperbody) raise RuntimeError() # newik = [side + jp_newik, en_newik + en_suffix] + bones[2][2:5] + [ikpar] # new names, copy pos, new par # newik += bones[2][6:8] + [1, 1, 1, 1] + [0, [0,1,0]] # copy deform layer, rot/trans/vis/en, tail type # newik += [0, 0, [], 0, [], 0, [], 0, []] # no inherit, no fixed axis, no local axis, no ext parent, yes IK # # add the ik info: [is_ik, [target, loops, anglelimit, [[link_idx, []]], [link_idx, []]]] ] ] # newik += [1, [shoulder_idx+3, newik_loops, newik_angle, [[shoulder_idx+2,[]],[shoulder_idx+1,[]]] ] ] newik = pmxstruct.PmxBone( name_jp=side + jp_newik, name_en=en_newik + en_suffix, pos=bones[2].pos, parent_idx=ikpar, deform_layer=bones[2].deform_layer, deform_after_phys=bones[2].deform_after_phys, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, tail_usebonelink=False, tail=[0,1,0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, has_ik=True, ik_target_idx=shoulder_idx+3, ik_numloops=newik_loops, ik_angle=newik_angle, ik_links=[pmxstruct.PmxBoneIkLink(idx=shoulder_idx+2), pmxstruct.PmxBoneIkLink(idx=shoulder_idx+1)] ) insert_single_bone(pmx, newik, shoulder_idx + 4) # then add to dispframe # first, does the frame already exist? f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik, getitem=True) newframeitem = pmxstruct.PmxFrameItem(is_morph=False, idx=shoulder_idx + 4) if f is None: # need to create the new dispframe! easy newframe = pmxstruct.PmxFrame(name_jp=jp_newik, name_en=en_newik, is_special=False, items=[newframeitem]) pmx.frames.append(newframe) else: # frame already exists, also easy f.items.append(newframeitem) else: # remove IK branch core.MY_PRINT_FUNC(">>>> Removing arm IK <<<") # set output name if input_filename_pmx.lower().endswith(pmx_yesik_suffix.lower()): output_filename = input_filename_pmx[0:-(len(pmx_yesik_suffix))] + pmx_noik_suffix else: output_filename = input_filename_pmx[0:-4] + pmx_noik_suffix # identify all bones in ik chain of hand ik bones bone_dellist = [] for b in [r, l]: bone_dellist.append(b) # this IK bone bone_dellist.append(pmx.bones[b].ik_target_idx) # the target of the bone for v in pmx.bones[b].ik_links: bone_dellist.append(v.idx) # each link along the bone bone_dellist.sort() # do the actual delete & shift delete_multiple_bones(pmx, bone_dellist) # delete dispframe for hand ik # first, does the frame already exist? f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik) if f is not None: # frame already exists, delete it pmx.frames.pop(f) pass # write out output_filename = core.get_unused_file_name(output_filename) pmxlib.write_pmx(output_filename, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # TODO: actually load it in MMD and verify that the curves look how they should # not 100% certain that the order of interpolation values is correct for bone/cam frames # TODO: some sort of additional stats somehow? # TODO: progress % trackers? # prompt VMD file name core.MY_PRINT_FUNC("Please enter name of VMD motion input file:") input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd") # next, read/use/prune the dance vmd vmd = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # dictify the boneframes so i can deal with one bone at a time boneframe_dict = dictify_framelist(vmd.boneframes) # add the camframes to the dict so I can process them at the same time # this makes the typechecker angry if len(vmd.camframes) != 0: boneframe_dict[NAME_FOR_CAMFRAMES] = vmd.camframes # >>>>>> part 0: verify that there are no "multiple frames on the same timestep" situations # the MMD GUI shouldn't let this happen, but apparently it has happened... how??? # the only explanation I can think of is that it's due to physics bones with names that are too long and # get truncated, and the uniquifying numbers are in the part that got lost. they originally had one frame # per bone but because the names were truncated they look like they're all the same name so it looks like # there are many frames for that non-real bone at the same timestep. for bonename, boneframe_list in boneframe_dict.items(): # if a bone has only 1 (or 0?) frames associated with it then there's definitely no overlap probelm if len(boneframe_list) < 2: continue i = 0 while i < len(boneframe_list) - 1: # look at all pairs of adjacent frames along a bone A = boneframe_list[i] B = boneframe_list[i + 1] # are they on the same timestep? if so, problem! if A.f == B.f: # are they setting the same pose? if A == B: # if they are setting the same values at the same frame, just fix the problem silently pass else: # if they are trying to set different values at the same frame, this is a problem! # gotta fix it to continue, but also gotta print some kind of warning if bonename == NAME_FOR_CAMFRAMES: core.MY_PRINT_FUNC( "WARNING: at timestep t=%d, there are multiple cam frames trying to set different poses. How does this even happen???" % A.f) else: core.MY_PRINT_FUNC( "WARNING: at timestep t=%d, there are multiple frames trying to set bone '%s' to different poses. How does this even happen???" % (A.f, bonename)) core.MY_PRINT_FUNC( "I will delete one of them and continue.") # remove the 2nd one so that there is only one frame at each timestep boneframe_list.pop(i + 1) continue # otherwise, no problem at all i += 1 # >>>>>> part 1: identify the desired slope for each metric of each frame core.MY_PRINT_FUNC("Finding smooth approach/depart slopes...") global CURRENT_BONENAME allbone_bezier_slopes = {} for bonename in sorted(boneframe_dict.keys()): CURRENT_BONENAME = bonename # you're not supposed to pass info via global like this, but idgaf sue me boneframe_list = boneframe_dict[bonename] # this will hold all the resulting bezier slopes # each item corresponds to one frame and is stored as: # [approach posx,y,z,rot],[depart posx,y,z,rot] thisbone_bezier_slopes = [] # for each sequence of frames on a single bone, for i in range(len(boneframe_list)): thisframe_bezier_approach = [] thisframe_bezier_depart = [] A = boneframe_list[i - 1] if i != 0 else None B = boneframe_list[i] C = boneframe_list[i + 1] if i != len(boneframe_list) - 1 else None # now i have the 3 frames I want to analyze # need to do the analysis for rotations & for positions # POSITION for j in range(3): A_point = (A.f, A.pos[j]) if (A is not None) else None B_point = (B.f, B.pos[j]) C_point = (C.f, C.pos[j]) if (C is not None) else None # stuffed all operations into one function for encapsulation bez_a, bez_d = scalar_calculate_ideal_bezier_slope( A_point, B_point, C_point) # store it thisframe_bezier_approach.append(bez_a) thisframe_bezier_depart.append(bez_d) # ROTATION A_point = (A.f, A.rot) if (A is not None) else None B_point = (B.f, B.rot) C_point = (C.f, C.rot) if (C is not None) else None # stuffed all operations into one function for encapsulation bez_a, bez_d = rotation_calculate_ideal_bezier_slope( A_point, B_point, C_point) # store it thisframe_bezier_approach.append(bez_a) thisframe_bezier_depart.append(bez_d) # CAMFRAME ONLY STUFF if bonename == NAME_FOR_CAMFRAMES: # the typechecker expects boneframes so it gets angry here # distance from camera to position A_point = (A.f, A.dist) if (A is not None) else None B_point = (B.f, B.dist) C_point = (C.f, C.dist) if (C is not None) else None # stuffed all operations into one function for encapsulation bez_a, bez_d = scalar_calculate_ideal_bezier_slope( A_point, B_point, C_point) # store it thisframe_bezier_approach.append(bez_a) thisframe_bezier_depart.append(bez_d) # field of view A_point = (A.f, A.fov) if (A is not None) else None B_point = (B.f, B.fov) C_point = (C.f, C.fov) if (C is not None) else None # stuffed all operations into one function for encapsulation bez_a, bez_d = scalar_calculate_ideal_bezier_slope( A_point, B_point, C_point) # store it thisframe_bezier_approach.append(bez_a) thisframe_bezier_depart.append(bez_d) # next i need to store them in some sensible manner # ..., [approach posx,y,z,rot], [depart posx,y,z,rot], ... thisbone_bezier_slopes.append(thisframe_bezier_approach) thisbone_bezier_slopes.append(thisframe_bezier_depart) pass # end "for each frame in this bone" # now i have calculated all the desired bezier approach/depart slopes for both rotation and position # next i need to rearrange things slightly # currently the slopes are stored in "approach,depart" pairs associated with a single frame. # but the interpolation curves are stored as "depart, approach" associated with the segment leading up to a frame. # AKA, interpolation info stored with frame i is to interpolate from i-1 to i # therefore there is no place for the slope when interpolating away from the last frame, pop it thisbone_bezier_slopes.pop(-1) # the new list needs to start with 1,1,1,1 to interpolate up to the first frame, insert it if bonename == NAME_FOR_CAMFRAMES: thisbone_bezier_slopes.insert(0, [1] * 6) else: thisbone_bezier_slopes.insert(0, [1] * 4) # now every pair is a "depart,approach" associated with a single frame final = [] for i in range(0, len(thisbone_bezier_slopes), 2): # now store as pairs final.append( [thisbone_bezier_slopes[i], thisbone_bezier_slopes[i + 1]]) assert len(final) == len(boneframe_list) # save it! allbone_bezier_slopes[bonename] = final pass # end of "for each bone # >>>>>> part 2: calculate the x/y position of the control points for the curve, based on the slope core.MY_PRINT_FUNC("Calculating control points...") allbone_bezier_points = {} for bonename in sorted(allbone_bezier_slopes.keys()): bezier_for_one_frame = allbone_bezier_slopes[bonename] thisbone_bezier_points = [] for depart_slopes, approach_slopes in bezier_for_one_frame: slopes_per_channel = list(zip(depart_slopes, approach_slopes)) # print(slopes_per_channel) depart_points = [] approach_points = [] for depart_slope, approach_slope in slopes_per_channel: # now i have the approach & depart for one channel of one frame of one bone # 1. handle double-sided cutpoint if approach_slope == -1 and depart_slope == -1: # this is a double-sided cutpoint! # see where the global is declared to understand the modes if HOW_TO_HANDLE_DOUBLE_CUTPOINT == 1: approach_slope, depart_slope = 0, 0 else: #elif HOW_TO_HANDLE_DOUBLE_CUTPOINT == 2: approach_slope, depart_slope = 1, 1 # 3a. in this mode the cutpoint is handled BEFORE normal calculation if HOW_TO_HANDLE_SINGLE_SIDE_CUTPOINT == 1: if approach_slope == -1: approach_slope = 0 if depart_slope == -1: depart_slope = 0 # 2. base case: calculate the point position based on the slope depart_point = (10, 10) approach_point = (117, 117) if approach_slope != -1: # note: the approach point is based on 127,127 approach_point = tuple( 127 - p for p in make_point_from_slope(approach_slope)) if depart_slope != -1: depart_point = make_point_from_slope(depart_slope) # 3b. handle the one-sided cutpoint if HOW_TO_HANDLE_SINGLE_SIDE_CUTPOINT == 2: # fancy "point at the control point of the other side" idea # define the slope via the opposing control point and re-run step 2 if approach_slope == -1: # note: depart_point[0] can be 127, if so then this is divide by 0 if depart_point[0] == 127: approach_slope = 1000 else: approach_slope = (depart_point[1] - 127) / (depart_point[0] - 127) # note: the approach point is based on 127,127 approach_point = tuple( 127 - p for p in make_point_from_slope(approach_slope)) if depart_slope == -1: # note: approach_point[0] CAN BE 0, in theory. if approach_point[0] == 0: depart_slope = 1000 else: depart_slope = approach_point[1] / approach_point[0] depart_point = make_point_from_slope(depart_slope) # 4. accumulate teh channels depart_points.append(depart_point) approach_points.append(approach_point) pass # end "for one channel of one frame of one bone" # 5. accumulate all the frames thisbone_bezier_points.append([depart_points, approach_points]) pass # end "for one frame of one bone" # 6. accumulate teh bones allbone_bezier_points[bonename] = thisbone_bezier_points pass # end "for one bone" # >>>>>> part 3: store this into the boneframe & un-dictify the frames to put it back into the VMD for bonename in sorted(boneframe_dict.keys()): boneframe_list = boneframe_dict[bonename] bezier_points_list = allbone_bezier_points[bonename] if bonename == NAME_FOR_CAMFRAMES: # this is a list of camframes! # for each frame & associated points, for camframe, b in zip(boneframe_list, bezier_points_list): # print(b) # interp = [x_ax, x_bx, x_ay, x_by, y_ax, y_bx, y_ay, y_by, z_ax, z_bx, z_ay, z_by, # r_ax, r_bx, r_ay, r_by, dist_ax, dist_bx, dist_ay, dist_by, fov_ax, fov_bx, fov_ay, fov_by] interp = [ b[0][0][0], b[1][0][0], b[0][0][1], b[1][0][1], b[0][1][0], b[1][1][0], b[0][1][1], b[1][1][1], b[0][2][0], b[1][2][0], b[0][2][1], b[1][2][1], b[0][3][0], b[1][3][0], b[0][3][1], b[1][3][1], b[0][4][0], b[1][4][0], b[0][4][1], b[1][4][1], b[0][5][0], b[1][5][0], b[0][5][1], b[1][5][1], ] camframe.interp = interp else: # for each frame & associated points, for boneframe, b in zip(boneframe_list, bezier_points_list): # print(b) # interp = [x_ax, y_ax, z_ax, r_ax, x_ay, y_ay, z_ay, r_ay, # x_bx, y_bx, z_bx, r_bx, x_by, y_by, z_by, r_by] interp = [ b[0][0][0], b[0][1][0], b[0][2][0], b[0][3][0], b[0][0][1], b[0][1][1], b[0][2][1], b[0][3][1], b[1][0][0], b[1][1][0], b[1][2][0], b[1][3][0], b[1][0][1], b[1][1][1], b[1][2][1], b[1][3][1], ] # this goes into the actual boneframe object still in the lists in boneframe_dict boneframe.interp = interp # un-dictify it! # first, extract the camframes if NAME_FOR_CAMFRAMES in boneframe_dict: vmd.camframes = boneframe_dict.pop(NAME_FOR_CAMFRAMES) # then do the boneframes # the names dont matter, make a list of all the lists in the dict asdf = list(boneframe_dict.values()) # flatten it flat_boneframe_list = [item for sublist in asdf for item in sublist] vmd.boneframes = flat_boneframe_list core.MY_PRINT_FUNC("") # write out the VMD output_filename_vmd = "%s_smoothed.vmd" % input_filename_vmd[0:-4] output_filename_vmd = core.get_unused_file_name(output_filename_vmd) vmdlib.write_vmd(output_filename_vmd, vmd, moreinfo=moreinfo) # H = plt.hist([j for j in ANGLE_SHARPNESS_FACTORS if j!=0 and j!=1], bins=40, density=True) print("factors=", len(ANGLE_SHARPNESS_FACTORS)) H = plt.hist(ANGLE_SHARPNESS_FACTORS, bins=16, density=True) plt.show() core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) ################################## # user flow: # ask for the helper bone (to be merged) # ask for the destination bone (merged onto) # try to infer proper merge factor, if it cannot infer then prompt user # then write out to file ################################## dest_idx = 0 while True: # any input is considered valid s = core.MY_GENERAL_INPUT_FUNC(lambda x: True, [ "Please specify the DESTINATION bone that weights will be transferred to.", "Enter bone #, JP name, or EN name (names are case sensitive).", "Empty input will quit the script." ]) # if empty, leave & do nothing if s == "": dest_idx = -1 break # then get the bone index from this # search JP names first dest_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == s) if dest_idx is not None: break # did i find a match? # search EN names next dest_idx = core.my_list_search(pmx.bones, lambda x: x.name_en == s) if dest_idx is not None: break # did i find a match? # try to cast to int next try: dest_idx = int(s) if 0 <= dest_idx < len(pmx.bones): break # is this within the proper bounds? else: core.MY_PRINT_FUNC("valid bone indexes are 0-%d" % (len(pmx.bones) - 1)) except ValueError: pass core.MY_PRINT_FUNC("unable to find matching bone for name '%s'" % s) if dest_idx == -1: core.MY_PRINT_FUNC("quitting") return None dest_tag = "bone #{} JP='{}' / EN='{}'".format(dest_idx, pmx.bones[dest_idx].name_jp, pmx.bones[dest_idx].name_jp) source_idx = 0 while True: # any input is considered valid s = core.MY_GENERAL_INPUT_FUNC(lambda x: True, [ "Please specify the SOURCE bone that will be merged onto %s." % dest_tag, "Enter bone #, JP name, or EN name (names are case sensitive).", "Empty input will quit the script." ]) # if empty, leave & do nothing if s == "": source_idx = -1 break # then get the morph index from this # search JP names first source_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == s) if source_idx is not None: break # did i find a match? # search EN names next source_idx = core.my_list_search(pmx.bones, lambda x: x.name_en == s) if source_idx is not None: break # did i find a match? # try to cast to int next try: source_idx = int(s) if 0 <= source_idx < len(pmx.bones): break # is this within the proper bounds? else: core.MY_PRINT_FUNC("valid bone indexes are 0-%d" % (len(pmx.bones) - 1)) except ValueError: pass core.MY_PRINT_FUNC("unable to find matching bone for name '%s'" % s) if source_idx == -1: core.MY_PRINT_FUNC("quitting") return None # print to confirm core.MY_PRINT_FUNC( "Merging bone #{} JP='{}' / EN='{}' ===> bone #{} JP='{}' / EN='{}'". format(source_idx, pmx.bones[source_idx].name_jp, pmx.bones[source_idx].name_en, dest_idx, pmx.bones[dest_idx].name_jp, pmx.bones[dest_idx].name_en)) # now try to infer the merge factor f = 0.0 if pmx.bones[source_idx].inherit_rot and pmx.bones[ source_idx].inherit_parent_idx == dest_idx and pmx.bones[ source_idx].inherit_ratio != 0: # if using partial rot inherit AND inheriting from dest_idx AND ratio != 0, use that # think this is good, if twistbones exist they should be children of preferred f = pmx.bones[source_idx].inherit_ratio elif pmx.bones[source_idx].parent_idx == dest_idx: # if they have a direct parent-child relationship, then factor is 1 f = 1 else: # otherwise, prompt for the factor factor_str = core.MY_GENERAL_INPUT_FUNC( is_float, "Unable to infer relationship, please specify a merge factor:") if factor_str == "": core.MY_PRINT_FUNC("quitting") return None f = float(factor_str) # do the actual transfer transfer_bone_weights(pmx, dest_idx, source_idx, f) # run the weight-cleanup function dummy = normalize_weights(pmx) # write out output_filename_pmx = input_filename_pmx[0:-4] + "_weightmerge.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # detect whether arm ik exists r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik) if r is None: r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik2) l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik) if l is None: l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik2) # decide whether to create or remove arm ik if r is None and l is None: # add IK branch core.MY_PRINT_FUNC(">>>> Adding arm IK <<<") # set output name if input_filename_pmx.lower().endswith(pmx_noik_suffix.lower()): output_filename = input_filename_pmx[0:-( len(pmx_noik_suffix))] + pmx_yesik_suffix else: output_filename = input_filename_pmx[0:-4] + pmx_yesik_suffix for side in [jp_l, jp_r]: # first find all 3 arm bones # even if i insert into the list, this will still be a valid reference i think bones = [] bones: List[pmxstruct.PmxBone] for n in [jp_arm, jp_elbow, jp_wrist]: i = core.my_list_search(pmx.bones, lambda x: x.name_jp == side + n, getitem=True) if i is None: core.MY_PRINT_FUNC( "ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % (side + n)) raise RuntimeError() bones.append(i) # get parent of arm bone shoulder_idx = bones[0].parent_idx # then do the "remapping" on all existing bone references, to make space for inserting 4 bones # don't delete any bones, just remap them bone_shiftmap = ([shoulder_idx + 1], [-4]) apply_bone_remapping(pmx, [], bone_shiftmap) # new bones will be inserted AFTER shoulder_idx # newarm_idx = shoulder_idx+1 # newelbow_idx = shoulder_idx+2 # newwrist_idx = shoulder_idx+3 # newik_idx = shoulder_idx+4 # make copies of the 3 armchain bones for i, b in enumerate(bones): b: pmxstruct.PmxBone # newarm = b[0:5] + [shoulder_idx + i] + b[6:8] # copy names/pos, parent, copy deform layer # newarm += [1, 0, 0, 0] # rotateable, not translateable, not visible, not enabled(?) # newarm += [1, [shoulder_idx + 2 + i], 0, 0, [], 0, []] # tail type, no inherit, no fixed axis, # newarm += b[19:21] + [0, [], 0, []] # copy local axis, no ext parent, no ik # newarm[0] += jp_ikchainsuffix # add suffix to jp name # newarm[1] += jp_ikchainsuffix # add suffix to en name newarm = pmxstruct.PmxBone( name_jp=b.name_jp + jp_ikchainsuffix, name_en=b.name_en + jp_ikchainsuffix, pos=b.pos, parent_idx=b.parent_idx, deform_layer=b.deform_layer, deform_after_phys=b.deform_after_phys, has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, tail_type=True, tail=shoulder_idx + 2 + i, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=b.has_localaxis, localaxis_x=b.localaxis_x, localaxis_z=b.localaxis_z, has_externalparent=False, has_ik=False, ) pmx.bones.insert(shoulder_idx + 1 + i, newarm) # then change the existing arm/elbow (not the wrist) to inherit rot from them if i != 2: b.inherit_rot = True b.inherit_parent_idx = shoulder_idx + 1 + i b.inherit_ratio = 1 # copy the wrist to make the IK bone en_suffix = "_L" if side == jp_l else "_R" # get index of "upperbody" to use as parent of hand IK bone ikpar = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_upperbody) if ikpar is None: core.MY_PRINT_FUNC( "ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % jp_upperbody) raise RuntimeError() # newik = [side + jp_newik, en_newik + en_suffix] + bones[2][2:5] + [ikpar] # new names, copy pos, new par # newik += bones[2][6:8] + [1, 1, 1, 1] + [0, [0,1,0]] # copy deform layer, rot/trans/vis/en, tail type # newik += [0, 0, [], 0, [], 0, [], 0, []] # no inherit, no fixed axis, no local axis, no ext parent, yes IK # # add the ik info: [is_ik, [target, loops, anglelimit, [[link_idx, []]], [link_idx, []]]] ] ] # newik += [1, [shoulder_idx+3, newik_loops, newik_angle, [[shoulder_idx+2,[]],[shoulder_idx+1,[]]] ] ] newik = pmxstruct.PmxBone( name_jp=side + jp_newik, name_en=en_newik + en_suffix, pos=bones[2].pos, parent_idx=ikpar, deform_layer=bones[2].deform_layer, deform_after_phys=bones[2].deform_after_phys, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, tail_type=False, tail=[0, 1, 0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, has_ik=True, ik_target_idx=shoulder_idx + 3, ik_numloops=newik_loops, ik_angle=newik_angle, ik_links=[ pmxstruct.PmxBoneIkLink(idx=shoulder_idx + 2), pmxstruct.PmxBoneIkLink(idx=shoulder_idx + 1) ]) pmx.bones.insert(shoulder_idx + 4, newik) # then add to dispframe # first, does the frame already exist? f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik, getitem=True) if f is None: # need to create the new dispframe! easy newframe = pmxstruct.PmxFrame(name_jp=jp_newik, name_en=en_newik, is_special=False, items=[[0, shoulder_idx + 4]]) pmx.frames.append(newframe) else: # frame already exists, also easy f.items.append([0, shoulder_idx + 4]) else: # remove IK branch core.MY_PRINT_FUNC(">>>> Removing arm IK <<<") # set output name if input_filename_pmx.lower().endswith(pmx_yesik_suffix.lower()): output_filename = input_filename_pmx[0:-( len(pmx_yesik_suffix))] + pmx_noik_suffix else: output_filename = input_filename_pmx[0:-4] + pmx_noik_suffix # identify all bones in ik chain of hand ik bones bone_dellist = [] for b in [r, l]: bone_dellist.append(b) # this IK bone bone_dellist.append( pmx.bones[b].ik_target_idx) # the target of the bone for v in pmx.bones[b].ik_links: bone_dellist.append(v.idx) # each link along the bone bone_dellist.sort() # build the remap thing bone_shiftmap = delme_list_to_rangemap(bone_dellist) # do the actual delete & shift apply_bone_remapping(pmx, bone_dellist, bone_shiftmap) # delete dispframe for hand ik # first, does the frame already exist? f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik) if f is not None: # frame already exists, delete it pmx.frames.pop(f) pass # write out output_filename = core.get_unused_file_name(output_filename) pmxlib.write_pmx(output_filename, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # the goal: extract rotation around the "arm" bone local X? axis and transfer it to rotation around the "armtwist" bone local axis # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # get bones realbones = pmx.bones twistbone_axes = [] # then, grab the "twist" bones & save their fixed-rotate axes, if they have them # fallback plan: find the arm-to-elbow and elbow-to-wrist unit vectors and use those for i in range(len(jp_twistbones)): r = core.my_list_search(realbones, lambda x: x.name_jp == jp_twistbones[i], getitem=True) if r is None: core.MY_PRINT_FUNC("ERROR1: twist bone '{}'({}) cannot be found model, unable to continue. Ensure they use the correct semistandard names, or edit the script to change the JP names it is looking for.".format(jp_twistbones[i], eng_twistbones[i])) raise RuntimeError() if r.has_fixedaxis: # this bone DOES have fixed-axis enabled! use the unit vector in r[18] twistbone_axes.append(r.fixedaxis) else: # i can infer local axis by angle from arm-to-elbow or elbow-to-wrist start = core.my_list_search(realbones, lambda x: x.name_jp == jp_sourcebones[i], getitem=True) if start is None: core.MY_PRINT_FUNC("ERROR2: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_sourcebones[i]) raise RuntimeError() end = core.my_list_search(realbones, lambda x: x.name_jp == jp_pointat_bones[i], getitem=True) if end is None: core.MY_PRINT_FUNC("ERROR3: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_pointat_bones[i]) raise RuntimeError() start_pos = start.pos end_pos = end.pos # now have both startpoint and endpoint! find the delta! delta = [b - a for a,b in zip(start_pos, end_pos)] # normalize to length of 1 length = core.my_euclidian_distance(delta) unit = [t / length for t in delta] twistbone_axes.append(unit) # done extracting axes limits from bone CSV, in list "twistbone_axes" core.MY_PRINT_FUNC("...done extracting axis limits from PMX...") ################################################################################### # prompt VMD file name core.MY_PRINT_FUNC("Please enter name of VMD dance input file:") input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd") # next, read/use/prune the dance vmd nicelist_in = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo) # sort boneframes into individual lists: one for each [Larm + Lelbow + Rarm + Relbow] and remove them from the master boneframelist # frames for all other bones stay in the master boneframelist all_sourcebone_frames = [] for sourcebone in jp_sourcebones: # partition & writeback temp, nicelist_in.boneframes = core.my_list_partition(nicelist_in.boneframes, lambda x: x.name == sourcebone) # all frames for "sourcebone" get their own sublist here all_sourcebone_frames.append(temp) # verify that there is actually arm/elbow frames to process sourcenumframes = sum([len(x) for x in all_sourcebone_frames]) if sourcenumframes == 0: core.MY_PRINT_FUNC("No arm/elbow bone frames are found in the VMD, nothing for me to do!") core.MY_PRINT_FUNC("Aborting: no files were changed") return None else: core.MY_PRINT_FUNC("...source contains " + str(sourcenumframes) + " arm/elbow bone frames to decompose...") if USE_OVERKEY_BANDAID: # to fix the path that the arms take during interpolation we need to overkey the frames # i.e. create intermediate frames that they should have been passing through already, to FORCE it to take the right path # i'm replacing the interpolation curves with actual frames for sublist in all_sourcebone_frames: newframelist = [] sublist.sort(key=lambda x: x.f) # ensure they are sorted by frame number # for each frame for i in range(1, len(sublist)): this = sublist[i] prev = sublist[i-1] # use interpolation curve i to interpolate from i-1 to i # first: do i need to do anything or are they already close on the timeline? thisframenum = this.f prevframenum = prev.f if (thisframenum - prevframenum) <= OVERKEY_FRAME_SPACING: continue # if they are far enough apart that i need to do something, thisframequat = core.euler_to_quaternion(this.rot) prevframequat = core.euler_to_quaternion(prev.rot) # 3, 7, 11, 15 = r_ax, r_ay, r_bx, r_by bez = core.MyBezier((this.interp[3], this.interp[7]), (this.interp[11], this.interp[15]), resolution=50) # create new frames at these frame numbers, spacing is OVERKEY_FRAME_SPACING for interp_framenum in range(prevframenum + OVERKEY_FRAME_SPACING, thisframenum, OVERKEY_FRAME_SPACING): # calculate the x time percentage from prev frame to this frame x = (interp_framenum - prevframenum) / (thisframenum - prevframenum) # apply the interpolation curve to translate X to Y y = bez.approximate(x) # interpolate from prev to this by amount Y interp_quat = core.my_slerp(prevframequat, thisframequat, y) # begin building the new frame newframe = vmdstruct.VmdBoneFrame( name=this.name, # same name f=interp_framenum, # overwrite frame num pos=list(this.pos), # same pos (but make a copy) rot=list(core.quaternion_to_euler(interp_quat)), # overwrite euler angles phys_off=this.phys_off, # same phys_off interp=list(core.bone_interpolation_default_linear) # overwrite interpolation ) newframelist.append(newframe) # overwrite thisframe interp curve with default too this.interp = list(core.bone_interpolation_default_linear) # overwrite custom interpolation # concat the new frames onto the existing frames for this sublist sublist += newframelist # re-count the number of frames for printing purposes totalnumframes = sum([len(x) for x in all_sourcebone_frames]) overkeyframes = totalnumframes - sourcenumframes if overkeyframes != 0: core.MY_PRINT_FUNC("...overkeying added " + str(overkeyframes) + " arm/elbow bone frames...") core.MY_PRINT_FUNC("...beginning decomposition of " + str(totalnumframes) + " arm/elbow bone frames...") # now i am completely done reading the VMD file and parsing its data! everything has been distilled down to: # all_sourcebone_frames = [Larm, Lelbow, Rarm, Relbow] plus nicelist_in[1] ################################################################################### # begin the actual calculations # output array new_twistbone_frames = [] # progress tracker curr_progress = 0 # for each sourcebone & corresponding twistbone, for (twistbone, axis_orig, sourcebone_frames) in zip(jp_twistbones, twistbone_axes, all_sourcebone_frames): # for each frame of the sourcebone, for frame in sourcebone_frames: # XYZrot = 567 euler quat_in = core.euler_to_quaternion(frame.rot) axis = list(axis_orig) # make a copy to be safe # "swing twist decomposition" # swing = "local" x rotation and nothing else # swing = sourcebone, twist = twistbone (swing, twist) = swing_twist_decompose(quat_in, axis) # modify "frame" in-place # only modify the XYZrot to use new values new_sourcebone_euler = core.quaternion_to_euler(swing) frame.rot = list(new_sourcebone_euler) # create & store new twistbone frame # name=twistbone, framenum=copy, XYZpos=copy, XYZrot=new, phys=copy, interp16=copy new_twistbone_euler = core.quaternion_to_euler(twist) newframe = vmdstruct.VmdBoneFrame( name=twistbone, f=frame.f, pos=list(frame.pos), rot=list(new_twistbone_euler), phys_off=frame.phys_off, interp=list(frame.interp) ) new_twistbone_frames.append(newframe) # print progress updates curr_progress += 1 core.print_progress_oneline(curr_progress / totalnumframes) ###################################################################### # done with calculations! core.MY_PRINT_FUNC("...done with decomposition, now reassembling output...") # attach the list of newly created boneframes, modify the original input for sublist in all_sourcebone_frames: nicelist_in.boneframes += sublist nicelist_in.boneframes += new_twistbone_frames core.MY_PRINT_FUNC("") # write out the VMD output_filename_vmd = "%s_twistbones_for_%s.vmd" % \ (input_filename_vmd[0:-4], core.get_clean_basename(input_filename_pmx)) output_filename_vmd = core.get_unused_file_name(output_filename_vmd) vmdlib.write_vmd(output_filename_vmd, nicelist_in, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # valid input is any string that can matched aginst a morph idx s = core.MY_GENERAL_INPUT_FUNC( lambda x: morph_scale.get_idx_in_pmxsublist(x, pmx.morphs) is not None, [ "Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).", "Empty input will quit the script." ]) # do it again, cuz the lambda only returns true/false target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.morphs) # when given empty text, done! if target_index == -1 or target_index is None: core.MY_PRINT_FUNC("quitting") return None morphtype = pmx.morphs[target_index].morphtype # 1=vert # 3=UV # 8=material core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format( morphtype, target_index, pmx.morphs[target_index].name_jp, pmx.morphs[target_index].name_en)) if morphtype == pmxstruct.MorphType.VERTEX: # vertex # for each item in this morph: item: pmxstruct.PmxMorphItemVertex # type annotation for pycharm for d, item in enumerate(pmx.morphs[target_index].items): # apply the offset pmx.verts[item.vert_idx].pos[0] += item.move[0] pmx.verts[item.vert_idx].pos[1] += item.move[1] pmx.verts[item.vert_idx].pos[2] += item.move[2] # invert the morph morph_scale.morph_scale(pmx.morphs[target_index], -1) elif morphtype == pmxstruct.MorphType.UV: # UV item: pmxstruct.PmxMorphItemUV # type annotation for pycharm for d, item in enumerate(pmx.morphs[target_index].items): # (vert_idx, A, B, C, D) # apply the offset pmx.verts[item.vert_idx].uv[0] += item.move[0] pmx.verts[item.vert_idx].uv[1] += item.move[1] # invert the morph morph_scale.morph_scale(pmx.morphs[target_index], -1) elif morphtype in (pmxstruct.MorphType.UV_EXT1, pmxstruct.MorphType.UV_EXT2, pmxstruct.MorphType.UV_EXT3, pmxstruct.MorphType.UV_EXT4): # UV1 UV2 UV3 UV4 whichuv = morphtype.value - pmxstruct.MorphType.UV_EXT1.value item: pmxstruct.PmxMorphItemUV # type annotation for pycharm for d, item in enumerate(pmx.morphs[target_index].items): # apply the offset pmx.verts[item.vert_idx].addl_vec4s[whichuv][0] += item.move[0] pmx.verts[item.vert_idx].addl_vec4s[whichuv][1] += item.move[1] pmx.verts[item.vert_idx].addl_vec4s[whichuv][2] += item.move[2] pmx.verts[item.vert_idx].addl_vec4s[whichuv][3] += item.move[3] # invert the morph morph_scale.morph_scale(pmx.morphs[target_index], -1) elif morphtype == pmxstruct.MorphType.MATERIAL: # material core.MY_PRINT_FUNC("WIP") # todo # to invert a material morph means inverting the material's visible/notvisible state as well as flipping the morph # hide morph add -> show morph add # hide morph mult -> show morph add # show morph add -> hide morph mult core.MY_PRINT_FUNC("quitting") return None else: core.MY_PRINT_FUNC("Unhandled morph type") core.MY_PRINT_FUNC("quitting") return None # write out output_filename_pmx = input_filename_pmx[0:-4] + ("_%dinv.pmx" % target_index) output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # to shift the model by a set amount: # first, ask user for X Y Z # create the prompt popup scale_str = core.MY_GENERAL_INPUT_FUNC( lambda x: (model_shift.is_3float(x) is not None), [ "Enter the X,Y,Z amount to scale this model by:", "Three decimal values separated by commas.", "Empty input will quit the script." ]) # if empty, quit if scale_str == "": core.MY_PRINT_FUNC("quitting") return None # use the same func to convert the input string scale = model_shift.is_3float(scale_str) uniform_scale = (scale[0] == scale[1] == scale[2]) if not uniform_scale: core.MY_PRINT_FUNC( "Warning: when scaling by non-uniform amounts, rigidbody sizes will not be modified" ) #################### # what does it mean to scale the entire model? # scale vertex position, sdef params # ? scale vertex normal vectors, then normalize? need to convince myself of this interaction # scale bone position, tail offset # scale fixedaxis and localaxis vectors, then normalize # scale vert morph, bone morph # scale rigid pos, size # scale joint pos, movelimits for v in pmx.verts: # vertex position for i in range(3): v.pos[i] *= scale[i] # vertex normal for i in range(3): if scale[i] != 0: v.norm[i] /= scale[i] else: v.norm[i] = 100000 # then re-normalize the normal vector v.norm = core.normalize_distance(v.norm) # c, r0, r1 params of every SDEF vertex # these correspond to real positions in 3d space so they need to be modified if v.weighttype == pmxstruct.WeightMode.SDEF: for param in v.weight_sdef: for i in range(3): param[i] *= scale[i] for b in pmx.bones: # bone position for i in range(3): b.pos[i] *= scale[i] # bone tail if using offset mode if not b.tail_usebonelink: for i in range(3): b.tail[i] *= scale[i] # scale fixedaxis and localaxis vectors, then normalize if b.has_fixedaxis: for i in range(3): b.fixedaxis[i] *= scale[i] # then re-normalize b.fixedaxis = core.normalize_distance(b.fixedaxis) # scale fixedaxis and localaxis vectors, then normalize if b.has_localaxis: for i in range(3): b.localaxis_x[i] *= scale[i] for i in range(3): b.localaxis_z[i] *= scale[i] # then re-normalize b.localaxis_x = core.normalize_distance(b.localaxis_x) b.localaxis_z = core.normalize_distance(b.localaxis_z) for m in pmx.morphs: # vertex morph and bone morph (only translate, not rotate) if m.morphtype in (pmxstruct.MorphType.VERTEX, pmxstruct.MorphType.BONE): morph_scale.morph_scale(m, scale, bone_mode=1) for rb in pmx.rigidbodies: # rigid body position for i in range(3): rb.pos[i] *= scale[i] # rigid body size # NOTE: rigid body size is a special conundrum # spheres have only one dimension, capsules have two, and only boxes have 3 # what's the "right" way to scale a sphere by 1,5,1? there isn't a right way! # boxes and capsules can be rotated and stuff so their axes dont line up with world axes, too # is it at least possible to rotate bodies so they are still aligned with their bones? # eh, why even bother with any of that. 95% of the time full-model scale will be uniform scaling. # only scale the rigidbody size if doing uniform scaling: that is guaranteed to be safe! if uniform_scale: for i in range(3): rb.size[i] *= scale[i] for j in pmx.joints: # joint position for i in range(3): j.pos[i] *= scale[i] # joint min slip for i in range(3): j.movemin[i] *= scale[i] # joint max slip for i in range(3): j.movemax[i] *= scale[i] # that's it? that's it! # write out output_filename_pmx = input_filename_pmx[0:-4] + "_scale.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=False): core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # texture sorting plan: # 1. get startpath = basepath of input PMX # 2. get lists of relevant files # 2a. get list of ALL files within the tree, relative to startpath # 2b. extract top-level 'neighbor' pmx files from all-set # 2c. remove files i intend to ignore (filter by file ext or containing folder) # 3. ask about modifying neighbor PMX # 4. read PMX: either target or target+all neighbor # 5. "categorize files & normalize usages within PMX", NEW FUNC!!! # inputs: list of PMX obj, list of relevant files # outputs: list of structs that bundle all relevant info about the file (replace 2 structs currently used) # for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk # now have all files, know their states! # 6. ask for "aggression level" to control how files will be moved # 7. determine new names for files # this is the big one, slightly different logic for different categories # 8. print proposed names & other findings # for unused files under a folder, combine & replace with *** # 9. ask for confirmation # 10. zip backup (NEW FUNC!) # 11. apply renaming, NEW FUNC! # first try to rename all files # could plausibly fail, if so, set to-name to None/blank # then, in the PMXs, rename all files that didn't fail # absolute path to directory holding the pmx input_filename_pmx_abs = os.path.normpath( os.path.abspath(input_filename_pmx)) startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files relative_all_exist_files = walk_filetree_from_root(startpath) core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files)) # now fill "neighbor_pmx" by finding files without path separator that end in PMX # these are relative paths tho neighbor_pmx = [ f for f in relative_all_exist_files if (f.lower().endswith(".pmx")) and ( os.path.sep not in f) and f != input_filename_pmx_rel ] relevant_exist_files = [] for f in relative_all_exist_files: # ignore all files I expect to find alongside a PMX and don't want to touch or move if f.lower().endswith(IGNORE_FILETYPES): continue # ignore any files living below/inside 'special' folders like "fx/" if match_folder_anylevel(f, IGNORE_FOLDERS, toponly=False): continue # create the list of files we know exist and we know we care about relevant_exist_files.append(f) core.MY_PRINT_FUNC("RELEVANT EXISTING FILES:", len(relevant_exist_files)) core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx)) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # now ask if I care about the neighbors and read the PMXes into memory pmx_filenames = [input_filename_pmx_rel] if neighbor_pmx: core.MY_PRINT_FUNC("") info = [ "Detected %d top-level neighboring PMX files, these probably share the same filebase as the target." % len(neighbor_pmx), "If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.", "Do you want to process all neighbors in addition to the target? (highly recommended)", "1 = Yes, 2 = No" ] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 1: core.MY_PRINT_FUNC("Processing target + all neighbor files") # append neighbor PMX files onto the list of files to be processed pmx_filenames += neighbor_pmx else: core.MY_PRINT_FUNC( "WARNING: Processing only target, ignoring %d neighbor PMX files" % len(neighbor_pmx)) # now read all the PMX objects & store in dict alongside the relative name # dictionary where keys are filename and values are resulting pmx objects all_pmx_obj = {} for this_pmx_name in pmx_filenames: this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name), moreinfo=moreinfo) all_pmx_obj[this_pmx_name] = this_pmx_obj # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk # also fill out how much and how each file is used, and unify dupes between files, all that good stuff filerecord_list = categorize_files(all_pmx_obj, relevant_exist_files, moreinfo) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # now check which files are used/unused/dont exist # break this into used/notused/notexist lists for simplicity sake # all -> used + notused # used -> used_exist + used_notexist # notused -> notused_img + notused_notimg used, notused = core.my_list_partition(filerecord_list, lambda q: q.numused != 0) used_exist, used_notexist = core.my_list_partition(used, lambda q: q.exists) notused_img, notused_notimg = core.my_list_partition( notused, lambda q: q.name.lower().endswith(IMG_EXT)) core.MY_PRINT_FUNC("PMX TEXTURE SOURCES:", len(used)) if moreinfo: for x in used: core.MY_PRINT_FUNC(" " + str(x)) # now: # all duplicates have been resolved within PMX, including modifying the PMX # all duplicates have been resolved across PMXes # all file exist/notexist status is known # all file used/notused status is known (via numused), or used_pmx # all ways a file is used is known move_toplevel_unused_img = True move_all_unused_img = False # only ask what files to move if there are files that could potentially be moved if notused_img: # count the number of toplevel vs not-toplevel in "notused_img" num_toplevel = len( [p for p in notused_img if (os.path.sep not in p.name)]) num_nontoplevel = len(notused_img) - num_toplevel # ask the user what "aggression" level they want showinfo = [ "Detected %d unused top-level files and %d unused files in directories." % (num_toplevel, num_nontoplevel), "Which files do you want to move to 'unused' folder?", "1 = Do not move any, 2 = Move only top-level unused, 3 = Move all unused" ] c = core.MY_SIMPLECHOICE_FUNC((1, 2, 3), showinfo) if c == 2: move_toplevel_unused_img = True move_all_unused_img = False elif c == 3: move_toplevel_unused_img = True move_all_unused_img = True else: # c == 1: move_toplevel_unused_img = False move_all_unused_img = False # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # DETERMINE NEW NAMES FOR FILES # how to remap: build a list of all destinations (lowercase) to see if any proposed change would lead to collision all_new_names = set() # don't touch the unused_notimg files at all, unless some flag is set # not-used top-level image files get moved to 'unused' folder # also all spa/sph get renamed to .bmp (but remember these are all unused so i don't need to update them in the pmx) for p in notused_img: newname = remove_pattern(p.name) if ((os.path.sep not in p.name) and move_toplevel_unused_img) or move_all_unused_img: # this deserves to be moved to 'unused' folder! newname = os.path.join(FOLDER_UNUSED, os.path.basename(newname)) # ensure the extension is lowercase, for cleanliness dot = newname.rfind(".") newname = newname[:dot] + newname[dot:].lower() if CONVERT_SPA_SPH_TO_BMP and newname.endswith((".spa", ".sph")): newname = newname[:-4] + ".bmp" # if the name I build is not the name it already has, queue it for actual rename if newname != p.name: # resolve potential collisions by adding numbers suffix to file names # first need to make path absolute so get_unused_file_name can check the disk. # then check uniqueness against files on disk and files in namelist (files that WILL be on disk) newname = core.get_unused_file_name(os.path.join( startpath, newname), namelist=all_new_names) # now dest path is guaranteed unique against other existing files & other proposed name changes all_new_names.add(newname.lower()) # make the path no longer absolute: undo adding "startpath" above newname = os.path.relpath(newname, startpath) p.newname = newname # used files get sorted into tex/toon/sph/multi (unless tex and already in a folder that says clothes, etc) # all SPH/SPA get renamed to BMP, used or unused for p in used_exist: newname = remove_pattern(p.name) usage_list = list(p.usage) if len(p.usage) != 1: # this is a rare multiple-use file newname = os.path.join(FOLDER_MULTI, os.path.basename(newname)) elif usage_list[0] == FOLDER_SPH: # this is an sph, duh if not match_folder_anylevel( p.name, KEEP_FOLDERS_SPH, toponly=True): # if its name isn't already good, then move it to my new location newname = os.path.join(FOLDER_SPH, os.path.basename(newname)) elif usage_list[0] == FOLDER_TOON: # this is a toon, duh if not match_folder_anylevel( p.name, KEEP_FOLDERS_TOON, toponly=True): # if its name isn't already good, then move it to my new location newname = os.path.join(FOLDER_TOON, os.path.basename(newname)) elif usage_list[0] == FOLDER_TEX: # if a tex AND already in a folder like body, clothes, wear, tex, etc then keep that folder if not match_folder_anylevel( p.name, KEEP_FOLDERS_TEX, toponly=True): # if its name isn't already good, then move it to my new location newname = os.path.join(FOLDER_TEX, os.path.basename(newname)) # ensure the extension is lowercase, for cleanliness dot = newname.rfind(".") newname = newname[:dot] + newname[dot:].lower() if CONVERT_SPA_SPH_TO_BMP and newname.lower().endswith( (".spa", ".sph")): newname = newname[:-4] + ".bmp" # if the name I build is not the name it already has, queue it for actual rename if newname != p.name: # resolve potential collisions by adding numbers suffix to file names # first need to make path absolute so get_unused_file_name can check the disk. # then check uniqueness against files on disk and files in namelist (files that WILL be on disk) newname = core.get_unused_file_name(os.path.join( startpath, newname), namelist=all_new_names) # now dest path is guaranteed unique against other existing files & other proposed name changes all_new_names.add(newname.lower()) # make the path no longer absolute: undo adding "startpath" above newname = os.path.relpath(newname, startpath) p.newname = newname # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # NOW PRINT MY PROPOSED RENAMINGS and other findings # isolate the ones with proposed renaming used_rename = [u for u in used_exist if u.newname is not None] notused_img_rename = [u for u in notused_img if u.newname is not None] notused_img_norename = [u for u in notused_img if u.newname is None] # bonus goal: if ALL files under a folder are unused, replace its name with a star # first build dict of each dirs to each file any depth below that dir all_dirnames = {} for f in relative_all_exist_files: d = os.path.dirname(f) while d != "": try: all_dirnames[d].append(f) except KeyError: all_dirnames[d] = [f] d = os.path.dirname(d) unused_dirnames = [] all_notused_searchable = [x.name for x in notused_img_norename ] + [x.name for x in notused_notimg] for d, files_under_d in all_dirnames.items(): # if all files beginning with d are notused (either type), this dir can be replaced with * # note: min crashes if input list is empty, but this is guaranteed to not be empty dir_notused = min([(f in all_notused_searchable) for f in files_under_d]) if dir_notused: unused_dirnames.append(d) # print("allundir", unused_dirnames) # now, remove all dirnames that are encompassed by another dirname j = 0 while j < len(unused_dirnames): dj = unused_dirnames[j] k = 0 while k < len(unused_dirnames): dk = unused_dirnames[k] if dj != dk and dk.startswith(dj): unused_dirnames.pop(k) else: k += 1 j += 1 # make sure unused_dirnames has the deepest directories first unused_dirnames = sorted(unused_dirnames, key=lambda y: y.count(os.path.sep), reverse=True) # print("unqundir", unused_dirnames) # then as I go to print notused_img_norename or notused_notimg, collapse them? # for each section, if it exists, print its names sorted first by directory depth then alphabetically (case insensitive) if used_notexist: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC( "Found %d references to images that don't exist (no proposed changes)" % len(used_notexist)) for p in sorted(used_notexist, key=lambda y: sortbydirdepth(y.name)): # print orig name, usage modes, # used, and # files that use it core.MY_PRINT_FUNC(" " + str(p)) if notused_img_norename: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC( "Found %d not-used images in the file tree (no proposed changes)" % len(notused_img_norename)) printme = set() for p in notused_img_norename: # is this notused-file anywhere below any unused dir? t = False for d in unused_dirnames: if p.name.startswith(d): # add this dir, not this file, to the print set printme.add(os.path.join(d, "***")) t = True if not t: # if not encompassed by an unused dir, add the filename printme.add(p.name) # convert set back to sorted list printme = sorted(list(printme), key=sortbydirdepth) for s in printme: core.MY_PRINT_FUNC(" " + s) if notused_notimg: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC( "Found %d not-used not-images in the file tree (no proposed changes)" % len(notused_notimg)) printme = set() for p in notused_notimg: # is this notused-file anywhere below any unused dir? t = False for d in unused_dirnames: if p.name.startswith(d): # add this dir, not this file, to the print set printme.add(os.path.join(d, "***")) t = True if not t: # if not encompassed by an unused dir, add the filename printme.add(p.name) # convert set back to sorted list printme = sorted(list(printme), key=sortbydirdepth) for s in printme: core.MY_PRINT_FUNC(" " + s) # print with all "from" file names left-justified so all the arrows are nicely lined up (unless they use jp characters) longest_name_len = 0 for p in used_rename: longest_name_len = max(longest_name_len, len(p.name)) for p in notused_img_rename: longest_name_len = max(longest_name_len, len(p.name)) if used_rename: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC("Found %d used files to be moved/renamed:" % len(used_rename)) oldname_list = core.MY_JUSTIFY_STRINGLIST( [p.name for p in used_rename]) newname_list = [p.newname for p in used_rename] zipped = list(zip(oldname_list, newname_list)) zipped_and_sorted = sorted(zipped, key=lambda y: sortbydirdepth(y[0])) for o, n in zipped_and_sorted: # print 'from' with the case/separator it uses in the PMX core.MY_PRINT_FUNC(" {:s} --> {:s}".format(o, n)) if notused_img_rename: core.MY_PRINT_FUNC("=" * 60) core.MY_PRINT_FUNC("Found %d not-used images to be moved/renamed:" % len(notused_img_rename)) oldname_list = core.MY_JUSTIFY_STRINGLIST( [p.name for p in notused_img_rename]) newname_list = [p.newname for p in notused_img_rename] zipped = list(zip(oldname_list, newname_list)) zipped_and_sorted = sorted(zipped, key=lambda y: sortbydirdepth(y[0])) for o, n in zipped_and_sorted: # print 'from' with the case/separator it uses in the PMX core.MY_PRINT_FUNC(" {:s} --> {:s}".format(o, n)) core.MY_PRINT_FUNC("=" * 60) if not (used_rename or notused_img_rename): core.MY_PRINT_FUNC("No proposed file changes") core.MY_PRINT_FUNC("Aborting: no files were changed") return None info = [ "Do you accept these new names/locations?", "1 = Yes, 2 = No (abort)" ] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 2: core.MY_PRINT_FUNC("Aborting: no files were changed") return None # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # finally, do the actual renaming: # first, create a backup of the folder if MAKE_BACKUP_BEFORE_RENAMES: r = make_zipfile_backup(startpath, BACKUP_SUFFIX) if not r: # this happens if the backup failed somehow AND the user decided to quit core.MY_PRINT_FUNC("Aborting: no files were changed") return None # do all renaming on disk and in PMXes, and also handle the print statements apply_file_renaming(all_pmx_obj, filerecord_list, startpath) # write out for this_pmx_name, this_pmx_obj in all_pmx_obj.items(): # NOTE: this is OVERWRITING THE PREVIOUS PMX FILE, NOT CREATING A NEW ONE # because I make a zipfile backup I don't need to feel worried about preserving the old version output_filename_pmx = os.path.join(startpath, this_pmx_name) # output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=False): # step zero: verify that Pillow exists if Image is None: core.MY_PRINT_FUNC("ERROR: Python library 'Pillow' not found. This script requires this library to run!") core.MY_PRINT_FUNC("This script cannot be ran from the EXE version, the Pillow library is too large to package into the executable.") core.MY_PRINT_FUNC("To install Pillow, please use the command 'pip install Pillow' in the Windows command prompt and then run the Python scripts directly.") return None # print pillow version just cuz core.MY_PRINT_FUNC("Using Pillow version '%s'" % Image.__version__) core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # absolute path to directory holding the pmx input_filename_pmx_abs = os.path.normpath(os.path.abspath(input_filename_pmx)) startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files relative_all_exist_files = file_sort_textures.walk_filetree_from_root(startpath) core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files)) # now fill "neighbor_pmx" by finding files without path separator that end in PMX # these are relative paths tho neighbor_pmx = [f for f in relative_all_exist_files if (f.lower().endswith(".pmx")) and (os.path.sep not in f) and f != input_filename_pmx_rel] core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx)) # filter down to just image files relevant_exist_files = [f for f in relative_all_exist_files if f.lower().endswith(IMG_EXT)] core.MY_PRINT_FUNC("RELEVANT EXISTING FILES:", len(relevant_exist_files)) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # now ask if I care about the neighbors and read the PMXes into memory pmx_filenames = [input_filename_pmx_rel] if neighbor_pmx: core.MY_PRINT_FUNC("") info = [ "Detected %d top-level neighboring PMX files, these probably share the same filebase as the target." % len(neighbor_pmx), "If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.", "Do you want to process all neighbors in addition to the target? (highly recommended)", "1 = Yes, 2 = No"] r = core.MY_SIMPLECHOICE_FUNC((1, 2), info) if r == 1: core.MY_PRINT_FUNC("Processing target + all neighbor files") # append neighbor PMX files onto the list of files to be processed pmx_filenames += neighbor_pmx else: core.MY_PRINT_FUNC("WARNING: Processing only target, ignoring %d neighbor PMX files" % len(neighbor_pmx)) # now read all the PMX objects & store in dict alongside the relative name # dictionary where keys are filename and values are resulting pmx objects all_pmx_obj = {} for this_pmx_name in pmx_filenames: this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name), moreinfo=moreinfo) all_pmx_obj[this_pmx_name] = this_pmx_obj # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk # also fill out how much and how each file is used, and unify dupes between files, all that good stuff filerecord_list = file_sort_textures.categorize_files(all_pmx_obj, relevant_exist_files, moreinfo) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # DETERMINE NEW NAMES FOR FILES # first, create a backup of the folder # save the name, so that i can delete it if i didn't make any changes zipfile_name = "" if MAKE_BACKUP_ZIPFILE: r = file_sort_textures.make_zipfile_backup(startpath, BACKUP_SUFFIX) if not r: # this happens if the backup failed somehow AND the user decided to quit core.MY_PRINT_FUNC("Aborting: no files were changed") return None zipfile_name = r # name used for temporary location tempfilename = os.path.join(startpath,"temp_image_file_just_delete_me.png") pil_cannot_inspect = 0 pil_cannot_inspect_list = [] pil_imgext_mismatch = 0 num_recompressed = 0 # list of memory saved by recompressing each file. same order/length as "image_filerecords" mem_saved = [] # make image persistient, so I know it always exists and I can always call "close" before open im = None # only iterate over images that exist, obviously image_filerecords = [f for f in filerecord_list if f.exists] # iterate over the images for i, p in enumerate(image_filerecords): abspath = os.path.join(startpath, p.name) orig_size = os.path.getsize(abspath) # if not moreinfo, then each line overwrites the previous like a progress printout does # if moreinfo, then each line is printed permanently core.MY_PRINT_FUNC("...analyzing {:>3}/{:>3}, file='{}', size={} ".format( i+1, len(image_filerecords), p.name, core.prettyprint_file_size(orig_size)), is_progress=(not moreinfo)) mem_saved.append(0) # before opening, try to close it just in case if im is not None: im.close() # open the image & catch all possible errors try: im = Image.open(abspath) except FileNotFoundError as eeee: core.MY_PRINT_FUNC("FILESYSTEM MALFUNCTION!!", eeee.__class__.__name__, eeee) core.MY_PRINT_FUNC("os.walk created a list of all filenames on disk, but then this filename doesn't exist when i try to open it?") im = None except OSError as eeee: # this has 2 causes, "Unsupported BMP bitfields layout" or "cannot identify image file" if DEBUG: print("CANNOT INSPECT!1", eeee.__class__.__name__, eeee, p.name) im = None except NotImplementedError as eeee: # this is because there's some DDS format it can't make sense of if DEBUG: print("CANNOT INSPECT!2", eeee.__class__.__name__, eeee, p.name) im = None if im is None: pil_cannot_inspect += 1 pil_cannot_inspect_list.append(p.name) continue if im.format not in IMG_TYPE_TO_EXT: core.MY_PRINT_FUNC("WARNING: file '%s' has unusual image format '%s', attempting to continue" % (p.name, im.format)) # now the image is successfully opened! newname = p.name base, currext = os.path.splitext(newname) # 1, depending on image format, attempt to re-save as PNG if im.format not in IM_FORMAT_ALWAYS_SKIP: # delete temp file if it still exists if os.path.exists(tempfilename): try: os.remove(tempfilename) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR1: failed to delete temp image file '%s' during processing" % tempfilename) break # save to tempfilename with png format, use optimize=true try: im.save(tempfilename, format="PNG", optimize=True) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR2: failed to re-compress image '%s', original not modified" % p.name) continue # measure & compare file size new_size = os.path.getsize(tempfilename) diff = orig_size - new_size # if using a 16-bit BMP format, re-save back to bmp with same name is_bad_bmp = False if im.format == "BMP": try: # this might fail, images are weird, sometimes they don't have the attributes i expect if im.tile[0][3][0] in KNOWN_BAD_FORMATS: is_bad_bmp = True except Exception as e: if DEBUG: print(e.__class__.__name__, e, "BMP THING", p.name, im.tile) if diff > (REQUIRED_COMPRESSION_AMOUNT_KB * 1024) \ or is_bad_bmp\ or im.format in IM_FORMAT_ALWAYS_CONVERT: # if it frees up at least XXX kb, i will keep it! # also keep it if it is a bmp encoded with 15-bit or 16-bit colors # set p.newname = png, and delete original and move tempname to base.png try: # delete original os.remove(os.path.join(startpath, p.name)) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR3: failed to delete old image '%s' after recompressing" % p.name) continue newname = base + ".png" # resolve potential collisions by adding numbers suffix to file names # first need to make path absolute so get_unused_file_name can check the disk. newname = os.path.join(startpath, newname) # then check uniqueness against files on disk newname = core.get_unused_file_name(newname) # now dest path is guaranteed unique against other existing files # make the path no longer absolute: undo adding "startpath" above newname = os.path.relpath(newname, startpath) try: # move new into place os.rename(tempfilename, os.path.join(startpath, newname)) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR4: after deleting original '%s', failed to move recompressed version into place!" % p.name) continue num_recompressed += 1 p.newname = newname mem_saved[-1] = diff continue # if succesfully re-saved, do not do the extension-checking below # if this is not sufficiently compressed, do not use "continue", DO hit the extension-checking below # 2, if the file extension doesn't match with the image type, then make it match # this only happens if the image was not re-saved above if im.format in IMG_TYPE_TO_EXT and currext not in IMG_TYPE_TO_EXT[im.format]: newname = base + IMG_TYPE_TO_EXT[im.format][0] # resolve potential collisions by adding numbers suffix to file names # first need to make path absolute so get_unused_file_name can check the disk. newname = os.path.join(startpath, newname) # then check uniqueness against files on disk newname = core.get_unused_file_name(newname) # now dest path is guaranteed unique against other existing files # make the path no longer absolute: undo adding "startpath" above newname = os.path.relpath(newname, startpath) # do the actual rename here & now try: # os.renames creates all necessary intermediate folders needed for the destination # it also deletes the source folders if they become empty after the rename operation os.renames(os.path.join(startpath, p.name), os.path.join(startpath, newname)) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR5: unable to rename file '%s' --> '%s', attempting to continue with other file rename operations" % (p.name, newname)) continue pil_imgext_mismatch += 1 p.newname = newname continue # these must be the same length after iterating assert len(mem_saved) == len(image_filerecords) # if the image is still open, close it if im is not None: im.close() # delete temp file if it still exists if os.path.exists(tempfilename): try: os.remove(tempfilename) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("WARNING: failed to delete temp image file '%s' after processing" % tempfilename) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # are there any with proposed renaming? if not any(u.newname is not None for u in image_filerecords): core.MY_PRINT_FUNC("No proposed file changes") # if nothing was changed, delete the backup zip! core.MY_PRINT_FUNC("Deleting backup archive") if os.path.exists(zipfile_name): try: os.remove(zipfile_name) except OSError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("WARNING: failed to delete pointless zip file '%s'" % zipfile_name) core.MY_PRINT_FUNC("Aborting: no files were changed") return None # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # finally, do the actual renaming: # do all renaming in PMXes file_sort_textures.apply_file_renaming(all_pmx_obj, image_filerecords, startpath, skipdiskrename=True) # write out for this_pmx_name, this_pmx_obj in all_pmx_obj.items(): # NOTE: this is OVERWRITING THE PREVIOUS PMX FILE, NOT CREATING A NEW ONE # because I make a zipfile backup I don't need to feel worried about preserving the old version output_filename_pmx = os.path.join(startpath, this_pmx_name) # output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # NOW PRINT MY RENAMINGS and other findings filerecord_with_savings = zip(image_filerecords, mem_saved) changed_files = [u for u in filerecord_with_savings if u[0].newname is not None] core.MY_PRINT_FUNC("="*60) if pil_cannot_inspect: core.MY_PRINT_FUNC("WARNING: failed to inspect %d image files, these must be handled manually" % pil_cannot_inspect) core.MY_PRINT_FUNC(pil_cannot_inspect_list) if num_recompressed: core.MY_PRINT_FUNC("Recompressed %d images! %s of disk space has been freed" % (num_recompressed, core.prettyprint_file_size(sum(mem_saved)))) if pil_imgext_mismatch: core.MY_PRINT_FUNC("Renamed %d images that had incorrect extensions (included below)" % pil_imgext_mismatch) oldname_list = [p[0].name for p in changed_files] oldname_list_j = core.MY_JUSTIFY_STRINGLIST(oldname_list) newname_list = [p[0].newname for p in changed_files] newname_list_j = core.MY_JUSTIFY_STRINGLIST(newname_list) savings_list = [("" if p[1]==0 else "saved " + core.prettyprint_file_size(p[1])) for p in changed_files] zipped = list(zip(oldname_list_j, newname_list_j, savings_list)) zipped_and_sorted = sorted(zipped, key=lambda y: file_sort_textures.sortbydirdepth(y[0])) for o,n,s in zipped_and_sorted: # print 'from' with the case/separator it uses in the PMX core.MY_PRINT_FUNC(" {:s} --> {:s} | {:s}".format(o, n, s)) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # get bones realbones = pmx.bones # then, make 2 lists: one starting from jp_righttoe, one starting from jp_lefttoe # start from each "toe" bone (names are known), go parent-find-parent-find until reaching no-parent bonechain_r = build_bonechain(realbones, jp_righttoe) bonechain_l = build_bonechain(realbones, jp_lefttoe) # assert that the bones were found, have correct names, and are in the correct positions # also verifies that they are direct parent-child with nothing in between try: assert bonechain_r[-1].name == jp_righttoe assert bonechain_r[-2].name == jp_rightfoot assert bonechain_l[-1].name == jp_lefttoe assert bonechain_l[-2].name == jp_leftfoot except AssertionError: core.MY_PRINT_FUNC( "ERROR: unexpected structure found for foot/toe bones, verify semistandard names and structure" ) raise RuntimeError() # then walk down these 2 lists, add each name to a set: build union of all relevant bones relevant_bones = set() for b in bonechain_r + bonechain_l: relevant_bones.add(b.name) # check if waist-cancellation bones are in "relevant_bones", print a warning if they are if jp_left_waistcancel in relevant_bones or jp_right_waistcancel in relevant_bones: # TODO LOW: i probably could figure out how to support them but this whole script is useless so idgaf core.MY_PRINT_FUNC( "Warning: waist-cancellation bones found in the model! These are not supported, tool may produce bad results! Attempting to continue..." ) # also need to find initial positions of ik bones (names are known) # build a full parentage-chain for each leg bonechain_ikr = build_bonechain(realbones, jp_righttoe_ik) bonechain_ikl = build_bonechain(realbones, jp_lefttoe_ik) # verify that the ik bones were found, have correct names, and are in the correct positions try: assert bonechain_ikr[-1].name == jp_righttoe_ik assert bonechain_ikr[-2].name == jp_rightfoot_ik assert bonechain_ikl[-1].name == jp_lefttoe_ik assert bonechain_ikl[-2].name == jp_leftfoot_ik except AssertionError: core.MY_PRINT_FUNC( "ERROR: unexpected structure found for foot/toe IK bones, verify semistandard names and structure" ) raise RuntimeError() # verify that the bonechains are symmetric in length try: assert len(bonechain_l) == len(bonechain_r) assert len(bonechain_ikl) == len(bonechain_ikr) except AssertionError: core.MY_PRINT_FUNC( "ERROR: unexpected structure found, model is not left-right symmetric" ) raise RuntimeError() # determine how many levels of parentage, this value "t" should hold the first level where they are no longer shared t = 0 while bonechain_l[t].name == bonechain_ikl[t].name: t += 1 # back off one level lowest_shared_parent = t - 1 # now i am completely done with the bones CSV, all the relevant info has been distilled down to: # !!! bonechain_r, bonechain_l, bonechain_ikr, bonechain_ikl, relevant_bones core.MY_PRINT_FUNC("...identified " + str(len(bonechain_l)) + " bones per leg-chain, " + str(len(relevant_bones)) + " relevant bones total") core.MY_PRINT_FUNC("...identified " + str(len(bonechain_ikl)) + " bones per IK leg-chain") ################################################################################### # prompt VMD file name core.MY_PRINT_FUNC("Please enter name of VMD dance input file:") input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd") nicelist_in = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo) # check if this VMD uses IK or not, print a warning if it does any_ik_on = False for ikdispframe in nicelist_in.ikdispframes: for ik_bone in ikdispframe.ikbones: if ik_bone.enable is True: any_ik_on = True break if any_ik_on: core.MY_PRINT_FUNC( "Warning: the input VMD already has IK enabled, there is no point in running this script. Attempting to continue..." ) # reduce down to only the boneframes for the relevant bones # also build a list of each framenumber with a frame for a bone we care about relevant_framenums = set() boneframe_list = [] for boneframe in nicelist_in.boneframes: if boneframe.name in relevant_bones: boneframe_list.append(boneframe) relevant_framenums.add(boneframe.f) # sort the boneframes by frame number boneframe_list.sort(key=lambda x: x.f) # make the relevant framenumbers also an ascending list relevant_framenums = sorted(list(relevant_framenums)) boneframe_dict = dict() # now restructure the data from a list to a dictionary, keyed by bone name. also discard excess data when i do for b in boneframe_list: if b.name not in boneframe_dict: boneframe_dict[b.name] = [] # only storing the frame#(1) + position(234) + rotation values(567) saveme = [b.f, *b.pos, *b.rot] boneframe_dict[b.name].append(saveme) core.MY_PRINT_FUNC( "...running interpolation to rectangularize the frames...") has_warned = False # now fill in the blanks by using interpolation, if needed for key, bone in boneframe_dict.items(): # for each bone, # start a list of frames generated by interpolation interpframe_list = [] i = 0 j = 0 while j < len(relevant_framenums): # for each frame it should have, if i == len(bone): # if i is beyond end of bone, then copy the values from the last frame and use as a new frame newframe = [relevant_framenums[j]] + bone[-1][1:7] interpframe_list.append(newframe) j += 1 elif bone[i][0] == relevant_framenums[j]: # does it have it? i += 1 j += 1 else: # TODO LOW: i could modify this to include my interpolation curve math now that I understand it, but i dont care if not has_warned: core.MY_PRINT_FUNC( "Warning: interpolation is needed but interpolation curves are not fully tested! Assuming linear interpolation..." ) has_warned = True # if there is a mismatch then the target framenum is less than the boneframe framenum # build a frame that has frame# + position(123) + rotation values(456) newframe = [relevant_framenums[j]] # if target is less than the current boneframe, interp between here and prev boneframe for p in range(1, 4): # interpolate for each position offset newframe.append( core.linear_map(bone[i][0], bone[i][p], bone[i - 1][0], bone[i - 1][p], relevant_framenums[j])) # rotation interpolation must happen in the quaternion-space quat1 = core.euler_to_quaternion(bone[i - 1][4:7]) quat2 = core.euler_to_quaternion(bone[i][4:7]) # desired frame is relevant_framenums[j] = d # available frames are bone[i-1][0] = s and bone[i][0] = e # percentage = (d - s) / (e - s) percentage = (relevant_framenums[j] - bone[i - 1][0]) / (bone[i][0] - bone[i - 1][0]) quat_slerp = core.my_slerp(quat1, quat2, percentage) euler_slerp = core.quaternion_to_euler(quat_slerp) newframe += euler_slerp interpframe_list.append(newframe) j += 1 bone += interpframe_list bone.sort(key=core.get1st) # the dictionary should be fully filled out and rectangular now for bone in boneframe_dict: assert len(boneframe_dict[bone]) == len(relevant_framenums) # now i am completely done reading the VMD file and parsing its data! everything has been distilled down to: # relevant_framenums, boneframe_dict ################################################################################### # begin the actual calculations core.MY_PRINT_FUNC("...beginning forward kinematics computation for " + str(len(relevant_framenums)) + " frames...") # output array ikframe_list = [] # have list of bones, parentage, initial pos # have list of frames # now i "run the dance" and build the ik frames # for each relevant frame, for I in range(len(relevant_framenums)): # for each side, for (thisik, this_chain) in zip([bonechain_ikr, bonechain_ikl], [bonechain_r, bonechain_l]): # for each bone in this_chain (ordered, start with root!), for J in range(len(this_chain)): # reset the current to be the inital position again this_chain[J].reset() # for each bone in this_chain (ordered, start with toe! do children before parents!) # also, don't read/use root! because the IK are also children of root, they inherit the same root transformations # count backwards from end to lowest_shared_parent, not including lowest_shared_parent for J in range(len(this_chain) - 1, lowest_shared_parent, -1): # get bone J within this_chain, translate to name name = this_chain[J].name # get bone [name] at index I: position & rotation try: xpos, ypos, zpos, xrot, yrot, zrot = boneframe_dict[name][ I][1:7] except KeyError: continue # apply position offset to self & children # also resets the currposition when changing frames for K in range(J, len(this_chain)): # set this_chain[K].current456 = current456 + position this_chain[K].xcurr += xpos this_chain[K].ycurr += ypos this_chain[K].zcurr += zpos # apply rotation offset to all children, but not self _origin = [ this_chain[J].xcurr, this_chain[J].ycurr, this_chain[J].zcurr ] _angle = [xrot, yrot, zrot] _angle_quat = core.euler_to_quaternion(_angle) for K in range(J, len(this_chain)): # set this_chain[K].current456 = current rotated around this_chain[J].current456 _point = [ this_chain[K].xcurr, this_chain[K].ycurr, this_chain[K].zcurr ] _newpoint = rotate3d(_origin, _angle_quat, _point) (this_chain[K].xcurr, this_chain[K].ycurr, this_chain[K].zcurr) = _newpoint # also rotate the angle of this bone curr_angle_euler = [ this_chain[K].xrot, this_chain[K].yrot, this_chain[K].zrot ] curr_angle_quat = core.euler_to_quaternion( curr_angle_euler) new_angle_quat = core.hamilton_product( _angle_quat, curr_angle_quat) new_angle_euler = core.quaternion_to_euler(new_angle_quat) (this_chain[K].xrot, this_chain[K].yrot, this_chain[K].zrot) = new_angle_euler pass pass # now i have cascaded this frame's pose data down the this_chain # grab foot/toe (-2 and -1) current position and calculate IK offset from that # first, foot: # footikend - footikinit = footikoffset xfoot = this_chain[-2].xcurr - thisik[-2].xinit yfoot = this_chain[-2].ycurr - thisik[-2].yinit zfoot = this_chain[-2].zcurr - thisik[-2].zinit # save as boneframe to be ultimately formatted for VMD: # need bonename = (known) # need frame# = relevantframe#s[I] # position = calculated # rotation = 0 # phys = not disabled # interp = default (20/107) # # then, foot-angle: just copy the angle that the foot has if STORE_IK_AS_FOOT_ONLY: ikframe = [ thisik[-2].name, relevant_framenums[I], xfoot, yfoot, zfoot, this_chain[-2].xrot, this_chain[-2].yrot, this_chain[-2].zrot, False ] else: ikframe = [ thisik[-2].name, relevant_framenums[I], xfoot, yfoot, zfoot, 0.0, 0.0, 0.0, False ] ikframe += [20] * 8 ikframe += [107] * 8 # append the freshly-built frame ikframe_list.append(ikframe) if not STORE_IK_AS_FOOT_ONLY: # then, toe: # toeikend - toeikinit - footikoffset = toeikoffset xtoe = this_chain[-1].xcurr - thisik[-1].xinit - xfoot ytoe = this_chain[-1].ycurr - thisik[-1].yinit - yfoot ztoe = this_chain[-1].zcurr - thisik[-1].zinit - zfoot ikframe = [ thisik[-1].name, relevant_framenums[I], xtoe, ytoe, ztoe, 0.0, 0.0, 0.0, False ] ikframe += [20] * 8 ikframe += [107] * 8 # append the freshly-built frame ikframe_list.append(ikframe) # now done with a timeframe for all bones on both sides # print progress updates core.print_progress_oneline(I / len(relevant_framenums)) core.MY_PRINT_FUNC( "...done with forward kinematics computation, now writing output...") if INCLUDE_IK_ENABLE_FRAME: # create a single ikdispframe that enables the ik bones at frame 0 ikbones = [ vmdstruct.VmdIkbone(name=jp_rightfoot_ik, enable=True), vmdstruct.VmdIkbone(name=jp_righttoe_ik, enable=True), vmdstruct.VmdIkbone(name=jp_leftfoot_ik, enable=True), vmdstruct.VmdIkbone(name=jp_lefttoe_ik, enable=True) ] ikdispframe_list = [ vmdstruct.VmdIkdispFrame(f=0, disp=True, ikbones=ikbones) ] else: ikdispframe_list = [] core.MY_PRINT_FUNC( "Warning: IK following will NOT be enabled when this VMD is loaded, you will need enable it manually!" ) # convert old-style bonelist ikframe_list to new object format ikframe_list = [ vmdstruct.VmdBoneFrame(name=r[0], f=r[1], pos=r[2:5], rot=r[5:8], phys_off=r[8], interp=r[9:]) for r in ikframe_list ] # build actual VMD object nicelist_out = vmdstruct.Vmd( vmdstruct.VmdHeader(2, "SEMISTANDARD-IK-BONES--------"), ikframe_list, # bone [], # morph [], # cam [], # light [], # shadow ikdispframe_list # ikdisp ) # write out output_filename_vmd = "%s_ik_from_%s.vmd" % \ (input_filename_vmd[0:-4], core.get_clean_basename(input_filename_pmx)) output_filename_vmd = core.get_unused_file_name(output_filename_vmd) vmdlib.write_vmd(output_filename_vmd, nicelist_out, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None