Example #1
0
def convert_vmd_to_txt(input_filename: str, moreinfo=True):
	"""
	Read a VMD motion file from disk, convert it, and write to disk as a text file.
	The output will have the same path and basename, but the opposite file extension.
	See 'README.txt' for more details about VMD-as-text output format.
	
	:param input_filename: filepath to input vmd, absolute or relative to CWD
	:param moreinfo: default false. if true, get extra printouts with more info about stuff.
	"""
	# read the entire VMD, all in this one function
	# also create the bonedict & morphdict
	vmd_nicelist = vmdlib.read_vmd(input_filename, moreinfo=moreinfo)
	core.MY_PRINT_FUNC("")
	# identify an unused filename for writing the output
	dumpname = core.get_unused_file_name(input_filename[0:-4] + filestr_txt)
	# write the output VMD-as-text file
	write_vmdtext(dumpname, vmd_nicelist)
	
	# #####################################
	# # summary file:
	#
	# # if there are no bones and no morphs, there is no need for a summary file... just return early
	# if len(bonedict) == 0 and len(morphdict) == 0:
	# 	return None
	# # if the user doesn't want a summary, dont bother
	# elif not PRINT_BONE_MORPH_SUMMARY_FILE:
	# 	return None
	# else:
	# 	# identify an unused filename for writing the output
	# 	summname = core.get_unused_file_name(core.get_clean_basename(dumpname) + "_summary" + filestr_txt)
	# 	write_summary_dicts(bonedict, morphdict, summname)
	
	# done!
	return
Example #2
0
def convert_vmd_to_vpd(vmd_path: str, moreinfo=True):
    """
	Read a VMD motion file from disk, convert it, and write to disk as a VPD pose file.
	All frames of the VMD are ignored except for frames at time=0.
	The output will have the same path and basename, but the opposite file extension.
	
	:param vmd_path: filepath to input vmd, absolute or relative to CWD
	:param moreinfo: default false. if true, get extra printouts with more info about stuff.
	"""
    # read the entire VMD, all in this one function
    vmd = vmdlib.read_vmd(vmd_path, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("")
    # identify an unused filename for writing the output
    vpd_outpath = core.get_unused_file_name(vmd_path[0:-4] + ".vpd")
    # write the output VPD file
    vpdlib.write_vpd(vpd_outpath, vmd, moreinfo=moreinfo)
    # done!
    return
def 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
Example #5
0
def main(moreinfo=True):
    # TODO: actually load it in MMD and verify that the curves look how they should
    #  not 100% certain that the order of interpolation values is correct for bone/cam frames

    # TODO: some sort of additional stats somehow?

    # TODO: progress % trackers?

    # prompt VMD file name
    core.MY_PRINT_FUNC("Please enter name of VMD motion input file:")
    input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd")

    # next, read/use/prune the dance vmd
    vmd = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo)

    core.MY_PRINT_FUNC("")

    # dictify the boneframes so i can deal with one bone at a time
    boneframe_dict = dictify_framelist(vmd.boneframes)

    # add the camframes to the dict so I can process them at the same time
    # this makes the typechecker angry
    if len(vmd.camframes) != 0:
        boneframe_dict[NAME_FOR_CAMFRAMES] = vmd.camframes

    # >>>>>> part 0: verify that there are no "multiple frames on the same timestep" situations
    # the MMD GUI shouldn't let this happen, but apparently it has happened... how???
    # the only explanation I can think of is that it's due to physics bones with names that are too long and
    # get truncated, and the uniquifying numbers are in the part that got lost. they originally had one frame
    # per bone but because the names were truncated they look like they're all the same name so it looks like
    # there are many frames for that non-real bone at the same timestep.
    for bonename, boneframe_list in boneframe_dict.items():
        # if a bone has only 1 (or 0?) frames associated with it then there's definitely no overlap probelm
        if len(boneframe_list) < 2:
            continue
        i = 0
        while i < len(boneframe_list) - 1:
            # look at all pairs of adjacent frames along a bone
            A = boneframe_list[i]
            B = boneframe_list[i + 1]
            # are they on the same timestep? if so, problem!
            if A.f == B.f:
                # are they setting the same pose?
                if A == B:
                    # if they are setting the same values at the same frame, just fix the problem silently
                    pass
                else:
                    # if they are trying to set different values at the same frame, this is a problem!
                    # gotta fix it to continue, but also gotta print some kind of warning
                    if bonename == NAME_FOR_CAMFRAMES:
                        core.MY_PRINT_FUNC(
                            "WARNING: at timestep t=%d, there are multiple cam frames trying to set different poses. How does this even happen???"
                            % A.f)
                    else:
                        core.MY_PRINT_FUNC(
                            "WARNING: at timestep t=%d, there are multiple frames trying to set bone '%s' to different poses. How does this even happen???"
                            % (A.f, bonename))
                    core.MY_PRINT_FUNC(
                        "I will delete one of them and continue.")
                # remove the 2nd one so that there is only one frame at each timestep
                boneframe_list.pop(i + 1)
                continue
            # otherwise, no problem at all
            i += 1

    # >>>>>> part 1: identify the desired slope for each metric of each frame
    core.MY_PRINT_FUNC("Finding smooth approach/depart slopes...")
    global CURRENT_BONENAME
    allbone_bezier_slopes = {}
    for bonename in sorted(boneframe_dict.keys()):
        CURRENT_BONENAME = bonename  # you're not supposed to pass info via global like this, but idgaf sue me
        boneframe_list = boneframe_dict[bonename]
        # this will hold all the resulting bezier slopes
        # each item corresponds to one frame and is stored as:
        # [approach posx,y,z,rot],[depart posx,y,z,rot]
        thisbone_bezier_slopes = []

        # for each sequence of frames on a single bone,
        for i in range(len(boneframe_list)):

            thisframe_bezier_approach = []
            thisframe_bezier_depart = []

            A = boneframe_list[i - 1] if i != 0 else None
            B = boneframe_list[i]
            C = boneframe_list[i + 1] if i != len(boneframe_list) - 1 else None
            # now i have the 3 frames I want to analyze
            # need to do the analysis for rotations & for positions

            # POSITION
            for j in range(3):
                A_point = (A.f, A.pos[j]) if (A is not None) else None
                B_point = (B.f, B.pos[j])
                C_point = (C.f, C.pos[j]) if (C is not None) else None
                # stuffed all operations into one function for encapsulation
                bez_a, bez_d = scalar_calculate_ideal_bezier_slope(
                    A_point, B_point, C_point)
                # store it
                thisframe_bezier_approach.append(bez_a)
                thisframe_bezier_depart.append(bez_d)

            # ROTATION
            A_point = (A.f, A.rot) if (A is not None) else None
            B_point = (B.f, B.rot)
            C_point = (C.f, C.rot) if (C is not None) else None
            # stuffed all operations into one function for encapsulation
            bez_a, bez_d = rotation_calculate_ideal_bezier_slope(
                A_point, B_point, C_point)
            # store it
            thisframe_bezier_approach.append(bez_a)
            thisframe_bezier_depart.append(bez_d)

            # CAMFRAME ONLY STUFF
            if bonename == NAME_FOR_CAMFRAMES:
                # the typechecker expects boneframes so it gets angry here
                # distance from camera to position
                A_point = (A.f, A.dist) if (A is not None) else None
                B_point = (B.f, B.dist)
                C_point = (C.f, C.dist) if (C is not None) else None
                # stuffed all operations into one function for encapsulation
                bez_a, bez_d = scalar_calculate_ideal_bezier_slope(
                    A_point, B_point, C_point)
                # store it
                thisframe_bezier_approach.append(bez_a)
                thisframe_bezier_depart.append(bez_d)
                # field of view
                A_point = (A.f, A.fov) if (A is not None) else None
                B_point = (B.f, B.fov)
                C_point = (C.f, C.fov) if (C is not None) else None
                # stuffed all operations into one function for encapsulation
                bez_a, bez_d = scalar_calculate_ideal_bezier_slope(
                    A_point, B_point, C_point)
                # store it
                thisframe_bezier_approach.append(bez_a)
                thisframe_bezier_depart.append(bez_d)

            # next i need to store them in some sensible manner
            # ..., [approach posx,y,z,rot], [depart posx,y,z,rot], ...
            thisbone_bezier_slopes.append(thisframe_bezier_approach)
            thisbone_bezier_slopes.append(thisframe_bezier_depart)
            pass  # end "for each frame in this bone"
        # now i have calculated all the desired bezier approach/depart slopes for both rotation and position
        # next i need to rearrange things slightly

        # currently the slopes are stored in "approach,depart" pairs associated with a single frame.
        # but the interpolation curves are stored as "depart, approach" associated with the segment leading up to a frame.
        # AKA, interpolation info stored with frame i is to interpolate from i-1 to i
        # therefore there is no place for the slope when interpolating away from the last frame, pop it
        thisbone_bezier_slopes.pop(-1)
        # the new list needs to start with 1,1,1,1 to interpolate up to the first frame, insert it
        if bonename == NAME_FOR_CAMFRAMES:
            thisbone_bezier_slopes.insert(0, [1] * 6)
        else:
            thisbone_bezier_slopes.insert(0, [1] * 4)
        # now every pair is a "depart,approach" associated with a single frame
        final = []
        for i in range(0, len(thisbone_bezier_slopes), 2):
            # now store as pairs
            final.append(
                [thisbone_bezier_slopes[i], thisbone_bezier_slopes[i + 1]])

        assert len(final) == len(boneframe_list)

        # save it!
        allbone_bezier_slopes[bonename] = final
        pass  # end of "for each bone

    # >>>>>> part 2: calculate the x/y position of the control points for the curve, based on the slope
    core.MY_PRINT_FUNC("Calculating control points...")
    allbone_bezier_points = {}
    for bonename in sorted(allbone_bezier_slopes.keys()):
        bezier_for_one_frame = allbone_bezier_slopes[bonename]
        thisbone_bezier_points = []
        for depart_slopes, approach_slopes in bezier_for_one_frame:
            slopes_per_channel = list(zip(depart_slopes, approach_slopes))
            # print(slopes_per_channel)
            depart_points = []
            approach_points = []
            for depart_slope, approach_slope in slopes_per_channel:
                # now i have the approach & depart for one channel of one frame of one bone
                # 1. handle double-sided cutpoint
                if approach_slope == -1 and depart_slope == -1:
                    # this is a double-sided cutpoint!
                    # see where the global is declared to understand the modes
                    if HOW_TO_HANDLE_DOUBLE_CUTPOINT == 1:
                        approach_slope, depart_slope = 0, 0
                    else:  #elif HOW_TO_HANDLE_DOUBLE_CUTPOINT == 2:
                        approach_slope, depart_slope = 1, 1

                # 3a. in this mode the cutpoint is handled BEFORE normal calculation
                if HOW_TO_HANDLE_SINGLE_SIDE_CUTPOINT == 1:
                    if approach_slope == -1:
                        approach_slope = 0
                    if depart_slope == -1:
                        depart_slope = 0

                # 2. base case: calculate the point position based on the slope
                depart_point = (10, 10)
                approach_point = (117, 117)
                if approach_slope != -1:
                    # note: the approach point is based on 127,127
                    approach_point = tuple(
                        127 - p for p in make_point_from_slope(approach_slope))
                if depart_slope != -1:
                    depart_point = make_point_from_slope(depart_slope)

                # 3b. handle the one-sided cutpoint
                if HOW_TO_HANDLE_SINGLE_SIDE_CUTPOINT == 2:
                    # fancy "point at the control point of the other side" idea
                    # define the slope via the opposing control point and re-run step 2
                    if approach_slope == -1:
                        # note: depart_point[0] can be 127, if so then this is divide by 0
                        if depart_point[0] == 127:
                            approach_slope = 1000
                        else:
                            approach_slope = (depart_point[1] -
                                              127) / (depart_point[0] - 127)
                        # note: the approach point is based on 127,127
                        approach_point = tuple(
                            127 - p
                            for p in make_point_from_slope(approach_slope))
                    if depart_slope == -1:
                        # note: approach_point[0] CAN BE 0, in theory.
                        if approach_point[0] == 0:
                            depart_slope = 1000
                        else:
                            depart_slope = approach_point[1] / approach_point[0]
                        depart_point = make_point_from_slope(depart_slope)

                # 4. accumulate teh channels
                depart_points.append(depart_point)
                approach_points.append(approach_point)
                pass  # end "for one channel of one frame of one bone"
            # 5. accumulate all the frames
            thisbone_bezier_points.append([depart_points, approach_points])
            pass  # end "for one frame of one bone"
        # 6. accumulate teh bones
        allbone_bezier_points[bonename] = thisbone_bezier_points
        pass  # end "for one bone"

    # >>>>>> part 3: store this into the boneframe & un-dictify the frames to put it back into the VMD
    for bonename in sorted(boneframe_dict.keys()):
        boneframe_list = boneframe_dict[bonename]
        bezier_points_list = allbone_bezier_points[bonename]
        if bonename == NAME_FOR_CAMFRAMES:
            # this is a list of camframes!
            # for each frame & associated points,
            for camframe, b in zip(boneframe_list, bezier_points_list):
                # print(b)
                # interp = [x_ax, x_bx, x_ay, x_by, 	y_ax, y_bx, y_ay, y_by, 				z_ax, z_bx, z_ay, z_by,
                # 			r_ax, r_bx, r_ay, r_by,		dist_ax, dist_bx, dist_ay, dist_by, 	fov_ax, fov_bx, fov_ay, fov_by]
                interp = [
                    b[0][0][0],
                    b[1][0][0],
                    b[0][0][1],
                    b[1][0][1],
                    b[0][1][0],
                    b[1][1][0],
                    b[0][1][1],
                    b[1][1][1],
                    b[0][2][0],
                    b[1][2][0],
                    b[0][2][1],
                    b[1][2][1],
                    b[0][3][0],
                    b[1][3][0],
                    b[0][3][1],
                    b[1][3][1],
                    b[0][4][0],
                    b[1][4][0],
                    b[0][4][1],
                    b[1][4][1],
                    b[0][5][0],
                    b[1][5][0],
                    b[0][5][1],
                    b[1][5][1],
                ]
                camframe.interp = interp
        else:
            # for each frame & associated points,
            for boneframe, b in zip(boneframe_list, bezier_points_list):
                # print(b)
                # interp = [x_ax, y_ax, z_ax, r_ax, 	x_ay, y_ay, z_ay, r_ay,
                # 			x_bx, y_bx, z_bx, r_bx, 	x_by, y_by, z_by, r_by]
                interp = [
                    b[0][0][0],
                    b[0][1][0],
                    b[0][2][0],
                    b[0][3][0],
                    b[0][0][1],
                    b[0][1][1],
                    b[0][2][1],
                    b[0][3][1],
                    b[1][0][0],
                    b[1][1][0],
                    b[1][2][0],
                    b[1][3][0],
                    b[1][0][1],
                    b[1][1][1],
                    b[1][2][1],
                    b[1][3][1],
                ]
                # this goes into the actual boneframe object still in the lists in boneframe_dict
                boneframe.interp = interp

    # un-dictify it!
    # first, extract the camframes
    if NAME_FOR_CAMFRAMES in boneframe_dict:
        vmd.camframes = boneframe_dict.pop(NAME_FOR_CAMFRAMES)
    # then do the boneframes
    # the names dont matter, make a list of all the lists in the dict
    asdf = list(boneframe_dict.values())
    # flatten it
    flat_boneframe_list = [item for sublist in asdf for item in sublist]
    vmd.boneframes = flat_boneframe_list

    core.MY_PRINT_FUNC("")
    # write out the VMD
    output_filename_vmd = "%s_smoothed.vmd" % input_filename_vmd[0:-4]
    output_filename_vmd = core.get_unused_file_name(output_filename_vmd)
    vmdlib.write_vmd(output_filename_vmd, vmd, moreinfo=moreinfo)

    # H = plt.hist([j for j in ANGLE_SHARPNESS_FACTORS if j!=0 and j!=1], bins=40, density=True)
    print("factors=", len(ANGLE_SHARPNESS_FACTORS))
    H = plt.hist(ANGLE_SHARPNESS_FACTORS, bins=16, density=True)
    plt.show()

    core.MY_PRINT_FUNC("Done!")
    return None
Example #6
0
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