def parse_vmd_header(raw: bytearray, moreinfo: bool) -> vmdstruct.VmdHeader: ############################ # unpack the header, get file version and model name # version only affects the length of the model name text field, but i'll return it anyway try: header = core.my_unpack(fmt_header, raw) except Exception as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("section=header") core.MY_PRINT_FUNC( "Err: something went wrong while parsing, file is probably corrupt/malformed" ) raise RuntimeError() if header == "Vocaloid Motion Data 0002": # if this matches, this is version >= 1.30 # but i will just return "2" version = 2 # model name string is 20-chars long useme = fmt_modelname_new elif header == "Vocaloid Motion Data file": # this is actually untested & unverified, but according to the docs this is how it's labelled # if this matches, this is version < 1.30 # but i will just return "1" version = 1 # model name string is 10-chars long useme = fmt_modelname_old else: core.MY_PRINT_FUNC( "ERR: found unsupported file version identifier string, '%s'" % header) raise RuntimeError() try: modelname = core.my_unpack(useme, raw) except Exception as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("section=modelname") core.MY_PRINT_FUNC( "Err: something went wrong while parsing, file is probably corrupt/malformed" ) raise RuntimeError() if moreinfo: core.MY_PRINT_FUNC("...model name = JP:'%s'" % modelname) return vmdstruct.VmdHeader(version=version, modelname=modelname)
def read_vmdtext_header(rawlist_text: List[list]) -> vmdstruct.VmdHeader: ################################## # header data global readfrom_line # first check for bad format check2_match_first_item(rawlist_text, keystr_version) # read version version = rawlist_text[readfrom_line][1] readfrom_line += 1 # check for bad format again check2_match_first_item(rawlist_text, keystr_modelname) # read model name modelname = rawlist_text[readfrom_line][1] readfrom_line += 1 core.MY_PRINT_FUNC("...model name = JP:'%s'" % modelname) # assemble and return return vmdstruct.VmdHeader(version, modelname)
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 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