def read_vmd(vmd_filename: str, moreinfo=False) -> vmdstruct.Vmd: vmd_filename_clean = core.get_clean_basename(vmd_filename) + ".vmd" # creates object (header, boneframe_list, morphframe_list, camframe_list, lightframe_list, shadowframe_list, ikdispframe_list) # assumes the calling function already verified correct file extension core.MY_PRINT_FUNC("Begin reading VMD file '%s'" % vmd_filename_clean) vmd_bytes = core.read_binfile_to_bytes(vmd_filename) core.MY_PRINT_FUNC("...total size = %sKB" % round(len(vmd_bytes) / 1024)) core.MY_PRINT_FUNC("Begin parsing VMD file '%s'" % vmd_filename_clean) core.reset_unpack() core.set_encoding("shift_jis") # !!!! this does eliminate all the garbage data MMD used to pack strings so this isnt 100% reversable !!! # read the bytes object and return all the data from teh VMD broken up into a list of lists # also convert things from packed formats to human-readable scales # (quaternion to euler, radians to degrees, floats to ints, etc) # also generate the bonedict and morphdict core.print_progress_oneline(0) A = parse_vmd_header(vmd_bytes, moreinfo) B = parse_vmd_boneframe(vmd_bytes, moreinfo) C = parse_vmd_morphframe(vmd_bytes, moreinfo) D = parse_vmd_camframe(vmd_bytes, moreinfo) E = parse_vmd_lightframe(vmd_bytes, moreinfo) F = parse_vmd_shadowframe(vmd_bytes, moreinfo) G = parse_vmd_ikdispframe(vmd_bytes, moreinfo) if moreinfo: core.print_failed_decodes() bytes_remain = len(vmd_bytes) - core.get_readfrom_byte() if bytes_remain != 0: # padding with my SIGNATURE is acceptable, anything else is strange leftover = vmd_bytes[core.get_readfrom_byte():] if leftover == bytes(SIGNATURE, encoding="shift_jis"): core.MY_PRINT_FUNC( "...note: this VMD file was previously modified with this tool!" ) else: core.MY_PRINT_FUNC( "Warning: finished parsing but %d bytes are left over at the tail!" % bytes_remain) core.MY_PRINT_FUNC( "The file may be corrupt or maybe it contains unknown/unsupported data formats" ) core.MY_PRINT_FUNC(leftover) core.MY_PRINT_FUNC("Done parsing VMD file '%s'" % vmd_filename_clean) vmd = vmdstruct.Vmd(A, B, C, D, E, F, G) # this is where sorting happens, if it happens if GUARANTEE_FRAMES_SORTED: # bones & morphs: primarily sorted by NAME, with FRAME# as tiebreaker. the second sort is the primary one. vmd.boneframes.sort(key=lambda x: x.f) # frame# vmd.boneframes.sort(key=lambda x: x.name) # name vmd.morphframes.sort(key=lambda x: x.f) vmd.morphframes.sort(key=lambda x: x.name) # all of these only sort by frame number. vmd.camframes.sort(key=lambda x: x.f) # frame# vmd.lightframes.sort(key=lambda x: x.f) vmd.shadowframes.sort(key=lambda x: x.f) vmd.ikdispframes.sort(key=lambda x: x.f) return vmd
def write_vmd(vmd_filename: str, vmd: vmdstruct.Vmd, moreinfo=False): vmd_filename_clean = core.get_clean_basename(vmd_filename) + ".vmd" # recives object (header, boneframe_list, morphframe_list, camframe_list, lightframe_list, shadowframe_list, ikdispframe_list) # first, verify that the data is valid before trying to write vmd.validate() # assumes the calling function already verified correct file extension core.MY_PRINT_FUNC("Begin encoding VMD file '%s'" % vmd_filename_clean) core.set_encoding("shift_jis") core.print_progress_oneline(0) # this is where sorting happens, if it happens if GUARANTEE_FRAMES_SORTED: # bones & morphs: primarily sorted by NAME, with FRAME# as tiebreaker. the second sort is the primary one. vmd.boneframes.sort(key=lambda x: x.f) # frame# vmd.boneframes.sort(key=lambda x: x.name) # name vmd.morphframes.sort(key=lambda x: x.f) vmd.morphframes.sort(key=lambda x: x.name) # all of these only sort by frame number. vmd.camframes.sort(key=lambda x: x.f) # frame# vmd.lightframes.sort(key=lambda x: x.f) vmd.shadowframes.sort(key=lambda x: x.f) vmd.ikdispframes.sort(key=lambda x: x.f) global ENCODE_PERCENT_BONE global ENCODE_PERCENT_MORPH # cam is not included cuz a file contains only bone+morph OR cam total_bone = len(vmd.boneframes) * ENCODE_FACTOR_BONE total_morph = len(vmd.morphframes) * ENCODE_FACTOR_MORPH ALLENCODE = total_bone + total_morph if ALLENCODE == 0: ALLENCODE = 1 # just a bandaid to avoid zero-div error when writing empty VMD ENCODE_PERCENT_BONE = total_bone / ALLENCODE ENCODE_PERCENT_MORPH = total_morph / ALLENCODE # arg "vmd" is the same structure created by "parse_vmd()" # assume the object is perfect, no sanity-checking needed, it will all be done when parsing the text input output_bytes = bytearray() output_bytes += encode_vmd_header(vmd.header, moreinfo) output_bytes += encode_vmd_boneframe(vmd.boneframes, moreinfo) output_bytes += encode_vmd_morphframe(vmd.morphframes, moreinfo) output_bytes += encode_vmd_camframe(vmd.camframes, moreinfo) output_bytes += encode_vmd_lightframe(vmd.lightframes, moreinfo) output_bytes += encode_vmd_shadowframe(vmd.shadowframes, moreinfo) output_bytes += encode_vmd_ikdispframe(vmd.ikdispframes, moreinfo) # done encoding!! # add a cheeky little binary stamp just to prove that people actually used my tool :) if APPEND_SIGNATURE: # signature to prove that this file was created with this tool output_bytes += bytes(SIGNATURE, encoding="shift_jis") core.MY_PRINT_FUNC("Begin writing VMD file '%s'" % vmd_filename_clean) core.MY_PRINT_FUNC("...total size = %s" % core.prettyprint_file_size(len(output_bytes))) core.write_bytes_to_binfile(vmd_filename, output_bytes) core.MY_PRINT_FUNC("Done writing VMD file '%s'" % vmd_filename_clean) # done with everything! return
def write_vmdtext(vmdtext_filename: str, nicelist: vmdstruct.Vmd): # assume the output filename has already been validated as unused, etc cleanname = core.get_clean_basename(vmdtext_filename) + ".txt" core.MY_PRINT_FUNC("Begin formatting VMD-as-text file '%s'" % cleanname) rawlist = format_nicelist_as_rawlist(nicelist) # done formatting! core.MY_PRINT_FUNC("Begin writing VMD-as-text file '%s'" % cleanname) core.MY_PRINT_FUNC("...total size = %s lines" % len(rawlist)) core.write_csvlist_to_file(vmdtext_filename, rawlist) core.MY_PRINT_FUNC("Done writing VMD-as-text file '%s'" % cleanname) return
def read_vmdtext(vmdtext_filename: str) -> vmdstruct.Vmd: # break apart the CSV text-file format, arrange the data into a easier-to-manipulate list of lists # also check that headers are where they should be and each line has the proper number of items on it # return nicelist = [header, modelname, bone_list, morph_list, cam_list, light_list, shadow_list, ikdisp_list] cleanname = core.get_clean_basename(vmdtext_filename) + ".txt" core.MY_PRINT_FUNC("Begin reading VMD-as-text file '%s'" % cleanname) vmdtext_rawlist = core.read_file_to_csvlist(vmdtext_filename) core.MY_PRINT_FUNC("...total size = %s lines" % len(vmdtext_rawlist)) core.MY_PRINT_FUNC("Begin parsing VMD-as-text file '%s'" % cleanname) global readfrom_line # set this to zero just in case readfrom_line = 0 # wrap the entire parsing section in a try-except block looking for index errors try: A = read_vmdtext_header(vmdtext_rawlist) B = read_vmdtext_boneframe(vmdtext_rawlist) C = read_vmdtext_morphframe(vmdtext_rawlist) D = read_vmdtext_camframe(vmdtext_rawlist) E = read_vmdtext_lightframe(vmdtext_rawlist) F = read_vmdtext_shadowframe(vmdtext_rawlist) G = read_vmdtext_ikdispframe(vmdtext_rawlist) except IndexError as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC( "ERROR: unexpected end-of-file or end-of-line, was reading from line " + str(readfrom_line + 1)) raise RuntimeError() if readfrom_line != len(vmdtext_rawlist): core.MY_PRINT_FUNC( "Warning: there are unsupported trailing lines on the end of the file", readfrom_line, len(vmdtext_rawlist)) core.MY_PRINT_FUNC("Done parsing VMD-as-text file '%s'" % cleanname) # stuff to return: # version+modelname, bonelist, morphlist, camlist, lightlist, shadowlist, ikdisplist return vmdstruct.Vmd(A, B, C, D, E, F, G)
def read_vpd(vpd_filepath: str, moreinfo=False) -> vmdstruct.Vmd: """ Read a VPD text file and convert it to a VMD object with all boneframes and morphframes at time=0. :param vpd_filepath: destination filepath/name, relative from CWD or absolute :param moreinfo: if true, get extra printouts with more info about stuff :return: VMD object """ cleanname = core.get_clean_basename(vpd_filepath) + ".vpd" core.MY_PRINT_FUNC("Begin reading VPD file '%s'" % cleanname) # read textfile to linelist, no CSV fields to untangle here lines = core.read_txtfile_to_list(vpd_filepath, use_jis_encoding=True) # verify magic header "Vocaloid Pose Data file" if lines[0] != "Vocaloid Pose Data file": core.MY_PRINT_FUNC( "warning: did not find expected magic header! this might not be a real VPD file!" ) # get rid of the header lines.pop(0) # this var is a state machine that keeps track of what I expect to find next # if i find anything other than blankspace or what I expect, then err & die parse_state = 0 # save this so I know when I'm done reading all the bones the header promised num_bones = 0 # temp vars to hold stuff from previous lines temp_title = "qwertyuiop" temp_name = "foobar" temp_pos = tuple() temp_rot = tuple() temp_value = 0.0 # this is the VMD object that will be ultimately returned vmd_boneframes = [] vmd_morphframes = [] # iterate over the remaining lines until end-of-file for d, line in enumerate(lines): # vertical whitespace is always acceptable if not line or line.isspace(): continue # if line is not blank, it had better be something good: if parse_state == 0: # 0 = model title m = title_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find model title" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() temp_title = m.group( 1) # if valid match, then grab the actual title if moreinfo: core.MY_PRINT_FUNC("...model name = JP:'%s'" % temp_title) parse_state = 10 # next thing to look for is #bones elif parse_state == 10: # 10 = #bones m = f1_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find number of bones" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() num_bones = int(float(m.group( 1))) # if a valid match, then grab the actual # of bones if moreinfo: core.MY_PRINT_FUNC("...# of boneframes = %d" % num_bones) if num_bones == 0: parse_state = 30 # if there are 0 bones then immediately begin with the morphs else: parse_state = 20 # otherwise look for bones next elif parse_state == 20: # 20 = boneA, name m = bone_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find bone name" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() idx, name = m.group(1, 2) # get idx and name temp_name = name # can i use idx for anything? or is it totally useless? parse_state = 21 # next look for quaternion rotation elif parse_state == 21: # 21 = boneB, xyz pos m = f3_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find bone XYZ position" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() pos = m.group(1, 2, 3) # get all 3 components temp_pos = [float(f) for f in pos] # convert strings to floats parse_state = 22 # next look for quaternion rotation elif parse_state == 22: # 22 = boneC, xyzw quaternion rotation m = f4_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find bone XYZW rotation" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() quat = m.group(1, 2, 3, 4) # get all 4 components quat = [float(f) for f in quat] # convert strings to floats quat.insert( 0, quat.pop(-1)) # WXYZW -> XYZW, AKA move tail (w) to head temp_rot = core.quaternion_to_euler( quat) # convert quaternion to euler angles parse_state = 23 # next look for closing curly elif parse_state == 23: # 23 = boneD, closing curly m = close_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: bone item not properly closed" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() # finish the bone-obj and add to VMD structure # this_boneframe = [bname_str, f, xp, yp, zp, xrot, yrot, zrot, phys_off, x_ax, y_ax, z_ax, r_ax, x_ay, y_ay, # z_ay, r_ay, x_bx, y_bx, z_bx, r_bx, x_by, y_by, z_by, r_by] newframe = vmdstruct.VmdBoneFrame( name=temp_name, f=0, pos=temp_pos, rot=list(temp_rot), phys_off=False, interp=list(core.bone_interpolation_default_linear)) vmd_boneframes.append(newframe) if len(vmd_boneframes) == num_bones: parse_state = 30 # if i got all the bones i expected, move to morphs else: parse_state = 20 # otherwise, get another bone elif parse_state == 30: # 30 = morphA, name m = morph_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find morph name" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() idx, name = m.group(1, 2) # get idx and name temp_name = name # can i use idx for anything? or is it totally useless? parse_state = 31 # next look for value elif parse_state == 31: # 31 = morphB, value m = f1_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: failed to find morph value" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() v = m.group(1) # get value temp_value = float(v) # convert strings to floats parse_state = 32 # next look for close elif parse_state == 32: # 32 = morphC, closing curly m = close_re.match(line) # regex match from beginning of line if m is None: core.MY_PRINT_FUNC( "Parse err line %d state %d: morph item not properly closed" % (d + 2, parse_state)) core.MY_PRINT_FUNC("line = '%s'" % line) raise RuntimeError() # finish the morph-obj and add to VMD structure # morphframe_list.append([mname_str, f, v]) newframe = vmdstruct.VmdMorphFrame(name=temp_name, f=0, val=temp_value) vmd_morphframes.append(newframe) parse_state = 30 # loop morphs until end-of-file else: core.MY_PRINT_FUNC("this should not happen, err & die") raise RuntimeError() if moreinfo: core.MY_PRINT_FUNC("...# of morphframes = %d" % len(vmd_morphframes)) # verify we did not hit end-of-file unexpectedly, looking-for-morphA is only valid ending state if parse_state != 30: core.MY_PRINT_FUNC("Parse err state %d: hit end-of-file unexpectedly" % parse_state) raise RuntimeError() # after hitting end-of-file, assemble the parts of the final returnable VMD-list thing # builds object (header, boneframe_list, morphframe_list, camframe_list, lightframe_list, shadowframe_list, ikdispframe_list) vmd_retme = vmdstruct.Vmd( vmdstruct.VmdHeader(version=2, modelname=temp_title), vmd_boneframes, vmd_morphframes, list(), list(), list(), list()) core.MY_PRINT_FUNC("Done reading VPD file '%s'" % cleanname) return vmd_retme
def write_vpd(vpd_filepath: str, vmd: vmdstruct.Vmd, moreinfo=False): """ Grab all bone/morph frames at time=0 in a VMD object and write them to a properly-formatted VPD text file. :param vpd_filepath: destination filepath/name, relative from CWD or absolute :param vmd: input VMD object :param moreinfo: if true, get extra printouts with more info about stuff """ cleanname = core.get_clean_basename(vpd_filepath) + ".vpd" core.MY_PRINT_FUNC("Begin writing VPD file '%s'" % cleanname) # first, lets partition boneframes & morphframes into those at/notat time=0 pose_bones, otherbones = core.my_list_partition(vmd.boneframes, lambda b: b.f == 0) pose_morphs, othermorphs = core.my_list_partition(vmd.morphframes, lambda b: b.f == 0) # if there are frames not on time=0, raise a warning but continue if otherbones or othermorphs: core.MY_PRINT_FUNC( "Warning: input VMD contains %d frames not at time=0, these will not be captured in the resulting pose!" % (len(otherbones) + len(othermorphs))) if moreinfo: core.MY_PRINT_FUNC("...model name = JP:'%s'" % vmd.header.modelname) # init printlist with magic header, title, and numbones printlist = [ "Vocaloid Pose Data file", "", "{:s}.osm;".format(vmd.header.modelname), "{:d};".format(len(pose_bones)), "", ] # now iterate over all bones # bone-floats always have exactly 6 digits if moreinfo: core.MY_PRINT_FUNC("...# of boneframes = %d" % len(pose_bones)) for d, pb in enumerate(pose_bones): quat = list(core.euler_to_quaternion(pb.rot)) # returns quat WXYZ quat.append(quat.pop(0)) # WXYZ -> XYZW, AKA move head (w) to tail newitem = [ "Bone{:d}{{{:s}".format(d, pb.name), " {:.6f},{:.6f},{:.6f};".format(*pb.pos), " {:.6f},{:.6f},{:.6f},{:.6f};".format(*quat), "}", "", ] printlist.extend(newitem) # now iterate over all morphs # morph-floats are flexible, need to TEST how long they can be! # lets say max precision is 3, but strip any trailing zeros and reduce "1." to "1" if moreinfo: core.MY_PRINT_FUNC("...# of morphframes = %d" % len(pose_morphs)) for d, pm in enumerate(pose_morphs): newitem = [ "Morph{:d}{{{:s}".format(d, pm.name), " {:.3f}".format(pm.val).rstrip("0").rstrip(".") + ";", "}", "" ] printlist.extend(newitem) # ok, now i'm done building the printlist! now actually write it! core.write_list_to_txtfile(vpd_filepath, printlist, use_jis_encoding=True) core.MY_PRINT_FUNC("Done writing VPD file '%s'" % cleanname) 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) # 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=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) # 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