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 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 showprompt(): # print info to explain what inputs/outputs it needs/creates core.MY_PRINT_FUNC(iotext) # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.prompt_user_filename(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=True) return pmx, input_filename_pmx
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 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) 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): # copied codes core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # object pmx_file_obj: pmxstruct.Pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # since there is an update to Valve Bip tools (I guess?), there is different bone names: the old and new one # only prefixes are changed along with order, thus there is a little bit scripting here to find the last leg big_dict: dict = {**body_dict, **leg_dict, **arm_dict, **finger_dict} ######################################################################### # stage 1: create & insert core/base bones (grooves, mother,...) ######################################################################### # base bone section # base order: 上半身, 下半身, 腰 (b_1), グルーブ, センター, 全ての親 base_bone_4_name = "全ての親" # motherbone base_bone_3_name = "センター" # center base_bone_2_name = "グルーブ" # groove base_bone_1_name = "腰" # waist # note: Source models apparently have a much larger scale than MMD models base_bone_4_pos = [0, 0, 0] base_bone_3_pos = [0, 21, -0.533614993095398] base_bone_2_pos = base_bone_3_pos base_bone_1_pos = [0, 32, -0.533614993095398] # pelvis_pos = [-4.999999873689376e-06, 38.566917419433594, -0.533614993095398] # 全ての親, name_en, [0.0, 0.0, -0.4735046625137329], -1, 0, False, # True, True, True, True, # False, [0.0, 0.0, 0.0], False, False, None, # None, False, None, False, None, None, False, None, False, # None, None, None, None # base order: 上半身, 下半身, 腰 (b_1), グルーブ, センター, 全ての親 base_bone_4_obj = pmxstruct.PmxBone( name_jp=base_bone_4_name, name_en="", pos=base_bone_4_pos, parent_idx=-1, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=False, tail_usebonelink=False, tail=[0, 3, 0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ) insert_single_bone(pmx_file_obj, base_bone_4_obj, 0) base_bone_3_obj = pmxstruct.PmxBone( name_jp=base_bone_3_name, name_en="", pos=base_bone_3_pos, parent_idx=0, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=False, tail_usebonelink=False, tail=[0, -3, 0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ) insert_single_bone(pmx_file_obj, base_bone_3_obj, 1) base_bone_2_obj = pmxstruct.PmxBone( name_jp=base_bone_2_name, name_en="", pos=base_bone_2_pos, parent_idx=1, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=False, tail_usebonelink=False, tail=[0, 0, 1.5], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ) insert_single_bone(pmx_file_obj, base_bone_2_obj, 2) base_bone_1_obj = pmxstruct.PmxBone( name_jp=base_bone_1_name, name_en="", pos=base_bone_1_pos, parent_idx=2, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=False, tail_usebonelink=False, tail=[0, 0, 0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ) insert_single_bone(pmx_file_obj, base_bone_1_obj, 3) ######################################################################### # phase 2: translate Source names to MMD names ######################################################################### # for each mapping of source-name to mmd-name, for mmd_name, source_possible_names in big_dict.items(): # for each bone, for index, bone_object in enumerate(pmx_file_obj.bones): # if it has a source-name, replace with mmd-name if bone_object.name_jp in source_possible_names: pmx_file_obj.bones[index].name_jp = mmd_name # next, fix the lowerbody bone # find lowerbod lowerbod_obj = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "下半身", getitem=True) # elif bone_object.name_jp in ["ValveBiped.Bip01_Pelvis", "bip_pelvis"]: if lowerbod_obj is not None: # should not be translateable lowerbod_obj.has_translate = False # parent should be waist lowerbod_obj.parent_idx = 3 # next, fix the upperbody bone upperbod_obj = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "上半身", getitem=True) if upperbod_obj is not None: # should not be translateable upperbod_obj.has_translate = False # parent should be waist upperbod_obj.parent_idx = 3 ######################################################################### # phase 3: create & insert IK bones for leg/toe ######################################################################### # find the last leg item index # when creating IK bones, want to insert the IK bones after both legs r_l_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "右足") r_k_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "右ひざ") r_a_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "右足首") r_t_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "右つま先") l_l_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "左足") l_k_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "左ひざ") l_a_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "左足首") l_t_index = core.my_list_search(pmx_file_obj.bones, lambda x: x.name_jp == "左つま先") # if somehow they aren't found, default to 0 if r_l_index is None: r_l_index = 0 if r_k_index is None: r_k_index = 0 if r_a_index is None: r_a_index = 0 if r_t_index is None: r_t_index = 0 if l_l_index is None: l_l_index = 0 if l_k_index is None: l_k_index = 0 if l_a_index is None: l_a_index = 0 if l_t_index is None: l_t_index = 0 if r_t_index > l_t_index: last_leg_item_index = r_t_index else: last_leg_item_index = l_t_index leg_left_ik_name = "左足IK" leg_left_toe_ik_name = "左つま先IK" leg_right_ik_name = "右足IK" leg_right_toe_ik_name = "右つま先IK" # these limits in degrees knee_limit_1 = [-180, 0.0, 0.0] knee_limit_2 = [-0.5, 0.0, 0.0] # other parameters ik_loops = 40 ik_toe_loops = 8 ik_angle = 114.5916 # degrees, =2 radians ik_toe_angle = 229.1831 # degrees, =4 radians # adding IK and such leg_left_ankle_obj = pmx_file_obj.bones[l_a_index] leg_left_toe_obj = pmx_file_obj.bones[l_t_index] leg_right_ankle_obj = pmx_file_obj.bones[r_a_index] leg_right_toe_obj = pmx_file_obj.bones[r_t_index] leg_left_ankle_pos = leg_left_ankle_obj.pos leg_left_toe_pos = leg_left_toe_obj.pos leg_right_ankle_pos = leg_right_ankle_obj.pos leg_right_toe_pos = leg_right_toe_obj.pos # toe /// places of some value wont match with the struct /// taken from hololive's korone model # name, name, [-0.823277473449707, 0.2155265510082245, -1.8799238204956055], 112, 0, False, # True, True, True, True, # False, [0.0, -1.3884940147399902, 1.2653569569920364e-07] /// This is offset, False, False, None, # None, False, None, False, None, None, False, None, True, # 111, 160, 1.0, [[110, None, None]] # leg # 右足IK, en_name, [-0.8402935862541199, 1.16348397731781, 0.3492986857891083], 0, 0, False, # True, True, True, True, # False, [0.0, -2.53071505085245e-07, 1.3884940147399902], False, False, None, # None, False, None, False, None, None, False, None, True, # 110, 85, 1.9896754026412964, [[109, [-3.1415927410125732, 0.0, 0.0], [-0.008726646192371845, 0.0, 0.0]] # /// These ik_links are in radians /// , [108, None, None]] leg_left_ik_obj = pmxstruct.PmxBone( name_jp=leg_left_ik_name, name_en="", pos=leg_left_ankle_pos, parent_idx=0, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=True, tail_usebonelink=False, tail=[0.0, 0.0, 1.0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ik_target_idx=l_a_index, ik_numloops=ik_loops, ik_angle=ik_angle, ik_links=[ pmxstruct.PmxBoneIkLink(idx=l_k_index, limit_min=knee_limit_1, limit_max=knee_limit_2), pmxstruct.PmxBoneIkLink(idx=l_l_index) ], ) insert_single_bone(pmx_file_obj, leg_left_ik_obj, last_leg_item_index + 1) leg_left_toe_ik_obj = pmxstruct.PmxBone( name_jp=leg_left_toe_ik_name, name_en="", pos=leg_left_toe_pos, parent_idx=last_leg_item_index + 1, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=True, tail_usebonelink=False, tail=[0.0, -1.0, 0.0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ik_target_idx=l_t_index, ik_numloops=ik_toe_loops, ik_angle=ik_toe_angle, ik_links=[pmxstruct.PmxBoneIkLink(idx=l_a_index)], ) insert_single_bone(pmx_file_obj, leg_left_toe_ik_obj, last_leg_item_index + 2) leg_right_ik_obj = pmxstruct.PmxBone( name_jp=leg_right_ik_name, name_en="", pos=leg_right_ankle_pos, parent_idx=0, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=True, tail_usebonelink=False, tail=[0.0, 0.0, 1.0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ik_target_idx=r_a_index, ik_numloops=ik_loops, ik_angle=ik_angle, ik_links=[ pmxstruct.PmxBoneIkLink(idx=r_k_index, limit_min=knee_limit_1, limit_max=knee_limit_2), pmxstruct.PmxBoneIkLink(idx=r_l_index) ], ) insert_single_bone(pmx_file_obj, leg_right_ik_obj, last_leg_item_index + 3) leg_right_toe_ik_obj = pmxstruct.PmxBone( name_jp=leg_right_toe_ik_name, name_en="", pos=leg_right_toe_pos, parent_idx=last_leg_item_index + 3, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=True, tail_usebonelink=False, tail=[0.0, -1.0, 0.0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ik_target_idx=r_t_index, ik_numloops=ik_toe_loops, ik_angle=ik_toe_angle, ik_links=[pmxstruct.PmxBoneIkLink(idx=r_a_index)], ) insert_single_bone(pmx_file_obj, leg_right_toe_ik_obj, last_leg_item_index + 4) # output the file output_filename_pmx = input_filename_pmx[0:-4] + "_sourcetrans.pmx" pmxlib.write_pmx(output_filename_pmx, pmx_file_obj, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def main(): # copied codes core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") moreinfo = False 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) # object retme: pmxstruct.Pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # since there is an update to Valve Bip tools (I guess?), there is different bone names: the old and new one # only prefixes are changed along with order, thus there is a little bit scripting here to find the last leg big_dict: dict is_old: bool = False if "bip_" in retme.bones[0].name_jp: big_dict = { **old_body_dict, **old_arm_dict, **old_leg_dict, **old_finger_dict } is_old = True else: big_dict = { **new_body_dict, **new_arm_dict, **new_leg_dict, **new_finger_dict } # checking for last leg item so the code could be scalable last_leg_item: int last_leg_name: str last_leg_name_cmp_r: str = "" last_leg_name_cmp_l: str = "" r_l_index: int = 0 r_k_index: int = 0 r_a_index: int = 0 r_t_index: int = 0 l_l_index: int = 0 l_k_index: int = 0 l_a_index: int = 0 l_t_index: int = 0 # lol this is a mess but it works just fine okay for key in big_dict: for index, i in enumerate(retme.bones): # usually, the toes are the last parts of the legs, from there, we can interject the IK bones if i.name_jp == "bip_toe_R" or i.name_jp == "ValveBiped.Bip01_R_Toe0": r_t_index = index # last_leg_name_cmp_r = i.name_jp elif i.name_jp == "bip_toe_L" or i.name_jp == "ValveBiped.Bip01_L_Toe0": l_t_index = index # last_leg_name_cmp_l = i.name_jp # without this, the pelvis will start as "green" elif i.name_jp == "ValveBiped.Bip01_Pelvis" or i.name_jp == "bip_pelvis": retme.bones[index].has_translate = False elif i.name_jp == "ValveBiped.Bip01_R_Foot" or i.name_jp == "bip_foot_R": r_a_index = index elif i.name_jp == "ValveBiped.Bip01_L_Foot" or i.name_jp == "bip_foot_L": l_a_index = index elif i.name_jp == "ValveBiped.Bip01_R_Calf" or i.name_jp == "bip_knee_R": r_k_index = index elif i.name_jp == "ValveBiped.Bip01_L_Calf" or i.name_jp == "bip_knee_L": l_k_index = index elif i.name_jp == "ValveBiped.Bip01_R_Thigh" or i.name_jp == "bip_hip_R": r_l_index = index elif i.name_jp == "ValveBiped.Bip01_L_Thigh" or i.name_jp == "bip_hip_L": l_l_index = index # the part that replaces texts if i.name_jp == key: retme.bones[index].name_jp = big_dict[key] last_bone = len(retme.bones) - 1 # if r_t_index > l_t_index: # last_leg_item = r_t_index # last_leg_name = last_leg_name_cmp_r # else: # last_leg_item = l_t_index # last_leg_name = last_leg_name_cmp_l # # print(f"This is last leg item {old_last_leg_item}") # base bone section # base order: 上半身, 下半身, 腰 (b_1), グルーブ, センター, 全ての親 b1_name = "腰" b2_name = "グルーブ" b3_name = "センター" b4_name = "全ての親" # IK bone section leg_left_ik_name = "左足IK" leg_left_toe_ik_name = "左つま先IK" leg_right_ik_name = "右足IK" leg_right_toe_ik_name = "右つま先IK" knee_limit_1 = [-3.1415927410125732, 0.0, 0.0] knee_limit_2 = [-0.008726646192371845, 0.0, 0.0] # for some reasons, this value will always be the same # pelvis_pos = [-4.999999873689376e-06, 38.566917419433594, -0.533614993095398] # adding IK and such # leg_left_obj = retme.bones[last_leg_item + l_l] # leg_left_knee_obj = retme.bones[last_leg_item + l_k] leg_left_ankle_obj = retme.bones[l_a_index] leg_left_toe_obj = retme.bones[l_t_index] # leg_right_obj = retme.bones[last_leg_item + r_l] # leg_right_knee_obj = retme.bones[last_leg_item + r_k] leg_right_ankle_obj = retme.bones[r_a_index] leg_right_toe_obj = retme.bones[r_t_index] leg_left_ankle_pos = leg_left_ankle_obj.pos leg_left_toe_pos = leg_left_toe_obj.pos leg_right_ankle_pos = leg_right_ankle_obj.pos leg_right_toe_pos = leg_right_toe_obj.pos # toe /// places of some value wont match with the struct /// taken from hololive's korone model # name, name, [-0.823277473449707, 0.2155265510082245, -1.8799238204956055], 112, 0, False, # True, True, True, True, # False, [0.0, -1.3884940147399902, 1.2653569569920364e-07] /// This is offset, False, False, None, # None, False, None, False, None, None, False, None, True, # 111, 160, 1.0, [[110, None, None]] # leg # 右足IK, en_name, [-0.8402935862541199, 1.16348397731781, 0.3492986857891083], 0, 0, False, # True, True, True, True, # False, [0.0, -2.53071505085245e-07, 1.3884940147399902], False, False, None, # None, False, None, False, None, None, False, None, True, # 110, 85, 1.9896754026412964, [[109, [-3.1415927410125732, 0.0, 0.0], [-0.008726646192371845, 0.0, 0.0]] # /// These ik_links are in radians /// , [108, None, None]] # if name == "ValveBiped.Bip01_R_Toe0": # retme.bones.insert(last_leg_item + 1, ) leg_left_ik_obj = pmxstruct.PmxBone( leg_left_ik_name, "", leg_left_ankle_pos, last_bone + 5, 0, False, True, True, True, True, True, False, [0.0, 0.0, 0.0], False, False, False, False, False, None, None, None, None, None, None, l_a_index, 40, 114.5916, [[l_k_index, knee_limit_1, knee_limit_2], [l_l_index, None, None]]) retme.bones.insert(last_bone + 1, leg_left_ik_obj) leg_left_toe_ik_obj = pmxstruct.PmxBone( leg_left_toe_ik_name, "", leg_left_toe_pos, last_bone + 1, 0, False, True, True, True, True, True, False, [0, 0, 0], False, False, False, False, False, None, None, None, None, None, None, l_t_index, 3, 229.1831, [[l_a_index, None, None]]) retme.bones.insert(last_bone + 2, leg_left_toe_ik_obj) leg_right_ik_obj = pmxstruct.PmxBone( leg_right_ik_name, "", leg_right_ankle_pos, last_bone + 5, 0, False, True, True, True, True, True, False, [0.0, 0.0, 0.0], False, False, False, False, False, None, None, None, None, None, None, r_a_index, 40, 114.5916, [[r_k_index, knee_limit_1, knee_limit_2], [r_l_index, None, None]]) retme.bones.insert(last_bone + 3, leg_right_ik_obj) leg_right_toe_ik_obj = pmxstruct.PmxBone( leg_right_toe_ik_name, "", leg_right_toe_pos, last_bone + 3, 0, False, True, True, True, True, True, False, [0, 0, 0], False, False, False, False, False, None, None, None, None, None, None, r_t_index, 3, 229.1831, [[r_a_index, None, None]]) retme.bones.insert(last_bone + 4, leg_right_toe_ik_obj) # # base part b4_pos = [0, 0, 0] # for some reasons, if we pass value from pelvis_pos to b3_pos, pelvis_pos will change as well? b3_pos = [-4.999999873689376e-06, 21, -0.533614993095398] b2_pos = b3_pos b1_pos = [-4.999999873689376e-06, 32, -0.533614993095398] # # # 全ての親, name_en, [0.0, 0.0, -0.4735046625137329], -1, 0, False, # # True, True, True, True, # # False, [0.0, 0.0, 0.0], False, False, None, # # None, False, None, False, None, None, False, None, False, # # None, None, None, None # # # base order: 上半身, 下半身, 腰 (b_1), グルーブ, センター, 全ての親 # # the parents would be fixed later b4_obj = pmxstruct.PmxBone(b4_name, "", b4_pos, -1, 0, False, True, True, True, True, False, False, [0, 0, 0], False, False, None, None, False, None, None, None, None, None, None, None, None, None, None) retme.bones.insert(last_bone + 5, b4_obj) b3_obj = pmxstruct.PmxBone(b3_name, "", b3_pos, last_bone + 5, 0, False, True, True, True, True, False, False, [0, 0, 0], False, False, None, None, False, None, None, None, None, None, None, None, None, None, None) retme.bones.insert(last_bone + 6, b3_obj) b2_obj = pmxstruct.PmxBone(b2_name, "", b2_pos, last_bone + 6, 0, False, True, True, True, True, False, False, [0, 0, 0], False, False, None, None, False, None, None, None, None, None, None, None, None, None, None) retme.bones.insert(last_bone + 7, b2_obj) b1_obj = pmxstruct.PmxBone(b1_name, "", b1_pos, last_bone + 7, 0, False, True, False, True, True, False, False, [0, 0, 0], False, False, None, None, False, None, None, None, None, None, None, None, None, None, None) retme.bones.insert(last_bone + 8, b1_obj) output_filename_pmx = input_filename_pmx[0:-4] + "_sourcetrans.pmx" pmxlib.write_pmx(output_filename_pmx, retme, moreinfo=moreinfo)
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) # prompt VMD file name core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC( "Please enter name of VMD motion or VPD pose file to check compatability with:" ) input_filename = core.MY_FILEPROMPT_FUNC(".vmd .vpd") if not input_filename.lower().endswith(".vpd"): # the actual VMD part isn't even used, only bonedict and morphdict vmd = vmdlib.read_vmd(input_filename, moreinfo=moreinfo) else: vmd = vpdlib.read_vpd(input_filename, moreinfo=moreinfo) bonedict = vmdlib.parse_vmd_used_dict(vmd.boneframes, frametype="bone", moreinfo=moreinfo) morphdict = vmdlib.parse_vmd_used_dict(vmd.morphframes, frametype="morph", moreinfo=moreinfo) core.MY_PRINT_FUNC("") # must use same encoding as I used when the VMD was unpacked, since the hex bytes only have meaning in that encoding core.set_encoding("shift_jis") ############################################## # check morph compatability # build list of morphs used in the dance VMD morphs_in_vmd = list(morphdict.keys()) # build list of ALL morphs in the PMX morphs_in_model = [pmxmorph.name_jp for pmxmorph in pmx.morphs] # ensure that the VMD contains at least some morphs, to prevent zero-divide error if len(morphs_in_vmd) == 0: core.MY_PRINT_FUNC( "MORPH SKIP: VMD '%s' does not contain any morphs that are used in a meaningful way." % core.get_clean_basename(input_filename)) elif len(morphs_in_model) == 0: core.MY_PRINT_FUNC( "MORPH SKIP: PMX '%s' does not contain any morphs." % core.get_clean_basename(input_filename_pmx)) else: # convert pmx-morph names to bytes # these can plausibly fail shift_jis encoding because they came from the UTF-8 pmx file morphs_in_model_b = [] for a in morphs_in_model: try: b = core.encode_string_with_escape(a) except UnicodeEncodeError as e: newerrstr = "%s: '%s' codec cannot encode char '%s' within string '%s'" % ( e.__class__.__name__, e.encoding, e.reason[e.start:e.end], e.reason) core.MY_PRINT_FUNC(newerrstr) b = bytearray() morphs_in_model_b.append(b) # convert vmd-morph names to bytes # these might be truncated but cannot fail because they were already decoded from the shift_jis vmd file morphs_in_vmd_b = [ core.encode_string_with_escape(a) for a in morphs_in_vmd ] matching_morphs = {} missing_morphs = {} # iterate over list of morphs for vmdmorph, vmdmorph_b in zip(morphs_in_vmd, morphs_in_vmd_b): # question: does "vmdmorph" match something in "morphs_in_model"? # BUT, doing comparison in bytes-space to handle escape characters: vmdmorph_b vs morphs_in_model_b # NOTE: MMD does not try to use "begins-with" matching like I had hoped/assumed, it only looks for exact matches # return list of ALL matches, this way i can raise an error if there are multiple matches # exact match modelmorphmatch_b = [ a for a in morphs_in_model_b if a == vmdmorph_b ] # copy the key,val in one of the dicts depending on results of matching attempt if len(modelmorphmatch_b) == 0: # MISS! key is the VMD morph name since that's the best clue Ive got missing_morphs[vmdmorph] = morphdict[vmdmorph] elif len(modelmorphmatch_b) == 1: # MATCH! key is the PMX morph name it matched against, since it might be a longer version wtihout escape char matching_morphs[core.decode_bytes_with_escape( modelmorphmatch_b[0])] = morphdict[vmdmorph] else: # more than 1 morph was a match!? core.MY_PRINT_FUNC( "Warning: VMDmorph '%s' matched multiple PMXmorphs, its behavior is uncertain." % vmdmorph) modelmorphmatch = [ core.decode_bytes_with_escape(a) for a in modelmorphmatch_b ] # core.MY_PRINT_FUNC(modelmorphmatch) matching_morphs[modelmorphmatch[0]] = morphdict[vmdmorph] # display results! r = "PASS" if len(matching_morphs) == len(morphs_in_vmd) else "FAIL" core.MY_PRINT_FUNC( "MORPH {}: {} / {} = {:.1%} of the morphs are supported".format( r, len(matching_morphs), len(morphs_in_vmd), len(matching_morphs) / len(morphs_in_vmd))) # if there are no missing morphs (all match), don't print anything at all if missing_morphs: if not moreinfo: core.MY_PRINT_FUNC( "For detailed list, please re-run with 'more info' enabled" ) else: # convert the dicts to lists and sort for printing # sort in-place descending by 2nd element as primary missing_morphs_list = sorted(list(missing_morphs.items()), key=core.get2nd, reverse=True) # justify the names! missing_just = core.MY_JUSTIFY_STRINGLIST( ["'" + m[0] + "'" for m in missing_morphs_list]) # re-attach the justified names to the usage numbers missing_morphs_list = list( zip(missing_just, [m[1] for m in missing_morphs_list])) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Unsupported morphs: name + times used") for m, num in missing_morphs_list: core.MY_PRINT_FUNC(" %s || %d" % (m, int(num))) # only print the matching morphs if there are some, and if enabled by options if matching_morphs and PRINT_MATCHING_ITEMS: matching_morphs_list = list(matching_morphs.items()) matching_morphs_list.sort( key=core.get2nd, reverse=True ) # sort in-place descending by 2nd element as primary matching_just = core.MY_JUSTIFY_STRINGLIST( ["'" + m[0] + "'" for m in matching_morphs_list]) matching_morphs_list = list( zip(matching_just, [m[1] for m in matching_morphs_list])) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Supported morphs: name + times used") for m, num in matching_morphs_list: core.MY_PRINT_FUNC(" %s || %d" % (m, int(num))) ############################################## # check bone compatability core.MY_PRINT_FUNC("") # build list of bones used in the dance VMD bones_in_vmd = list(bonedict.keys()) # build list of ALL bones in the PMX # first item of pmxbone is the jp name bones_in_model = [pmxbone.name_jp for pmxbone in pmx.bones] # ensure that the VMD contains at least some bones, to prevent zero-divide error if len(bones_in_vmd) == 0: core.MY_PRINT_FUNC( "BONE SKIP: VMD '%s' does not contain any bones that are used in a meaningful way." % core.get_clean_basename(input_filename)) elif len(bones_in_model) == 0: core.MY_PRINT_FUNC("BONE SKIP: PMX '%s' does not contain any bones." % core.get_clean_basename(input_filename_pmx)) else: # convert pmx-bone names to bytes # these can plausibly fail shift_jis encoding because they came from the UTF-8 pmx file bones_in_model_b = [] for a in bones_in_model: try: b = core.encode_string_with_escape(a) except UnicodeEncodeError as e: newerrstr = "%s: '%s' codec cannot encode char '%s' within string '%s'" % ( e.__class__.__name__, e.encoding, e.reason[e.start:e.end], e.reason) core.MY_PRINT_FUNC(newerrstr) b = bytearray() bones_in_model_b.append(b) # convert vmd-bone names to bytes # these might be truncated but cannot fail because they were already decoded from the shift_jis vmd file bones_in_vmd_b = [ core.encode_string_with_escape(a) for a in bones_in_vmd ] matching_bones = {} missing_bones = {} # iterate over list of bones that pass the size check for vmdbone, vmdbone_b in zip(bones_in_vmd, bones_in_vmd_b): # question: does "vmdbone" match something in "bones_in_model"? # BUT, doing comparison in bytes-space to handle escape characters: vmdbone_b vs bones_in_model_b # NOTE: MMD does not try to use "begins-with" matching like I had hoped/assumed, it only looks for exact matches # return list of ALL matches, this way i can raise an error if there are multiple matches # exact match modelbonematch_b = [a for a in bones_in_model_b if a == vmdbone_b] # copy the key,val in one of the dicts depending on results of matching attempt if len(modelbonematch_b) == 0: # MISS! key is the VMD bone name since that's the best clue Ive got missing_bones[vmdbone] = bonedict[vmdbone] elif len(modelbonematch_b) == 1: # MATCH! key is the PMX bone name it matched against, since it might be a longer version wtihout escape char matching_bones[core.decode_bytes_with_escape( modelbonematch_b[0])] = bonedict[vmdbone] else: # more than 1 bone was a match!? core.MY_PRINT_FUNC( "Warning: VMDbone '%s' matched multiple PMXbones, its behavior is uncertain." % vmdbone) modelbonematch = [ core.decode_bytes_with_escape(a) for a in modelbonematch_b ] # core.MY_PRINT_FUNC(modelbonematch) matching_bones[modelbonematch[0]] = bonedict[vmdbone] # display results! r = "PASS" if len(matching_bones) == len(bones_in_vmd) else "FAIL" core.MY_PRINT_FUNC( "BONE {}: {} / {} = {:.1%} of the bones are supported".format( r, len(matching_bones), len(bones_in_vmd), len(matching_bones) / len(bones_in_vmd))) # if there are no missing bones (all match), don't print anything at all if missing_bones: if not moreinfo: core.MY_PRINT_FUNC( "For detailed list, please re-run with 'more info' enabled" ) else: # convert the dicts to lists and sort for printing # sort in-place descending by 2nd element as primary missing_bones_list = sorted(list(missing_bones.items()), key=core.get2nd, reverse=True) # justify the names! missing_just = core.MY_JUSTIFY_STRINGLIST( ["'" + m[0] + "'" for m in missing_bones_list]) # re-attach the justified names to the usage numbers missing_bones_list = list( zip(missing_just, [m[1] for m in missing_bones_list])) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Unsupported bones: name + times used") for m, num in missing_bones_list: core.MY_PRINT_FUNC(" %s || %d" % (m, int(num))) # only print the matching bones if there are some, and if enabled by options if matching_bones and PRINT_MATCHING_ITEMS: matching_bones_list = list(matching_bones.items()) matching_bones_list.sort( key=core.get2nd, reverse=True ) # sort in-place descending by 2nd element as primary matching_just = core.MY_JUSTIFY_STRINGLIST( ["'" + m[0] + "'" for m in matching_bones_list]) matching_bones_list = list( zip(matching_just, [m[1] for m in matching_bones_list])) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("Supported bones: name + times used") for m, num in matching_bones_list: core.MY_PRINT_FUNC(" %s || %d" % (m, int(num))) # NEW: among matching bones, check whether any bones have unsupported translation/rotation for bonestr in sorted(list(matching_bones.keys())): # get the bone to get whether rot/trans enabled bone = core.my_list_search(pmx.bones, lambda x: x.name_jp == bonestr, getitem=True) # get all the frames from the VMD that are relevant to this bone thisboneframes = [f for f in vmd.boneframes if f.name == bonestr] # does the VMD use rotation? probably, check anyway vmd_use_rot = any(f.rot != [0, 0, 0] for f in thisboneframes) if vmd_use_rot and not (bone.has_rotate and bone.has_enabled): # raise some sort of warning w = "Warning: supported bone '%s' uses rotation in VMD, but rotation not allowed by PMX" % bonestr core.MY_PRINT_FUNC(w) # does the VMD use translation? vmd_use_trans = any(f.pos != [0, 0, 0] for f in thisboneframes) if vmd_use_trans and not (bone.has_translate and bone.has_enabled): # raise some sort of warning w = "Warning: supported bone '%s' uses move/shift in VMD, but move/shift not allowed by PMX" % bonestr core.MY_PRINT_FUNC(w) core.MY_PRINT_FUNC("") 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=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): # 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) # 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): # 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=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=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