Esempio n. 1
0
def make_zipfile_backup(startpath: str, backup_suffix: str) -> bool:
    """
	Make a .zip backup of the folder 'startpath' and all its contents. Returns True if all goes well, False if it should abort.
	Resulting zip will be adjacent to the folder it is backing up with a slightly different name.
	
	:param startpath: absolute path of the folder you want to zip
	:param backup_suffix: segment inserted between the foldername and .zip extension
	:return: true if things are good, False if i should abort
	"""
    # need to add .zip for checking against already-exising files and for printing
    zipname = startpath + "." + backup_suffix + ".zip"
    zipname = core.get_unused_file_name(zipname)
    core.MY_PRINT_FUNC("...making backup archive:")
    core.MY_PRINT_FUNC(zipname)
    try:
        root_dir = os.path.dirname(startpath)
        base_dir = os.path.basename(startpath)
        # need to remove .zip suffix because zipper forcefully adds .zip whether its already on the name or not
        shutil.make_archive(zipname[:-4], 'zip', root_dir, base_dir)
    except Exception as e:
        core.MY_PRINT_FUNC(e.__class__.__name__, e)
        info = [
            "ERROR3! Unable to create zipfile for backup.",
            "Do you want to continue without a zipfile backup?",
            "1 = Yes, 2 = No (abort)"
        ]
        r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
        if r == 2: return False
    return True
def main(moreinfo=True):
    # prompt PMX name
    core.MY_PRINT_FUNC("Please enter name of PMX input file:")
    input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
    pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)

    core.MY_PRINT_FUNC("L upper arm...")
    make_autotwist_segment(pmx, jp_l, jp_arm, jp_armtwist, jp_elbow, moreinfo)

    core.MY_PRINT_FUNC("R upper arm...")
    make_autotwist_segment(pmx, jp_r, jp_arm, jp_armtwist, jp_elbow, moreinfo)

    core.MY_PRINT_FUNC("L lower arm...")
    make_autotwist_segment(pmx, jp_l, jp_elbow, jp_wristtwist, jp_wrist,
                           moreinfo)

    core.MY_PRINT_FUNC("R lower arm...")
    make_autotwist_segment(pmx, jp_r, jp_elbow, jp_wristtwist, jp_wrist,
                           moreinfo)

    # if i want to, set elbowD parent to armT...?
    # if i want to, set wrist parent to elbowT...?
    # that's what the original does, but why? why would I want that?
    # armT should have exactly the same deformations as armtwist
    # it's better to have the twist-rigs be isolated from eachother

    # TODO: examine leg system! not universal because nobody has legtwist bones to hijack but worth understanding

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_autotwist.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 3
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)

    # to shift the model by a set amount:
    # first, ask user for X Y Z

    # create the prompt popup
    shift_str = core.MY_GENERAL_INPUT_FUNC(
        lambda x: (is_3float(x) is not None), [
            "Enter the X,Y,Z amount to shift this model by:",
            "Three decimal values separated by commas.",
            "Empty input will quit the script."
        ])

    # if empty, quit
    if shift_str == "":
        core.MY_PRINT_FUNC("quitting")
        return None
    # use the same func to convert the input string
    shift = is_3float(shift_str)

    ####################
    # then execute the shift:
    for v in pmx.verts:
        # every vertex position
        for i in range(3):
            v.pos[i] += shift[i]
        # c, r0, r1 params of every SDEF vertex
        # these correspond to real positions in 3d space so they need to be modified
        if v.weighttype == pmxstruct.WeightMode.SDEF:
            for param in v.weight_sdef:
                for i in range(3):
                    param[i] += shift[i]

    # bone position
    for b in pmx.bones:
        for i in range(3):
            b.pos[i] += shift[i]

    # rigid body position
    for rb in pmx.rigidbodies:
        for i in range(3):
            rb.pos[i] += shift[i]

    # joint position
    for j in pmx.joints:
        for i in range(3):
            j.pos[i] += shift[i]

    # that's it? that's it!

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_shift.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
def main(moreinfo=True):
	# prompt PMX name
	core.MY_PRINT_FUNC("Please enter name of PMX input file:")
	input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
	pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)
	
	# usually want to hide many morphs at a time, so put all this in a loop
	num_hidden = 0
	while True:
		core.MY_PRINT_FUNC("")
		# valid input is any string that can matched aginst a morph idx
		s = core.MY_GENERAL_INPUT_FUNC(lambda x: (morph_scale.get_idx_in_pmxsublist(x, pmx.morphs) is not None),
		   ["Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).",
			"Empty input will quit the script."])
		# do it again, cuz the lambda only returns true/false
		target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.morphs)
		
		# when given empty text, done!
		if target_index == -1 or target_index is None:
			core.MY_PRINT_FUNC("quitting")
			break
		
		# determine the morph type
		morphtype = pmx.morphs[target_index].morphtype
		core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format(
			morphtype, target_index, pmx.morphs[target_index].name_jp, pmx.morphs[target_index].name_en))
		core.MY_PRINT_FUNC("Was group {}, now group {}".format(
			pmx.morphs[target_index].panel, pmxstruct.MorphPanel.HIDDEN))
		# make the actual change
		pmx.morphs[target_index].panel = pmxstruct.MorphPanel.HIDDEN
		num_hidden += 1
		pass
	
	if num_hidden == 0:
		core.MY_PRINT_FUNC("Nothing was changed")
		return None
	
	# last step: remove all invalid morphs from all display panels
	for d, frame in enumerate(pmx.frames):  # for each display group,
		i = 0
		while i < len(frame.items):  # for each item in that display group,
			item = frame.items[i]
			if item.is_morph:  # if it is a morph
				# look up the morph
				morph = pmx.morphs[item.idx]
				# figure out what panel of this morph is
				# if it has an invalid panel #, delete it here
				if morph.panel == pmxstruct.MorphPanel.HIDDEN:
					frame.items.pop(i)
				else:
					i += 1
			else:
				i += 1
	
	# write out
	output_filename_pmx = input_filename_pmx[0:-4] + "_morphhide.pmx"
	output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
	pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
	core.MY_PRINT_FUNC("Done!")
	return None
Esempio n. 5
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
Esempio n. 6
0
def end(pmx, input_filename_pmx):
    # write out
    # output_filename_pmx = "%s_winnow.pmx" % core.get_clean_basename(input_filename_pmx)
    output_filename_pmx = input_filename_pmx[0:-4] + "_winnow.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=True)
    return None
Esempio n. 7
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)
    realbones = pmx.bones  # get bones
    realmorphs = pmx.morphs  # get morphs
    modelname_jp = pmx.header.name_jp
    modelname_en = pmx.header.name_en

    bonelist_out = [["modelname_jp", "'" + modelname_jp + "'"],
                    ["modelname_en", "'" + modelname_en + "'"],
                    ["bonename_jp", "bonename_en"]]
    morphlist_out = [["modelname_jp", "'" + modelname_jp + "'"],
                     ["modelname_en", "'" + modelname_en + "'"],
                     ["morphname_jp", "morphname_en"]]

    # in both lists, idx0 = name_jp, idx1 = name_en
    bonelist_pairs = [[a.name_jp, a.name_en] for a in realbones]
    morphlist_pairs = [[a.name_jp, a.name_en] for a in realmorphs]
    bonelist_out += bonelist_pairs
    morphlist_out += morphlist_pairs

    # write out
    output_filename_bone = "%s_bone_names.txt" % input_filename_pmx[0:-4]
    # output_filename_bone = output_filename_bone.replace(" ", "_")
    output_filename_bone = core.get_unused_file_name(output_filename_bone)
    core.MY_PRINT_FUNC("...writing result to file '%s'..." %
                       output_filename_bone)
    core.write_csvlist_to_file(output_filename_bone,
                               bonelist_out,
                               use_jis_encoding=False)

    output_filename_morph = "%s_morph_names.txt" % input_filename_pmx[0:-4]
    output_filename_morph = core.get_unused_file_name(output_filename_morph)
    core.MY_PRINT_FUNC("...writing result to file '%s'..." %
                       output_filename_morph)
    core.write_csvlist_to_file(output_filename_morph,
                               morphlist_out,
                               use_jis_encoding=False)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 8
0
def convert_txt_to_vmd(input_filename, moreinfo=True):
	"""
	Read a VMD-as-text file from disk, convert it, and write to disk as a VMD motion file.
	The output will have the same path and basename, but the opposite file extension.
	
	:param input_filename: filepath to input vmd, absolute or relative to CWD
	:param moreinfo: default false. if true, get extra printouts with more info about stuff.
	"""
	# read the VMD-as-text into the nicelist format, all in one function
	vmd_nicelist = read_vmdtext(input_filename)
	core.MY_PRINT_FUNC("")
	# identify an unused filename for writing the output
	dumpname = core.get_unused_file_name(input_filename[0:-4] + ".vmd")
	# write the output VMD file
	vmdlib.write_vmd(dumpname, vmd_nicelist, moreinfo=moreinfo)
	
	# done!
	return
Esempio n. 9
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
Esempio n. 10
0
def convert_vpd_to_vmd(vpd_path: str, moreinfo=True):
    """
	Read a VPD pose file from disk, convert it, and write to disk as a VMD motion file.
	The resulting VMD will be empty except for bone/morph frames at time=0.
	The output will have the same path and basename, but the opposite file extension.
	
	:param vpd_path: filepath to input vpd, absolute or relative to CWD
	:param moreinfo: default false. if true, get extra printouts with more info about stuff.
	"""
    # read the VPD into memory as a VMD object
    vmd = vpdlib.read_vpd(vpd_path, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("")
    # identify an unused filename for writing the output
    vmd_outpath = core.get_unused_file_name(vpd_path[0:-4] + ".vmd")
    # write the output VMD file
    vmdlib.write_vmd(vmd_outpath, vmd, moreinfo=moreinfo)
    # done!
    return
Esempio n. 11
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)

    core.MY_PRINT_FUNC("")
    # valid input is any string that can matched aginst a morph idx
    s = core.MY_GENERAL_INPUT_FUNC(
        lambda x: get_idx_in_pmxsublist(x, pmx.morphs) is not None, [
            "Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).",
            "Empty input will quit the script."
        ])
    # do it again, cuz the lambda only returns true/false
    target_index = get_idx_in_pmxsublist(s, pmx.morphs)

    # when given empty text, done!
    if target_index == -1 or target_index is None:
        core.MY_PRINT_FUNC("quitting")
        return None

    # determine the morph type
    morphtype = pmx.morphs[target_index].morphtype
    core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format(
        morphtype, target_index, pmx.morphs[target_index].name_jp,
        pmx.morphs[target_index].name_en))

    # if it is a bone morph, ask for translation/rotation/both
    bone_mode = 0
    if morphtype == pmxstruct.MorphType.BONE:
        bone_mode = core.MY_SIMPLECHOICE_FUNC((1, 2, 3), [
            "Bone morph detected: do you want to scale the motion(translation), rotation, or both?",
            "1 = motion(translation), 2 = rotation, 3 = both"
        ])

    # ask for factor: keep looping this prompt until getting a valid float
    def is_float(x):
        try:
            v = float(x)
            return True
        except ValueError:
            core.MY_PRINT_FUNC("Please enter a decimal number")
            return False

    factor_str = core.MY_GENERAL_INPUT_FUNC(
        is_float, "Enter the factor that you want to scale this morph by:")
    if factor_str == "":
        core.MY_PRINT_FUNC("quitting")
        return None
    factor = float(factor_str)

    # important values: target_index, factor, morphtype, bone_mode
    # first create the new morph that is a copy of current
    if SCALE_MORPH_IN_PLACE:
        newmorph = pmx.morphs[target_index]
    else:
        newmorph = copy.deepcopy(pmx.morphs[target_index])
        # then modify the names
        name_suffix = "*" + (str(factor)[0:6])
        newmorph.name_jp += name_suffix
        newmorph.name_en += name_suffix
    # now scale the actual values

    r = morph_scale(newmorph, factor, bone_mode)

    if not r:
        core.MY_PRINT_FUNC("quitting")
        return None

    pmx.morphs.append(newmorph)

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + ("_%dscal.pmx" %
                                                      target_index)
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
def main(moreinfo=False):
	# prompt PMX name
	core.MY_PRINT_FUNC("Please enter name of PMX model file:")
	input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
	pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)
	
	#### how should these operations be ordered?
	# faces before verts, because faces define what verts are used
	# verts before weights, so i operate on fewer vertices & run faster
	# weights before bones, because weights determine what bones are used
	# verts before morph winnow, so i operate on fewer vertices & run faster
	# translate after bones/disp groups/morph winnow because they reduce the # of things to translate
	# uniquify after translate, because translate can map multiple different JP to same EN names
	# alphamorphs after translate, so it uses post-translate names for printing
	# deform order after translate, so it uses post-translate names for printing
	
	# if ANY stage returns True then it has made changes
	# final file-write is skipped only if NO stage has made changes
	is_changed = False
	core.MY_PRINT_FUNC("\n>>>> Deleting invalid & duplicate faces <<<<")
	pmx, is_changed_t = _prune_invalid_faces.prune_invalid_faces(pmx, moreinfo)
	is_changed |= is_changed_t	# or-equals: if any component returns true, then ultimately this func returns true
	core.MY_PRINT_FUNC("\n>>>> Deleting orphaned/unused vertices <<<<")
	pmx, is_changed_t = _prune_unused_vertices.prune_unused_vertices(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Deleting unused bones <<<<")
	pmx, is_changed_t = _prune_unused_bones.prune_unused_bones(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Normalizing vertex weights & normals <<<<")
	pmx, is_changed_t = _weight_cleanup.weight_cleanup(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Pruning imperceptible vertex morphs <<<<")
	pmx, is_changed_t = _morph_winnow.morph_winnow(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Fixing display groups: duplicates, empty groups, missing items <<<<")
	pmx, is_changed_t = _dispframe_fix.dispframe_fix(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Adding missing English names <<<<")
	pmx, is_changed_t = _translate_to_english.translate_to_english(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Ensuring all names in the model are unique <<<<")
	pmx, is_changed_t = _uniquify_names.uniquify_names(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Fixing bone deform order <<<<")
	pmx, is_changed_t = _bonedeform_fix.bonedeform_fix(pmx, moreinfo)
	is_changed |= is_changed_t
	core.MY_PRINT_FUNC("\n>>>> Standardizing alphamorphs and accounting for edging <<<<")
	pmx, is_changed_t = _alphamorph_correct.alphamorph_correct(pmx, moreinfo)
	is_changed |= is_changed_t

	core.MY_PRINT_FUNC("")
	core.MY_PRINT_FUNC("++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
	core.MY_PRINT_FUNC("++++      Scanning for other potential issues      ++++")

	longbone, longmorph = find_toolong_bonemorph(pmx)
	# also checks that bone/morph names can be stored in shift_jis for VMD usage
	if longmorph or longbone:
		core.MY_PRINT_FUNC("")
		core.MY_PRINT_FUNC("Minor warning: this model contains bones/morphs with JP names that are too long (>15 bytes)")
		core.MY_PRINT_FUNC("These will work just fine in MMD but will not properly save/load in VMD motion files")
		if longbone:
			ss = "[" + ", ".join(longbone[0:MAX_WARNING_LIST]) + "]"
			if len(longbone) > MAX_WARNING_LIST:
				ss = ss[0:-1] + ", ...]"
			core.MY_PRINT_FUNC("These %d bones are too long (index[length]): %s" % (len(longbone), ss))
		if longmorph:
			ss = "[" + ", ".join(longmorph[0:MAX_WARNING_LIST]) + "]"
			if len(longmorph) > MAX_WARNING_LIST:
				ss = ss[0:-1] + ", ...]"
			core.MY_PRINT_FUNC("These %d morphs are too long (index[length]): %s" % (len(longmorph), ss))
	
	shadowy_mats = find_shadowy_materials(pmx)
	if shadowy_mats:
		core.MY_PRINT_FUNC("")
		core.MY_PRINT_FUNC("Minor warning: this model contains transparent materials with visible edging")
		core.MY_PRINT_FUNC("Edging is visible even if the material is transparent, so this will look like an ugly silhouette")
		core.MY_PRINT_FUNC("Either disable edging in MMD when using this model, or reduce the edge parameters to 0 and re-add them in the morph that restores its opacity")
		ss = str(shadowy_mats[0:MAX_WARNING_LIST])
		if len(shadowy_mats) > MAX_WARNING_LIST:
			ss = ss[0:-1] + ", ...]"
		core.MY_PRINT_FUNC("These %d materials need edging disabled (index): %s" % (len(shadowy_mats), ss))
	
	boneless_bodies = find_boneless_bonebodies(pmx)
	if boneless_bodies:
		core.MY_PRINT_FUNC("")
		core.MY_PRINT_FUNC("WARNING: this model has bone-type rigidbodies that aren't anchored to any bones")
		core.MY_PRINT_FUNC("This won't crash MMD but it is probably a mistake that needs corrected")
		ss = str(boneless_bodies[0:MAX_WARNING_LIST])
		if len(boneless_bodies) > MAX_WARNING_LIST:
			ss = ss[0:-1] + ", ...]"
		core.MY_PRINT_FUNC("These %d bodies are boneless (index): %s" % (len(boneless_bodies), ss))
		
	jointless_bodies = find_jointless_physbodies(pmx)
	if jointless_bodies:
		core.MY_PRINT_FUNC("")
		core.MY_PRINT_FUNC("WARNING: this model has physics-type rigidbodies that aren't constrained by joints")
		core.MY_PRINT_FUNC("These will just roll around on the floor wasting processing power in MMD")
		ss = str(jointless_bodies[0:MAX_WARNING_LIST])
		if len(jointless_bodies) > MAX_WARNING_LIST:
			ss = ss[0:-1] + ", ...]"
		core.MY_PRINT_FUNC("These %d bodies are jointless (index): %s" % (len(jointless_bodies), ss))
		
	crashing_joints = find_crashing_joints(pmx)
	if crashing_joints:
		# make the biggest f*****g alert i can cuz this is a critical issue
		core.MY_PRINT_FUNC("")
		core.MY_PRINT_FUNC("! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ")
		core.MY_PRINT_FUNC("CRITICAL WARNING: this model contains invalid joints which WILL cause MMD to crash!")
		core.MY_PRINT_FUNC("These must be manually deleted or repaired using PMXE")
		core.MY_PRINT_FUNC("These %d joints are invalid (index): %s" % (len(crashing_joints), crashing_joints))
	
	core.MY_PRINT_FUNC("")
	core.MY_PRINT_FUNC("++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
	if not is_changed:
		core.MY_PRINT_FUNC("++++             No writeback required              ++++")
		core.MY_PRINT_FUNC("Done!")
		return
	
	core.MY_PRINT_FUNC("++++ Done with cleanup, saving improvements to file ++++")
	
	# write out
	# output_filename_pmx = "%s_better.pmx" % core.get_clean_basename(input_filename_pmx)
	output_filename_pmx = input_filename_pmx[0:-4] + "_better.pmx"
	output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
	pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
	core.MY_PRINT_FUNC("Done!")
	return None
Esempio n. 13
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)

    # usually want to add/remove endpoints for many bones at once, so put all this in a loop
    num_changed = 0
    while True:
        core.MY_PRINT_FUNC("")
        # valid input is any string that can matched aginst a bone idx
        s = core.MY_GENERAL_INPUT_FUNC(
            lambda x:
            (morph_scale.get_idx_in_pmxsublist(x, pmx.bones) is not None), [
                "Please specify the target bone: bone #, JP name, or EN name (names are not case sensitive).",
                "Empty input will quit the script."
            ])
        # do it again, cuz the lambda only returns true/false
        target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.bones)

        # when given empty text, done!
        if target_index == -1 or target_index is None:
            core.MY_PRINT_FUNC("quitting")
            break
        target_bone = pmx.bones[target_index]

        # print the bone it found
        core.MY_PRINT_FUNC("Found bone #{}: '{}' / '{}'".format(
            target_index, target_bone.name_jp, target_bone.name_en))

        if target_bone.tail_type:
            core.MY_PRINT_FUNC(
                "Was tailmode 'bonelink', changing to mode 'offset'")
            if target_bone.tail == -1:
                core.MY_PRINT_FUNC(
                    "Error: bone is not linked to anything, skipping")
                continue
            # find the location of the bone currently pointing at
            endpos = pmx.bones[target_bone.tail].pos
            # determine the equivalent offset vector
            offset = [endpos[i] - target_bone.pos[i] for i in range(3)]
            # write it into the bone
            target_bone.tail_type = False
            target_bone.tail = offset
            # done unlinking endpoint!
            pass

        else:
            core.MY_PRINT_FUNC(
                "Was tailmode 'offset', changing to mode 'bonelink' and adding new endpoint bone"
            )
            if target_bone.tail == [0, 0, 0]:
                core.MY_PRINT_FUNC(
                    "Error: bone has offset of [0,0,0], skipping")
                continue
            # determine the position of the new endpoint bone
            endpos = [
                target_bone.pos[i] + target_bone.tail[i] for i in range(3)
            ]
            # create the new bone
            newbone = pmxstruct.PmxBone(
                name_jp=target_bone.name_jp + endpoint_suffix_jp,
                name_en=target_bone.name_en + endpoint_suffix_en,
                pos=endpos,
                parent_idx=target_index,
                deform_layer=target_bone.deform_layer,
                deform_after_phys=target_bone.deform_after_phys,
                has_rotate=False,
                has_translate=False,
                has_visible=False,
                has_enabled=True,
                has_ik=False,
                has_localaxis=False,
                has_fixedaxis=False,
                has_externalparent=False,
                inherit_rot=False,
                inherit_trans=False,
                tail_type=True,
                tail=-1)
            # set the target to point at the new bone
            target_bone.tail_type = True
            target_bone.tail = len(pmx.bones)
            # append the new bone
            pmx.bones.append(newbone)
            # done adding endpoint!
            pass

        num_changed += 1
        pass

    if num_changed == 0:
        core.MY_PRINT_FUNC("Nothing was changed")
        return None

    core.MY_PRINT_FUNC("")

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_endpoints.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 14
0
def main(moreinfo=False):
    core.MY_PRINT_FUNC("Please enter name of PMX model file:")
    input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")

    # step zero: set up the translator thingy
    translate_to_english.init_googletrans()

    # texture sorting plan:
    # 1. get startpath = basepath of input PMX
    # 2. get lists of relevant files
    # 	2a. extract top-level 'neighbor' pmx files from all-set
    # 3. ask about modifying neighbor PMX
    # 4. read PMX: either target or target+all neighbor
    # 5. "categorize files & normalize usages within PMX", NEW FUNC!!!
    # 6. translate all names via Google Trans, don't even bother with local dict
    # 7. mask out invalid windows filepath chars just to be safe
    # 8. print proposed names & other findings
    # 	for unused files under a folder, combine & replace with ***
    # 9. ask for confirmation
    # 10. zip backup (NEW FUNC!)
    # 11. apply renaming, NEW FUNC! rename all including old PMXes on disk
    # 12. get new names for PMXes, write PMX from mem to disk if any of its contents changed
    #	i.e. of all FileRecord with a new name, create a set of all the PMX that use them

    # absolute path to directory holding the pmx
    input_filename_pmx_abs = os.path.normpath(
        os.path.abspath(input_filename_pmx))
    startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs)

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files
    relative_all_exist_files = file_sort_textures.walk_filetree_from_root(
        startpath)
    core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files))
    # now fill "neighbor_pmx" by finding files without path separator that end in PMX
    # these are relative paths tho
    neighbor_pmx = [
        f for f in relative_all_exist_files
        if (f.lower().endswith(".pmx")) and (
            os.path.sep not in f) and f != input_filename_pmx_rel
    ]

    # no filtering, all files are relevant
    relevant_exist_files = relative_all_exist_files

    core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx))

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # now ask if I care about the neighbors and read the PMXes into memory

    pmx_filenames = [input_filename_pmx_rel]

    if neighbor_pmx:
        core.MY_PRINT_FUNC("")
        info = [
            "Detected %d top-level neighboring PMX files, these probably share the same filebase as the target."
            % len(neighbor_pmx),
            "If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.",
            "Do you want to process all neighbors in addition to the target? (highly recommended)",
            "1 = Yes, 2 = No"
        ]
        r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
        if r == 1:
            core.MY_PRINT_FUNC("Processing target + all neighbor files")
            # append neighbor PMX files onto the list of files to be processed
            pmx_filenames += neighbor_pmx
        else:
            core.MY_PRINT_FUNC(
                "WARNING: Processing only target, ignoring %d neighbor PMX files"
                % len(neighbor_pmx))
    # now read all the PMX objects & store in dict alongside the relative name
    # dictionary where keys are filename and values are resulting pmx objects
    all_pmx_obj = {}
    for this_pmx_name in pmx_filenames:
        this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name),
                                       moreinfo=moreinfo)
        all_pmx_obj[this_pmx_name] = this_pmx_obj

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # 	for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk
    #	also fill out how much and how each file is used, and unify dupes between files, all that good stuff

    filerecord_list = file_sort_textures.categorize_files(
        all_pmx_obj, relevant_exist_files, moreinfo)

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # DETERMINE NEW NAMES FOR FILES

    # how to remap: build a list of all destinations (lowercase) to see if any proposed change would lead to collision
    all_new_names = set()

    # get new names via google
    # force it to use chunk-wise translate
    newname_list = translate_to_english.google_translate(
        [p.name for p in filerecord_list], strategy=1)

    # now repair any windows-forbidden symbols that might have shown up after translation
    newname_list = [
        n.translate(invalid_windows_chars_ord) for n in newname_list
    ]

    # iterate over the results in parallel with the FileRecord items
    for p, newname in zip(filerecord_list, newname_list):
        if newname != p.name:
            # resolve potential collisions by adding numbers suffix to file names
            # first need to make path absolute so get_unused_file_name can check the disk.
            # then check uniqueness against files on disk and files in namelist (files that WILL be on disk)
            newname = core.get_unused_file_name(os.path.join(
                startpath, newname),
                                                namelist=all_new_names)
            # now dest path is guaranteed unique against other existing files & other proposed name changes
            all_new_names.add(newname.lower())
            # make the path no longer absolute: undo adding "startpath" above
            newname = os.path.relpath(newname, startpath)
            p.newname = newname

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # NOW PRINT MY PROPOSED RENAMINGS and other findings

    # isolate the ones with proposed renaming
    translated_file = [u for u in filerecord_list if u.newname is not None]

    if translated_file:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC("Found %d JP filenames to be translated:" %
                           len(translated_file))
        oldname_list = core.MY_JUSTIFY_STRINGLIST(
            [p.name for p in translated_file])
        newname_list = [p.newname for p in translated_file]
        zipped = list(zip(oldname_list, newname_list))
        zipped_and_sorted = sorted(
            zipped, key=lambda y: file_sort_textures.sortbydirdepth(y[0]))
        for o, n in zipped_and_sorted:
            # print 'from' with the case/separator it uses in the PMX
            core.MY_PRINT_FUNC("   {:s} --> {:s}".format(o, n))
        core.MY_PRINT_FUNC("=" * 60)
    else:
        core.MY_PRINT_FUNC("No proposed file changes")
        core.MY_PRINT_FUNC("Aborting: no files were changed")
        return None

    info = [
        "Do you accept these new names/locations?", "1 = Yes, 2 = No (abort)"
    ]
    r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
    if r == 2:
        core.MY_PRINT_FUNC("Aborting: no files were changed")
        return None

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # finally, do the actual renaming:

    # first, create a backup of the folder
    if MAKE_BACKUP_BEFORE_RENAMES:
        r = file_sort_textures.make_zipfile_backup(startpath, BACKUP_SUFFIX)
        if not r:
            # this happens if the backup failed somehow AND the user decided to quit
            core.MY_PRINT_FUNC("Aborting: no files were changed")
            return None

    # do all renaming on disk and in PMXes, and also handle the print statements
    file_sort_textures.apply_file_renaming(all_pmx_obj, filerecord_list,
                                           startpath)

    # write out
    for this_pmx_name, this_pmx_obj in all_pmx_obj.items():
        # what name do i write this pmx to? it may be different now! find it in the FileRecord!
        # this script does not filter filerecord_list so it is guaranteed to hae a record
        rec = None
        for r in filerecord_list:
            if r.name == this_pmx_name:
                rec = r
                break
        if rec.newname is None:
            # if there is no new name, write back to the name it had previously
            new_pmx_name = rec.name
        else:
            # if there is a new name, write to the new name
            new_pmx_name = rec.newname
        # make the name absolute
        output_filename_pmx = os.path.join(startpath, new_pmx_name)
        # write it, overwriting the existing file at that name
        pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo)

    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 15
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")
    # input_filename_pmx = "../../python_scripts/grasstest_better.pmx"
    pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)

    ##################################
    # user flow:
    # first ask whether they want to add armtwist, yes/no
    # second ask whether they want to add legtwist, yes/no
    # then do it
    # then write out to file
    ##################################

    working_queue = []

    s = core.MY_SIMPLECHOICE_FUNC((1, 2), [
        "Do you wish to add magic twistbones to the ARMS?", "1 = Yes, 2 = No"
    ])
    if s == 1:
        # add upperarm set and lowerarm set to the queue
        working_queue.append(armset)
        working_queue.append(wristset)
        pass
    s = core.MY_SIMPLECHOICE_FUNC((1, 2), [
        "Do you wish to add magic twistbones to the LEGS?", "1 = Yes, 2 = No"
    ])
    if s == 1:
        # TODO detect whether d-bones exist or not
        # add legs or d-legs set to the queue
        pass

    if not working_queue:
        core.MY_PRINT_FUNC("Nothing was changed")
        core.MY_PRINT_FUNC("Done")
        return None

    # for each set in the queue,
    for boneset in working_queue:
        # boneset = (start, end, preferred, oldrigs, bezier)
        for side in [jp_l, jp_r]:
            # print(side)
            # print(boneset)
            # 1. first, validate that start/end exist, these are required
            # NOTE: remember to prepend 'side' before all jp names!
            start_jp = side + boneset[0]
            start_idx = core.my_list_search(pmx.bones,
                                            lambda x: x.name_jp == start_jp)
            if start_idx is None:
                core.MY_PRINT_FUNC(
                    "ERROR: standard bone '%s' not found in model, this is required!"
                    % start_jp)
                continue
            end_jp = side + boneset[1]
            end_idx = core.my_list_search(pmx.bones,
                                          lambda x: x.name_jp == end_jp)
            if end_idx is None:
                core.MY_PRINT_FUNC(
                    "ERROR: standard bone '%s' not found in model, this is required!"
                    % end_jp)
                continue

            # 2. determine whether the 'preferredparent' exists and therefore what to acutally use as the parent
            parent_jp = side + boneset[2]
            parent_idx = core.my_list_search(pmx.bones,
                                             lambda x: x.name_jp == parent_jp)
            if parent_idx is None:
                parent_idx = start_idx

            # 3. attempt to collapse known armtwist rig names onto 'parent' so that the base case is further automated
            # for each bonename in boneset[3], if it exists, collapse onto boneidx parent_idx
            for bname in boneset[3]:
                rig_idx = core.my_list_search(
                    pmx.bones, lambda x: x.name_jp == side + bname)
                if rig_idx is None: continue  # if not found, try the next
                # when it is found, what 'factor' do i use?
                # print(side+bname)
                if pmx.bones[rig_idx].inherit_rot and pmx.bones[
                        rig_idx].inherit_parent_idx == parent_idx and pmx.bones[
                            rig_idx].inherit_ratio != 0:
                    # if using partial rot inherit AND inheriting from parent_idx AND ratio != 0, use that
                    # think this is good, if twistbones exist they should be children of preferred
                    f = pmx.bones[rig_idx].inherit_ratio
                elif pmx.bones[rig_idx].parent_idx == parent_idx:
                    # this should be just in case?
                    f = 1
                elif pmx.bones[rig_idx].parent_idx == start_idx:
                    # this should catch magic armtwist bones i previously created
                    f = 1
                else:
                    core.MY_PRINT_FUNC(
                        "Warning, found unusual relationship when collapsing old armtwist rig, assuming ratio=1"
                    )
                    f = 1
                transfer_bone_weights(pmx, parent_idx, rig_idx, f)
                pass
            # also collapse 'start' onto 'preferredparent' if it exists... want to transfer weight from 'arm' to 'armtwist'
            # if start == preferredparent this does nothing, no harm done
            transfer_bone_weights(pmx, parent_idx, start_idx, scalefactor=1)

            # 4. run the weight-cleanup function
            normalize_weights(pmx)

            # 5. append 3 new bones to end of bonelist
            # 	armYZ gets pos = start pos & parent = start parent
            basename_jp = pmx.bones[start_idx].name_jp
            armYZ_new_idx = len(pmx.bones)
            # armYZ = [basename_jp + yz_suffix, local_translate(basename_jp + yz_suffix)]  # name_jp,en
            # armYZ += pmx[5][start_idx][2:]					# copy the whole rest of the bone
            # armYZ[10:12] = [False, False]					# visible=false, enabled=false
            # armYZ[12:14] = [True, [armYZ_new_idx + 1]]		# tail type = tail, tail pointat = armYZend
            # armYZ[14:19] = [False, False, [], False, []]	# disable partial inherit + fixed axis
            # # local axis is copy
            # armYZ[21:25] = [False, [], False, []]			# disable ext parent + ik
            armYZ = pmxstruct.PmxBone(
                name_jp=basename_jp + yz_suffix,
                name_en=local_translate(basename_jp + yz_suffix),
                pos=pmx.bones[start_idx].pos,
                parent_idx=pmx.bones[start_idx].parent_idx,
                deform_layer=pmx.bones[start_idx].deform_layer,
                deform_after_phys=pmx.bones[start_idx].deform_after_phys,
                has_localaxis=True,
                localaxis_x=pmx.bones[start_idx].localaxis_x,
                localaxis_z=pmx.bones[start_idx].localaxis_z,
                tail_type=True,
                tail=armYZ_new_idx + 1,
                has_rotate=True,
                has_translate=True,
                has_visible=False,
                has_enabled=True,
                has_ik=False,
                inherit_rot=False,
                inherit_trans=False,
                has_fixedaxis=False,
                has_externalparent=False,
            )

            # 	armYZend gets pos = end pos & parent = armYZ
            # armYZend = [basename_jp + yz_suffix + "先", local_translate(basename_jp + yz_suffix + "先")]  # name_jp,en
            # armYZend += pmx[5][end_idx][2:]					# copy the whole rest of the bone
            # armYZend[5] = armYZ_new_idx						# parent = armYZ
            # armYZend[10:12] = [False, False]				# visible=false, enabled=false
            # armYZend[12:14] = [True, [-1]]					# tail type = tail, tail pointat = none
            # armYZend[14:19] = [False, False, [], False, []]	# disable partial inherit + fixed axis
            # # local axis is copy
            # armYZend[21:25] = [False, [], False, []]		# disable ext parent + ik
            armYZend = pmxstruct.PmxBone(
                name_jp=basename_jp + yz_suffix + "先",
                name_en=local_translate(basename_jp + yz_suffix + "先"),
                pos=pmx.bones[end_idx].pos,
                parent_idx=armYZ_new_idx,
                deform_layer=pmx.bones[end_idx].deform_layer,
                deform_after_phys=pmx.bones[end_idx].deform_after_phys,
                has_localaxis=True,
                localaxis_x=pmx.bones[end_idx].localaxis_x,
                localaxis_z=pmx.bones[end_idx].localaxis_z,
                tail_type=True,
                tail=-1,
                has_rotate=True,
                has_translate=True,
                has_visible=False,
                has_enabled=True,
                has_ik=False,
                inherit_rot=False,
                inherit_trans=False,
                has_fixedaxis=False,
                has_externalparent=False,
            )

            # # 	elbowIK gets pos = end pos & parent = end parent
            # armYZIK = [basename_jp + yz_suffix + "IK", local_translate(basename_jp + yz_suffix + "IK")]  # name_jp,en
            # armYZIK += pmx[5][end_idx][2:]					# copy the whole rest of the bone
            # armYZIK[10:12] = [False, False]					# visible=false, enabled=false
            # armYZIK[12:14] = [True, [-1]]					# tail type = tail, tail pointat = none
            # armYZIK[14:19] = [False, False, [], False, []]	# disable partial inherit + fixed axis
            # # local axis is copy
            # armYZIK[21:23] = [False, []]					# disable ext parent
            # armYZIK[23] = True								# ik=true
            # # add the ik info: [target, loops, anglelimit, [[link_idx, []], [link_idx, []]] ]
            # armYZIK[24] = [armYZ_new_idx+1, newik_loops, newik_angle, [[armYZ_new_idx, []]]]
            armYZIK = pmxstruct.PmxBone(
                name_jp=basename_jp + yz_suffix + "IK",
                name_en=local_translate(basename_jp + yz_suffix + "IK"),
                pos=pmx.bones[end_idx].pos,
                parent_idx=pmx.bones[end_idx].parent_idx,
                deform_layer=pmx.bones[end_idx].deform_layer,
                deform_after_phys=pmx.bones[end_idx].deform_after_phys,
                has_localaxis=True,
                localaxis_x=pmx.bones[end_idx].localaxis_x,
                localaxis_z=pmx.bones[end_idx].localaxis_z,
                tail_type=True,
                tail=-1,
                has_rotate=True,
                has_translate=True,
                has_visible=False,
                has_externalparent=False,
                has_enabled=True,
                inherit_rot=False,
                inherit_trans=False,
                has_fixedaxis=False,
                has_ik=True,
                ik_target_idx=armYZ_new_idx + 1,
                ik_numloops=newik_loops,
                ik_angle=newik_angle,
                ik_links=[pmxstruct.PmxBoneIkLink(idx=armYZ_new_idx)])

            # now append them to the bonelist
            pmx.bones.append(armYZ)
            pmx.bones.append(armYZend)
            pmx.bones.append(armYZIK)

            # 6. build the bezier curve
            bezier_curve = core.MyBezier(boneset[4][0],
                                         boneset[4][1],
                                         resolution=50)

            # 7. find relevant verts & determine unbounded percentile for each
            (verts, percentiles,
             centers) = calculate_percentiles(pmx, start_idx, end_idx,
                                              parent_idx)

            if moreinfo:
                core.MY_PRINT_FUNC(
                    "Blending between bones '{}'/'{}'=ZEROtwist and '{}'/'{}'=FULLtwist"
                    .format(armYZ.name_jp, armYZ.name_en,
                            pmx.bones[parent_idx].name_jp,
                            pmx.bones[parent_idx].name_en))
                core.MY_PRINT_FUNC(
                    "   Found %d potentially relevant vertices" % len(verts))

            # 8. use X or Y to choose border points, print for debugging, also scale the percentiles
            # first sort ascending by percentile value
            vert_zip = list(zip(verts, percentiles, centers))
            vert_zip.sort(key=lambda x: x[1])
            verts, percentiles, centers = zip(*vert_zip)  # unzip

            # X. highest point mode
            # "liberal" endpoints: extend as far as i can, include all good stuff even if i include some bad stuff with it
            # start at each end and work inward until i find a vert controlled by only parent_idx
            i_min_liberal = 0
            i_max_liberal = len(verts) - 1
            i_min_conserv = -1
            i_max_conserv = len(verts)
            for i_min_liberal in range(
                    0, len(verts)):  # start at head and work down,
                if pmx.verts[verts[
                        i_min_liberal]].weighttype == 0:  # if the vertex is BDEF1 type,
                    break  # then stop looking,
            p_min_liberal = percentiles[
                i_min_liberal]  # and save the percentile it found.
            for i_max_liberal in reversed(range(
                    0, len(verts))):  # start at tail and work up,
                if pmx.verts[verts[
                        i_max_liberal]].weighttype == 0:  # if the vertex is BDEF1 type,
                    break  # then stop looking,
            p_max_liberal = percentiles[
                i_max_liberal]  # and save the percentile it found.
            # Y. lowest highest point mode
            # "conservative" endpoints: define ends such that no bad stuff exists within bounds, even if i miss some good stuff
            # start in the middle and work outward until i find a vert NOT controlled by only parent_idx, then back off 1
            # where is the middle? use "bisect_left"
            middle = core.bisect_left(percentiles, 0.5)
            for i_min_conserv in reversed(
                    range(middle - 1)):  # start in middle, work toward head,
                if pmx.verts[verts[
                        i_min_conserv]].weighttype != 0:  # if the vertex is NOT BDEF1 type,
                    break  # then stop looking,
            i_min_conserv += 1  # and step back 1 to find the last vert that was good BDEF1,
            p_min_conserv = percentiles[
                i_min_conserv]  # and save the percentile it found.
            for i_max_conserv in range(
                    middle + 1,
                    len(verts)):  # start in middle, work toward tail,
                if pmx.verts[verts[
                        i_max_conserv]].weighttype != 0:  # if the vertex is NOT BDEF1 type,
                    break  # then stop looking,
            i_max_conserv -= 1  # and step back 1 to find the last vert that was good BDEF1,
            p_max_conserv = percentiles[
                i_max_conserv]  # and save the percentile it found.

            foobar = False
            if not (i_min_liberal <= i_min_conserv <= i_max_conserv <=
                    i_max_liberal):
                core.MY_PRINT_FUNC(
                    "ERROR: bounding indexes do not follow the expected relationship, results may be bad!"
                )
                foobar = True
            if foobar or moreinfo:
                core.MY_PRINT_FUNC(
                    "   Max liberal bounds:      idx = %d to %d, %% = %f to %f"
                    % (i_min_liberal, i_max_liberal, p_min_liberal,
                       p_max_liberal))
                core.MY_PRINT_FUNC(
                    "   Max conservative bounds: idx = %d to %d, %% = %f to %f"
                    % (i_min_conserv, i_max_conserv, p_min_conserv,
                       p_max_conserv))

            # IDEA: WEIGHTED BLEND! sliding scale!
            avg_factor = core.clamp(ENDPOINT_AVERAGE_FACTOR, 0.0, 1.0)
            if p_min_liberal != p_min_conserv:
                p_min = (p_min_liberal * avg_factor) + (p_min_conserv *
                                                        (1 - avg_factor))
            else:
                p_min = p_min_liberal
            if p_max_liberal != p_max_conserv:
                p_max = (p_max_liberal * avg_factor) + (p_max_conserv *
                                                        (1 - avg_factor))
            else:
                p_max = p_max_liberal
            # clamp just in case
            p_min = core.clamp(p_min, 0.0, 1.0)
            p_max = core.clamp(p_max, 0.0, 1.0)
            if moreinfo:
                i_min = core.bisect_left(percentiles, p_min)
                i_max = core.bisect_left(percentiles, p_max)
                core.MY_PRINT_FUNC(
                    "   Compromise bounds:       idx = %d to %d, %% = %f to %f"
                    % (i_min, i_max, p_min, p_max))

            # now normalize the percentiles to these endpoints
            p_len = p_max - p_min
            percentiles = [(p - p_min) / p_len for p in percentiles]

            # 9. divide weight between preferredparent (or parent) and armYZ
            vert_zip = list(zip(verts, percentiles, centers))
            num_modified, num_bleeders = divvy_weights(
                pmx=pmx,
                vert_zip=vert_zip,
                axis_limits=(pmx.bones[start_idx].pos, pmx.bones[end_idx].pos),
                bone_hasweight=parent_idx,
                bone_getsweight=armYZ_new_idx,
                bezier=bezier_curve)
            if moreinfo:
                core.MY_PRINT_FUNC(
                    "  Modified %d verts to use blending, %d are questionable 'bleeding' points"
                    % (num_modified, num_bleeders))
            pass
        pass

    # 10. run final weight-cleanup
    normalize_weights(pmx)

    # 11. write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_magictwist.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 16
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)
	
	# detect whether arm ik exists
	r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik)
	if r is None:
		r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik2)
	l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik)
	if l is None:
		l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik2)
	
	# decide whether to create or remove arm ik
	if r is None and l is None:
		# add IK branch
		core.MY_PRINT_FUNC(">>>> Adding arm IK <<<")
		# set output name
		if input_filename_pmx.lower().endswith(pmx_noik_suffix.lower()):
			output_filename = input_filename_pmx[0:-(len(pmx_noik_suffix))] + pmx_yesik_suffix
		else:
			output_filename = input_filename_pmx[0:-4] + pmx_yesik_suffix
		for side in [jp_l, jp_r]:
			# first find all 3 arm bones
			# even if i insert into the list, this will still be a valid reference i think
			bones = []
			bones: List[pmxstruct.PmxBone]
			for n in [jp_arm, jp_elbow, jp_wrist]:
				i = core.my_list_search(pmx.bones, lambda x: x.name_jp == side + n, getitem=True)
				if i is None:
					core.MY_PRINT_FUNC("ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % (side + n))
					raise RuntimeError()
				bones.append(i)
			# get parent of arm bone (shoulder bone), new bones will be inserted after this
			shoulder_idx = bones[0].parent_idx
			
			# new bones will be inserted AFTER shoulder_idx
			# newarm_idx = shoulder_idx+1
			# newelbow_idx = shoulder_idx+2
			# newwrist_idx = shoulder_idx+3
			# newik_idx = shoulder_idx+4
			
			# make copies of the 3 armchain bones
			
			# arm: parent is shoulder
			newarm = pmxstruct.PmxBone(
				name_jp=bones[0].name_jp + jp_ikchainsuffix, name_en=bones[0].name_en + jp_ikchainsuffix, 
				pos=bones[0].pos, parent_idx=shoulder_idx, deform_layer=bones[0].deform_layer, 
				deform_after_phys=bones[0].deform_after_phys,
				has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False,
				tail_usebonelink=True, tail=0,  # want arm tail to point at the elbow, can't set it until elbow is created
				inherit_rot=False, inherit_trans=False,
				has_localaxis=bones[0].has_localaxis, localaxis_x=bones[0].localaxis_x, localaxis_z=bones[0].localaxis_z,
				has_externalparent=False, has_fixedaxis=False, 
			)
			insert_single_bone(pmx, newarm, shoulder_idx + 1)
			# change existing arm to inherit rot from this
			bones[0].inherit_rot = True
			bones[0].inherit_parent_idx = shoulder_idx + 1
			bones[0].inherit_ratio = 1
			
			# elbow: parent is newarm
			newelbow = pmxstruct.PmxBone(
				name_jp=bones[1].name_jp + jp_ikchainsuffix, name_en=bones[1].name_en + jp_ikchainsuffix, 
				pos=bones[1].pos, parent_idx=shoulder_idx+1, deform_layer=bones[1].deform_layer, 
				deform_after_phys=bones[1].deform_after_phys,
				has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False,
				tail_usebonelink=True, tail=0,  # want elbow tail to point at the wrist, can't set it until wrist is created
				inherit_rot=False, inherit_trans=False,
				has_localaxis=bones[1].has_localaxis, localaxis_x=bones[1].localaxis_x, localaxis_z=bones[1].localaxis_z,
				has_externalparent=False, has_fixedaxis=False, 
			)
			insert_single_bone(pmx, newelbow, shoulder_idx + 2)
			# change existing elbow to inherit rot from this
			bones[1].inherit_rot = True
			bones[1].inherit_parent_idx = shoulder_idx + 2
			bones[1].inherit_ratio = 1
			# now that newelbow exists, change newarm tail to point to this
			newarm.tail = shoulder_idx + 2
			
			# wrist: parent is newelbow
			newwrist = pmxstruct.PmxBone(
				name_jp=bones[2].name_jp + jp_ikchainsuffix, name_en=bones[2].name_en + jp_ikchainsuffix, 
				pos=bones[2].pos, parent_idx=shoulder_idx+2, deform_layer=bones[2].deform_layer, 
				deform_after_phys=bones[2].deform_after_phys,
				has_rotate=True, has_translate=False, has_visible=False, has_enabled=True, has_ik=False,
				tail_usebonelink=True, tail=-1,  # newwrist has no tail
				inherit_rot=False, inherit_trans=False,
				has_localaxis=bones[2].has_localaxis, localaxis_x=bones[2].localaxis_x, localaxis_z=bones[2].localaxis_z,
				has_externalparent=False, has_fixedaxis=False, 
			)
			insert_single_bone(pmx, newwrist, shoulder_idx + 3)
			# now that newwrist exists, change newelbow tail to point to this
			newelbow.tail = shoulder_idx + 3
			
			# copy the wrist to make the IK bone
			en_suffix = "_L" if side == jp_l else "_R"
			# get index of "upperbody" to use as parent of hand IK bone
			ikpar = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_upperbody)
			if ikpar is None:
				core.MY_PRINT_FUNC("ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK" % jp_upperbody)
				raise RuntimeError()
			
			# newik = [side + jp_newik, en_newik + en_suffix] + bones[2][2:5] + [ikpar]  # new names, copy pos, new par
			# newik += bones[2][6:8] + [1, 1, 1, 1]  + [0, [0,1,0]] # copy deform layer, rot/trans/vis/en, tail type
			# newik += [0, 0, [], 0, [], 0, [], 0, []]  # no inherit, no fixed axis, no local axis, no ext parent, yes IK
			# # add the ik info: [is_ik, [target, loops, anglelimit, [[link_idx, []]], [link_idx, []]]] ] ]
			# newik += [1, [shoulder_idx+3, newik_loops, newik_angle, [[shoulder_idx+2,[]],[shoulder_idx+1,[]]] ] ]
			newik = pmxstruct.PmxBone(
				name_jp=side + jp_newik, name_en=en_newik + en_suffix, pos=bones[2].pos,
				parent_idx=ikpar, deform_layer=bones[2].deform_layer, deform_after_phys=bones[2].deform_after_phys,
				has_rotate=True, has_translate=True, has_visible=True, has_enabled=True,
				tail_usebonelink=False, tail=[0,1,0], inherit_rot=False, inherit_trans=False,
				has_fixedaxis=False, has_localaxis=False, has_externalparent=False, has_ik=True,
				ik_target_idx=shoulder_idx+3, ik_numloops=newik_loops, ik_angle=newik_angle,
				ik_links=[pmxstruct.PmxBoneIkLink(idx=shoulder_idx+2), pmxstruct.PmxBoneIkLink(idx=shoulder_idx+1)]
			)
			insert_single_bone(pmx, newik, shoulder_idx + 4)
			
			# then add to dispframe
			# first, does the frame already exist?
			f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik, getitem=True)
			newframeitem = pmxstruct.PmxFrameItem(is_morph=False, idx=shoulder_idx + 4)
			if f is None:
				# need to create the new dispframe! easy
				newframe = pmxstruct.PmxFrame(name_jp=jp_newik, name_en=en_newik, is_special=False, items=[newframeitem])
				pmx.frames.append(newframe)
			else:
				# frame already exists, also easy
				f.items.append(newframeitem)
	else:
		# remove IK branch
		core.MY_PRINT_FUNC(">>>> Removing arm IK <<<")
		# set output name
		if input_filename_pmx.lower().endswith(pmx_yesik_suffix.lower()):
			output_filename = input_filename_pmx[0:-(len(pmx_yesik_suffix))] + pmx_noik_suffix
		else:
			output_filename = input_filename_pmx[0:-4] + pmx_noik_suffix
		# identify all bones in ik chain of hand ik bones
		bone_dellist = []
		for b in [r, l]:
			bone_dellist.append(b) # this IK bone
			bone_dellist.append(pmx.bones[b].ik_target_idx) # the target of the bone
			for v in pmx.bones[b].ik_links:
				bone_dellist.append(v.idx) # each link along the bone
		bone_dellist.sort()
		# do the actual delete & shift
		delete_multiple_bones(pmx, bone_dellist)
		
		# delete dispframe for hand ik
		# first, does the frame already exist?
		f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik)
		if f is not None:
			# frame already exists, delete it
			pmx.frames.pop(f)
		
		pass
	
	# write out
	output_filename = core.get_unused_file_name(output_filename)
	pmxlib.write_pmx(output_filename, pmx, moreinfo=moreinfo)
	core.MY_PRINT_FUNC("Done!")
	return None
Esempio n. 17
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
Esempio n. 18
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)

    ##################################
    # user flow:
    # ask for the helper bone (to be merged)
    # ask for the destination bone (merged onto)
    # try to infer proper merge factor, if it cannot infer then prompt user
    # then write out to file
    ##################################

    dest_idx = 0
    while True:
        # any input is considered valid
        s = core.MY_GENERAL_INPUT_FUNC(lambda x: True, [
            "Please specify the DESTINATION bone that weights will be transferred to.",
            "Enter bone #, JP name, or EN name (names are case sensitive).",
            "Empty input will quit the script."
        ])
        # if empty, leave & do nothing
        if s == "":
            dest_idx = -1
            break
        # then get the bone index from this
        # search JP names first
        dest_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == s)
        if dest_idx is not None: break  # did i find a match?
        # search EN names next
        dest_idx = core.my_list_search(pmx.bones, lambda x: x.name_en == s)
        if dest_idx is not None: break  # did i find a match?
        # try to cast to int next
        try:
            dest_idx = int(s)
            if 0 <= dest_idx < len(pmx.bones):
                break  # is this within the proper bounds?
            else:
                core.MY_PRINT_FUNC("valid bone indexes are 0-%d" %
                                   (len(pmx.bones) - 1))
        except ValueError:
            pass
        core.MY_PRINT_FUNC("unable to find matching bone for name '%s'" % s)

    if dest_idx == -1:
        core.MY_PRINT_FUNC("quitting")
        return None

    dest_tag = "bone #{} JP='{}' / EN='{}'".format(dest_idx,
                                                   pmx.bones[dest_idx].name_jp,
                                                   pmx.bones[dest_idx].name_jp)
    source_idx = 0
    while True:
        # any input is considered valid
        s = core.MY_GENERAL_INPUT_FUNC(lambda x: True, [
            "Please specify the SOURCE bone that will be merged onto %s." %
            dest_tag,
            "Enter bone #, JP name, or EN name (names are case sensitive).",
            "Empty input will quit the script."
        ])
        # if empty, leave & do nothing
        if s == "":
            source_idx = -1
            break
        # then get the morph index from this
        # search JP names first
        source_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == s)
        if source_idx is not None: break  # did i find a match?
        # search EN names next
        source_idx = core.my_list_search(pmx.bones, lambda x: x.name_en == s)
        if source_idx is not None: break  # did i find a match?
        # try to cast to int next
        try:
            source_idx = int(s)
            if 0 <= source_idx < len(pmx.bones):
                break  # is this within the proper bounds?
            else:
                core.MY_PRINT_FUNC("valid bone indexes are 0-%d" %
                                   (len(pmx.bones) - 1))
        except ValueError:
            pass
        core.MY_PRINT_FUNC("unable to find matching bone for name '%s'" % s)

    if source_idx == -1:
        core.MY_PRINT_FUNC("quitting")
        return None

    # print to confirm
    core.MY_PRINT_FUNC(
        "Merging bone #{} JP='{}' / EN='{}' ===> bone #{} JP='{}' / EN='{}'".
        format(source_idx, pmx.bones[source_idx].name_jp,
               pmx.bones[source_idx].name_en, dest_idx,
               pmx.bones[dest_idx].name_jp, pmx.bones[dest_idx].name_en))
    # now try to infer the merge factor

    f = 0.0
    if pmx.bones[source_idx].inherit_rot and pmx.bones[
            source_idx].inherit_parent_idx == dest_idx and pmx.bones[
                source_idx].inherit_ratio != 0:
        # if using partial rot inherit AND inheriting from dest_idx AND ratio != 0, use that
        # think this is good, if twistbones exist they should be children of preferred
        f = pmx.bones[source_idx].inherit_ratio
    elif pmx.bones[source_idx].parent_idx == dest_idx:
        # if they have a direct parent-child relationship, then factor is 1
        f = 1
    else:
        # otherwise, prompt for the factor
        factor_str = core.MY_GENERAL_INPUT_FUNC(
            is_float,
            "Unable to infer relationship, please specify a merge factor:")
        if factor_str == "":
            core.MY_PRINT_FUNC("quitting")
            return None
        f = float(factor_str)

    # do the actual transfer
    transfer_bone_weights(pmx, dest_idx, source_idx, f)

    # run the weight-cleanup function
    dummy = normalize_weights(pmx)

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_weightmerge.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 19
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)

    # detect whether arm ik exists
    r = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_r + jp_newik)
    if r is None:
        r = core.my_list_search(pmx.bones,
                                lambda x: x.name_jp == jp_r + jp_newik2)
    l = core.my_list_search(pmx.bones, lambda x: x.name_jp == jp_l + jp_newik)
    if l is None:
        l = core.my_list_search(pmx.bones,
                                lambda x: x.name_jp == jp_l + jp_newik2)

    # decide whether to create or remove arm ik
    if r is None and l is None:
        # add IK branch
        core.MY_PRINT_FUNC(">>>> Adding arm IK <<<")
        # set output name
        if input_filename_pmx.lower().endswith(pmx_noik_suffix.lower()):
            output_filename = input_filename_pmx[0:-(
                len(pmx_noik_suffix))] + pmx_yesik_suffix
        else:
            output_filename = input_filename_pmx[0:-4] + pmx_yesik_suffix
        for side in [jp_l, jp_r]:
            # first find all 3 arm bones
            # even if i insert into the list, this will still be a valid reference i think
            bones = []
            bones: List[pmxstruct.PmxBone]
            for n in [jp_arm, jp_elbow, jp_wrist]:
                i = core.my_list_search(pmx.bones,
                                        lambda x: x.name_jp == side + n,
                                        getitem=True)
                if i is None:
                    core.MY_PRINT_FUNC(
                        "ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK"
                        % (side + n))
                    raise RuntimeError()
                bones.append(i)
            # get parent of arm bone
            shoulder_idx = bones[0].parent_idx

            # then do the "remapping" on all existing bone references, to make space for inserting 4 bones
            # don't delete any bones, just remap them
            bone_shiftmap = ([shoulder_idx + 1], [-4])
            apply_bone_remapping(pmx, [], bone_shiftmap)
            # new bones will be inserted AFTER shoulder_idx
            # newarm_idx = shoulder_idx+1
            # newelbow_idx = shoulder_idx+2
            # newwrist_idx = shoulder_idx+3
            # newik_idx = shoulder_idx+4

            # make copies of the 3 armchain bones
            for i, b in enumerate(bones):
                b: pmxstruct.PmxBone

                # newarm = b[0:5] + [shoulder_idx + i] + b[6:8]  # copy names/pos, parent, copy deform layer
                # newarm += [1, 0, 0, 0]  # rotateable, not translateable, not visible, not enabled(?)
                # newarm += [1, [shoulder_idx + 2 + i], 0, 0, [], 0, []]  # tail type, no inherit, no fixed axis,
                # newarm += b[19:21] + [0, [], 0, []]  # copy local axis, no ext parent, no ik
                # newarm[0] += jp_ikchainsuffix  # add suffix to jp name
                # newarm[1] += jp_ikchainsuffix  # add suffix to en name
                newarm = pmxstruct.PmxBone(
                    name_jp=b.name_jp + jp_ikchainsuffix,
                    name_en=b.name_en + jp_ikchainsuffix,
                    pos=b.pos,
                    parent_idx=b.parent_idx,
                    deform_layer=b.deform_layer,
                    deform_after_phys=b.deform_after_phys,
                    has_rotate=True,
                    has_translate=False,
                    has_visible=False,
                    has_enabled=True,
                    tail_type=True,
                    tail=shoulder_idx + 2 + i,
                    inherit_rot=False,
                    inherit_trans=False,
                    has_fixedaxis=False,
                    has_localaxis=b.has_localaxis,
                    localaxis_x=b.localaxis_x,
                    localaxis_z=b.localaxis_z,
                    has_externalparent=False,
                    has_ik=False,
                )
                pmx.bones.insert(shoulder_idx + 1 + i, newarm)
                # then change the existing arm/elbow (not the wrist) to inherit rot from them
                if i != 2:
                    b.inherit_rot = True
                    b.inherit_parent_idx = shoulder_idx + 1 + i
                    b.inherit_ratio = 1

            # copy the wrist to make the IK bone
            en_suffix = "_L" if side == jp_l else "_R"
            # get index of "upperbody" to use as parent of hand IK bone
            ikpar = core.my_list_search(pmx.bones,
                                        lambda x: x.name_jp == jp_upperbody)
            if ikpar is None:
                core.MY_PRINT_FUNC(
                    "ERROR1: semistandard bone '%s' is missing from the model, unable to create attached arm IK"
                    % jp_upperbody)
                raise RuntimeError()

            # newik = [side + jp_newik, en_newik + en_suffix] + bones[2][2:5] + [ikpar]  # new names, copy pos, new par
            # newik += bones[2][6:8] + [1, 1, 1, 1]  + [0, [0,1,0]] # copy deform layer, rot/trans/vis/en, tail type
            # newik += [0, 0, [], 0, [], 0, [], 0, []]  # no inherit, no fixed axis, no local axis, no ext parent, yes IK
            # # add the ik info: [is_ik, [target, loops, anglelimit, [[link_idx, []]], [link_idx, []]]] ] ]
            # newik += [1, [shoulder_idx+3, newik_loops, newik_angle, [[shoulder_idx+2,[]],[shoulder_idx+1,[]]] ] ]
            newik = pmxstruct.PmxBone(
                name_jp=side + jp_newik,
                name_en=en_newik + en_suffix,
                pos=bones[2].pos,
                parent_idx=ikpar,
                deform_layer=bones[2].deform_layer,
                deform_after_phys=bones[2].deform_after_phys,
                has_rotate=True,
                has_translate=True,
                has_visible=True,
                has_enabled=True,
                tail_type=False,
                tail=[0, 1, 0],
                inherit_rot=False,
                inherit_trans=False,
                has_fixedaxis=False,
                has_localaxis=False,
                has_externalparent=False,
                has_ik=True,
                ik_target_idx=shoulder_idx + 3,
                ik_numloops=newik_loops,
                ik_angle=newik_angle,
                ik_links=[
                    pmxstruct.PmxBoneIkLink(idx=shoulder_idx + 2),
                    pmxstruct.PmxBoneIkLink(idx=shoulder_idx + 1)
                ])
            pmx.bones.insert(shoulder_idx + 4, newik)

            # then add to dispframe
            # first, does the frame already exist?
            f = core.my_list_search(pmx.frames,
                                    lambda x: x.name_jp == jp_newik,
                                    getitem=True)
            if f is None:
                # need to create the new dispframe! easy
                newframe = pmxstruct.PmxFrame(name_jp=jp_newik,
                                              name_en=en_newik,
                                              is_special=False,
                                              items=[[0, shoulder_idx + 4]])
                pmx.frames.append(newframe)
            else:
                # frame already exists, also easy
                f.items.append([0, shoulder_idx + 4])
    else:
        # remove IK branch
        core.MY_PRINT_FUNC(">>>> Removing arm IK <<<")
        # set output name
        if input_filename_pmx.lower().endswith(pmx_yesik_suffix.lower()):
            output_filename = input_filename_pmx[0:-(
                len(pmx_yesik_suffix))] + pmx_noik_suffix
        else:
            output_filename = input_filename_pmx[0:-4] + pmx_noik_suffix
        # identify all bones in ik chain of hand ik bones
        bone_dellist = []
        for b in [r, l]:
            bone_dellist.append(b)  # this IK bone
            bone_dellist.append(
                pmx.bones[b].ik_target_idx)  # the target of the bone
            for v in pmx.bones[b].ik_links:
                bone_dellist.append(v.idx)  # each link along the bone
        bone_dellist.sort()
        # build the remap thing
        bone_shiftmap = delme_list_to_rangemap(bone_dellist)
        # do the actual delete & shift
        apply_bone_remapping(pmx, bone_dellist, bone_shiftmap)

        # delete dispframe for hand ik
        # first, does the frame already exist?
        f = core.my_list_search(pmx.frames, lambda x: x.name_jp == jp_newik)
        if f is not None:
            # frame already exists, delete it
            pmx.frames.pop(f)

        pass

    # write out
    output_filename = core.get_unused_file_name(output_filename)
    pmxlib.write_pmx(output_filename, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
def main(moreinfo=True):
	# the goal: extract rotation around the "arm" bone local X? axis and transfer it to rotation around the "armtwist" bone local axis
	
	# prompt PMX name
	core.MY_PRINT_FUNC("Please enter name of PMX input file:")
	input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
	pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)
	core.MY_PRINT_FUNC("")
	# get bones
	realbones = pmx.bones
	
	twistbone_axes = []
	# then, grab the "twist" bones & save their fixed-rotate axes, if they have them
	# fallback plan: find the arm-to-elbow and elbow-to-wrist unit vectors and use those
	for i in range(len(jp_twistbones)):
		r = core.my_list_search(realbones, lambda x: x.name_jp == jp_twistbones[i], getitem=True)
		if r is None:
			core.MY_PRINT_FUNC("ERROR1: twist bone '{}'({}) cannot be found model, unable to continue. Ensure they use the correct semistandard names, or edit the script to change the JP names it is looking for.".format(jp_twistbones[i], eng_twistbones[i]))
			raise RuntimeError()
		if r.has_fixedaxis:
			# this bone DOES have fixed-axis enabled! use the unit vector in r[18]
			twistbone_axes.append(r.fixedaxis)
		else:
			# i can infer local axis by angle from arm-to-elbow or elbow-to-wrist
			start = core.my_list_search(realbones, lambda x: x.name_jp == jp_sourcebones[i], getitem=True)
			if start is None:
				core.MY_PRINT_FUNC("ERROR2: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_sourcebones[i])
				raise RuntimeError()
			end = core.my_list_search(realbones, lambda x: x.name_jp == jp_pointat_bones[i], getitem=True)
			if end is None:
				core.MY_PRINT_FUNC("ERROR3: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_pointat_bones[i])
				raise RuntimeError()
			start_pos = start.pos
			end_pos = end.pos
			# now have both startpoint and endpoint! find the delta!
			delta = [b - a for a,b in zip(start_pos, end_pos)]
			# normalize to length of 1
			length = core.my_euclidian_distance(delta)
			unit = [t / length for t in delta]
			twistbone_axes.append(unit)
	
	# done extracting axes limits from bone CSV, in list "twistbone_axes"
	core.MY_PRINT_FUNC("...done extracting axis limits from PMX...")
	
	
	###################################################################################
	# prompt VMD file name
	core.MY_PRINT_FUNC("Please enter name of VMD dance input file:")
	input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd")
	
	# next, read/use/prune the dance vmd
	nicelist_in = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo)
	
	# sort boneframes into individual lists: one for each [Larm + Lelbow + Rarm + Relbow] and remove them from the master boneframelist
	# frames for all other bones stay in the master boneframelist
	all_sourcebone_frames = []
	for sourcebone in jp_sourcebones:
		# partition & writeback
		temp, nicelist_in.boneframes = core.my_list_partition(nicelist_in.boneframes, lambda x: x.name == sourcebone)
		# all frames for "sourcebone" get their own sublist here
		all_sourcebone_frames.append(temp)
	
	# verify that there is actually arm/elbow frames to process
	sourcenumframes = sum([len(x) for x in all_sourcebone_frames])
	if sourcenumframes == 0:
		core.MY_PRINT_FUNC("No arm/elbow bone frames are found in the VMD, nothing for me to do!")
		core.MY_PRINT_FUNC("Aborting: no files were changed")
		return None
	else:
		core.MY_PRINT_FUNC("...source contains " + str(sourcenumframes) + " arm/elbow bone frames to decompose...")
	
	if USE_OVERKEY_BANDAID:
		# to fix the path that the arms take during interpolation we need to overkey the frames
		# i.e. create intermediate frames that they should have been passing through already, to FORCE it to take the right path
		# i'm replacing the interpolation curves with actual frames
		for sublist in all_sourcebone_frames:
			newframelist = []
			sublist.sort(key=lambda x: x.f) # ensure they are sorted by frame number
			# for each frame
			for i in range(1, len(sublist)):
				this = sublist[i]
				prev = sublist[i-1]
				# use interpolation curve i to interpolate from i-1 to i
				# first: do i need to do anything or are they already close on the timeline?
				thisframenum = this.f
				prevframenum = prev.f
				if (thisframenum - prevframenum) <= OVERKEY_FRAME_SPACING:
					continue
				# if they are far enough apart that i need to do something,
				thisframequat = core.euler_to_quaternion(this.rot)
				prevframequat = core.euler_to_quaternion(prev.rot)
				# 3, 7, 11, 15 = r_ax, r_ay, r_bx, r_by
				bez = core.MyBezier((this.interp[3], this.interp[7]), (this.interp[11], this.interp[15]), resolution=50)
				# create new frames at these frame numbers, spacing is OVERKEY_FRAME_SPACING
				for interp_framenum in range(prevframenum + OVERKEY_FRAME_SPACING, thisframenum, OVERKEY_FRAME_SPACING):
					# calculate the x time percentage from prev frame to this frame
					x = (interp_framenum - prevframenum) / (thisframenum - prevframenum)
					# apply the interpolation curve to translate X to Y
					y = bez.approximate(x)
					# interpolate from prev to this by amount Y
					interp_quat = core.my_slerp(prevframequat, thisframequat, y)
					# begin building the new frame
					newframe = vmdstruct.VmdBoneFrame(
						name=this.name,  # same name
						f=interp_framenum,  # overwrite frame num
						pos=list(this.pos),  # same pos (but make a copy)
						rot=list(core.quaternion_to_euler(interp_quat)),  # overwrite euler angles
						phys_off=this.phys_off,  # same phys_off
						interp=list(core.bone_interpolation_default_linear)  # overwrite interpolation
					)
					newframelist.append(newframe)
				# overwrite thisframe interp curve with default too
				this.interp = list(core.bone_interpolation_default_linear) # overwrite custom interpolation
			# concat the new frames onto the existing frames for this sublist
			sublist += newframelist
			
	# re-count the number of frames for printing purposes
	totalnumframes = sum([len(x) for x in all_sourcebone_frames])
	overkeyframes = totalnumframes - sourcenumframes
	if overkeyframes != 0:
		core.MY_PRINT_FUNC("...overkeying added " + str(overkeyframes) + " arm/elbow bone frames...")
	core.MY_PRINT_FUNC("...beginning decomposition of " + str(totalnumframes) + " arm/elbow bone frames...")
	
	# now i am completely done reading the VMD file and parsing its data! everything has been distilled down to:
	# all_sourcebone_frames = [Larm, Lelbow, Rarm, Relbow] plus nicelist_in[1]
	
	###################################################################################
	# begin the actual calculations
	
	# output array
	new_twistbone_frames = []
	# progress tracker
	curr_progress = 0
	
	# for each sourcebone & corresponding twistbone,
	for (twistbone, axis_orig, sourcebone_frames) in zip(jp_twistbones, twistbone_axes, all_sourcebone_frames):
		# for each frame of the sourcebone,
		for frame in sourcebone_frames:
			# XYZrot = 567 euler
			quat_in = core.euler_to_quaternion(frame.rot)
			axis = list(axis_orig)	# make a copy to be safe
			
			# "swing twist decomposition"
			# swing = "local" x rotation and nothing else
			# swing = sourcebone, twist = twistbone
			(swing, twist) = swing_twist_decompose(quat_in, axis)
			
			# modify "frame" in-place
			# only modify the XYZrot to use new values
			new_sourcebone_euler = core.quaternion_to_euler(swing)
			frame.rot = list(new_sourcebone_euler)
			
			# create & store new twistbone frame
			# name=twistbone, framenum=copy, XYZpos=copy, XYZrot=new, phys=copy, interp16=copy
			new_twistbone_euler = core.quaternion_to_euler(twist)
			newframe = vmdstruct.VmdBoneFrame(
				name=twistbone,
				f=frame.f,
				pos=list(frame.pos),
				rot=list(new_twistbone_euler),
				phys_off=frame.phys_off,
				interp=list(frame.interp)
			)
			new_twistbone_frames.append(newframe)
			# print progress updates
			curr_progress += 1
			core.print_progress_oneline(curr_progress / totalnumframes)
	
	
	######################################################################
	# done with calculations!
	core.MY_PRINT_FUNC("...done with decomposition, now reassembling output...")
	# attach the list of newly created boneframes, modify the original input
	for sublist in all_sourcebone_frames:
		nicelist_in.boneframes += sublist
	nicelist_in.boneframes += new_twistbone_frames
	
	core.MY_PRINT_FUNC("")
	# write out the VMD
	output_filename_vmd = "%s_twistbones_for_%s.vmd" % \
						   (input_filename_vmd[0:-4], core.get_clean_basename(input_filename_pmx))
	output_filename_vmd = core.get_unused_file_name(output_filename_vmd)
	vmdlib.write_vmd(output_filename_vmd, nicelist_in, moreinfo=moreinfo)
	
	core.MY_PRINT_FUNC("Done!")
	return None
Esempio n. 21
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)

    core.MY_PRINT_FUNC("")
    # valid input is any string that can matched aginst a morph idx
    s = core.MY_GENERAL_INPUT_FUNC(
        lambda x: morph_scale.get_idx_in_pmxsublist(x, pmx.morphs) is not None,
        [
            "Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).",
            "Empty input will quit the script."
        ])
    # do it again, cuz the lambda only returns true/false
    target_index = morph_scale.get_idx_in_pmxsublist(s, pmx.morphs)

    # when given empty text, done!
    if target_index == -1 or target_index is None:
        core.MY_PRINT_FUNC("quitting")
        return None

    morphtype = pmx.morphs[target_index].morphtype
    # 1=vert
    # 3=UV
    # 8=material
    core.MY_PRINT_FUNC("Found {} morph #{}: '{}' / '{}'".format(
        morphtype, target_index, pmx.morphs[target_index].name_jp,
        pmx.morphs[target_index].name_en))

    if morphtype == pmxstruct.MorphType.VERTEX:  # vertex
        # for each item in this morph:
        item: pmxstruct.PmxMorphItemVertex  # type annotation for pycharm
        for d, item in enumerate(pmx.morphs[target_index].items):
            # apply the offset
            pmx.verts[item.vert_idx].pos[0] += item.move[0]
            pmx.verts[item.vert_idx].pos[1] += item.move[1]
            pmx.verts[item.vert_idx].pos[2] += item.move[2]
            # invert the morph
        morph_scale.morph_scale(pmx.morphs[target_index], -1)
    elif morphtype == pmxstruct.MorphType.UV:  # UV
        item: pmxstruct.PmxMorphItemUV  # type annotation for pycharm
        for d, item in enumerate(pmx.morphs[target_index].items):
            # (vert_idx, A, B, C, D)
            # apply the offset
            pmx.verts[item.vert_idx].uv[0] += item.move[0]
            pmx.verts[item.vert_idx].uv[1] += item.move[1]
            # invert the morph
        morph_scale.morph_scale(pmx.morphs[target_index], -1)
    elif morphtype in (pmxstruct.MorphType.UV_EXT1,
                       pmxstruct.MorphType.UV_EXT2,
                       pmxstruct.MorphType.UV_EXT3,
                       pmxstruct.MorphType.UV_EXT4):  # UV1 UV2 UV3 UV4
        whichuv = morphtype.value - pmxstruct.MorphType.UV_EXT1.value
        item: pmxstruct.PmxMorphItemUV  # type annotation for pycharm
        for d, item in enumerate(pmx.morphs[target_index].items):
            # apply the offset
            pmx.verts[item.vert_idx].addl_vec4s[whichuv][0] += item.move[0]
            pmx.verts[item.vert_idx].addl_vec4s[whichuv][1] += item.move[1]
            pmx.verts[item.vert_idx].addl_vec4s[whichuv][2] += item.move[2]
            pmx.verts[item.vert_idx].addl_vec4s[whichuv][3] += item.move[3]
            # invert the morph
        morph_scale.morph_scale(pmx.morphs[target_index], -1)
    elif morphtype == pmxstruct.MorphType.MATERIAL:  # material
        core.MY_PRINT_FUNC("WIP")
        # todo
        # to invert a material morph means inverting the material's visible/notvisible state as well as flipping the morph
        # hide morph add -> show morph add
        # hide morph mult -> show morph add
        # show morph add -> hide morph mult
        core.MY_PRINT_FUNC("quitting")
        return None
    else:
        core.MY_PRINT_FUNC("Unhandled morph type")
        core.MY_PRINT_FUNC("quitting")
        return None

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + ("_%dinv.pmx" %
                                                      target_index)
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
def main(moreinfo=True):
    # prompt PMX name
    core.MY_PRINT_FUNC("Please enter name of PMX input file:")
    input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
    pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo)

    # to shift the model by a set amount:
    # first, ask user for X Y Z

    # create the prompt popup
    scale_str = core.MY_GENERAL_INPUT_FUNC(
        lambda x: (model_shift.is_3float(x) is not None), [
            "Enter the X,Y,Z amount to scale this model by:",
            "Three decimal values separated by commas.",
            "Empty input will quit the script."
        ])

    # if empty, quit
    if scale_str == "":
        core.MY_PRINT_FUNC("quitting")
        return None
    # use the same func to convert the input string
    scale = model_shift.is_3float(scale_str)

    uniform_scale = (scale[0] == scale[1] == scale[2])
    if not uniform_scale:
        core.MY_PRINT_FUNC(
            "Warning: when scaling by non-uniform amounts, rigidbody sizes will not be modified"
        )

    ####################
    # what does it mean to scale the entire model?
    # scale vertex position, sdef params
    # ? scale vertex normal vectors, then normalize? need to convince myself of this interaction
    # scale bone position, tail offset
    # scale fixedaxis and localaxis vectors, then normalize
    # scale vert morph, bone morph
    # scale rigid pos, size
    # scale joint pos, movelimits

    for v in pmx.verts:
        # vertex position
        for i in range(3):
            v.pos[i] *= scale[i]
        # vertex normal
        for i in range(3):
            if scale[i] != 0:
                v.norm[i] /= scale[i]
            else:
                v.norm[i] = 100000
        # then re-normalize the normal vector
        v.norm = core.normalize_distance(v.norm)
        # c, r0, r1 params of every SDEF vertex
        # these correspond to real positions in 3d space so they need to be modified
        if v.weighttype == pmxstruct.WeightMode.SDEF:
            for param in v.weight_sdef:
                for i in range(3):
                    param[i] *= scale[i]

    for b in pmx.bones:
        # bone position
        for i in range(3):
            b.pos[i] *= scale[i]
        # bone tail if using offset mode
        if not b.tail_usebonelink:
            for i in range(3):
                b.tail[i] *= scale[i]
        # scale fixedaxis and localaxis vectors, then normalize
        if b.has_fixedaxis:
            for i in range(3):
                b.fixedaxis[i] *= scale[i]
            # then re-normalize
            b.fixedaxis = core.normalize_distance(b.fixedaxis)
        # scale fixedaxis and localaxis vectors, then normalize
        if b.has_localaxis:
            for i in range(3):
                b.localaxis_x[i] *= scale[i]
            for i in range(3):
                b.localaxis_z[i] *= scale[i]
            # then re-normalize
            b.localaxis_x = core.normalize_distance(b.localaxis_x)
            b.localaxis_z = core.normalize_distance(b.localaxis_z)

    for m in pmx.morphs:
        # vertex morph and bone morph (only translate, not rotate)
        if m.morphtype in (pmxstruct.MorphType.VERTEX,
                           pmxstruct.MorphType.BONE):
            morph_scale.morph_scale(m, scale, bone_mode=1)

    for rb in pmx.rigidbodies:
        # rigid body position
        for i in range(3):
            rb.pos[i] *= scale[i]
        # rigid body size
        # NOTE: rigid body size is a special conundrum
        # spheres have only one dimension, capsules have two, and only boxes have 3
        # what's the "right" way to scale a sphere by 1,5,1? there isn't a right way!
        # boxes and capsules can be rotated and stuff so their axes dont line up with world axes, too
        # is it at least possible to rotate bodies so they are still aligned with their bones?
        # eh, why even bother with any of that. 95% of the time full-model scale will be uniform scaling.
        # only scale the rigidbody size if doing uniform scaling: that is guaranteed to be safe!
        if uniform_scale:
            for i in range(3):
                rb.size[i] *= scale[i]

    for j in pmx.joints:
        # joint position
        for i in range(3):
            j.pos[i] *= scale[i]
        # joint min slip
        for i in range(3):
            j.movemin[i] *= scale[i]
        # joint max slip
        for i in range(3):
            j.movemax[i] *= scale[i]

    # that's it? that's it!

    # write out
    output_filename_pmx = input_filename_pmx[0:-4] + "_scale.pmx"
    output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
    pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo)
    core.MY_PRINT_FUNC("Done!")
    return None
Esempio n. 23
0
def main(moreinfo=False):
    core.MY_PRINT_FUNC("Please enter name of PMX model file:")
    input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")

    # texture sorting plan:
    # 1. get startpath = basepath of input PMX
    # 2. get lists of relevant files
    # 	2a. get list of ALL files within the tree, relative to startpath
    # 	2b. extract top-level 'neighbor' pmx files from all-set
    # 	2c. remove files i intend to ignore (filter by file ext or containing folder)
    # 3. ask about modifying neighbor PMX
    # 4. read PMX: either target or target+all neighbor
    # 5. "categorize files & normalize usages within PMX", NEW FUNC!!!
    # 	inputs: list of PMX obj, list of relevant files
    # 	outputs: list of structs that bundle all relevant info about the file (replace 2 structs currently used)
    # 	for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk
    # now have all files, know their states!
    # 6. ask for "aggression level" to control how files will be moved
    # 7. determine new names for files
    # 	this is the big one, slightly different logic for different categories
    # 8. print proposed names & other findings
    # 	for unused files under a folder, combine & replace with ***
    # 9. ask for confirmation
    # 10. zip backup (NEW FUNC!)
    # 11. apply renaming, NEW FUNC!
    # 	first try to rename all files
    # 		could plausibly fail, if so, set to-name to None/blank
    # 	then, in the PMXs, rename all files that didn't fail

    # absolute path to directory holding the pmx
    input_filename_pmx_abs = os.path.normpath(
        os.path.abspath(input_filename_pmx))
    startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs)

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files
    relative_all_exist_files = walk_filetree_from_root(startpath)
    core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files))
    # now fill "neighbor_pmx" by finding files without path separator that end in PMX
    # these are relative paths tho
    neighbor_pmx = [
        f for f in relative_all_exist_files
        if (f.lower().endswith(".pmx")) and (
            os.path.sep not in f) and f != input_filename_pmx_rel
    ]

    relevant_exist_files = []
    for f in relative_all_exist_files:
        # ignore all files I expect to find alongside a PMX and don't want to touch or move
        if f.lower().endswith(IGNORE_FILETYPES): continue
        # ignore any files living below/inside 'special' folders like "fx/"
        if match_folder_anylevel(f, IGNORE_FOLDERS, toponly=False): continue
        # create the list of files we know exist and we know we care about
        relevant_exist_files.append(f)

    core.MY_PRINT_FUNC("RELEVANT EXISTING FILES:", len(relevant_exist_files))

    core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx))

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # now ask if I care about the neighbors and read the PMXes into memory

    pmx_filenames = [input_filename_pmx_rel]

    if neighbor_pmx:
        core.MY_PRINT_FUNC("")
        info = [
            "Detected %d top-level neighboring PMX files, these probably share the same filebase as the target."
            % len(neighbor_pmx),
            "If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.",
            "Do you want to process all neighbors in addition to the target? (highly recommended)",
            "1 = Yes, 2 = No"
        ]
        r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
        if r == 1:
            core.MY_PRINT_FUNC("Processing target + all neighbor files")
            # append neighbor PMX files onto the list of files to be processed
            pmx_filenames += neighbor_pmx
        else:
            core.MY_PRINT_FUNC(
                "WARNING: Processing only target, ignoring %d neighbor PMX files"
                % len(neighbor_pmx))
    # now read all the PMX objects & store in dict alongside the relative name
    # dictionary where keys are filename and values are resulting pmx objects
    all_pmx_obj = {}
    for this_pmx_name in pmx_filenames:
        this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name),
                                       moreinfo=moreinfo)
        all_pmx_obj[this_pmx_name] = this_pmx_obj

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # 	for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk
    #	also fill out how much and how each file is used, and unify dupes between files, all that good stuff

    filerecord_list = categorize_files(all_pmx_obj, relevant_exist_files,
                                       moreinfo)

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # now check which files are used/unused/dont exist

    # break this into used/notused/notexist lists for simplicity sake
    # all -> used + notused
    # used -> used_exist + used_notexist
    # notused -> notused_img + notused_notimg
    used, notused = core.my_list_partition(filerecord_list,
                                           lambda q: q.numused != 0)
    used_exist, used_notexist = core.my_list_partition(used,
                                                       lambda q: q.exists)
    notused_img, notused_notimg = core.my_list_partition(
        notused, lambda q: q.name.lower().endswith(IMG_EXT))

    core.MY_PRINT_FUNC("PMX TEXTURE SOURCES:", len(used))
    if moreinfo:
        for x in used:
            core.MY_PRINT_FUNC("  " + str(x))

    # now:
    # all duplicates have been resolved within PMX, including modifying the PMX
    # all duplicates have been resolved across PMXes
    # all file exist/notexist status is known
    # all file used/notused status is known (via numused), or used_pmx
    # all ways a file is used is known

    move_toplevel_unused_img = True
    move_all_unused_img = False
    # only ask what files to move if there are files that could potentially be moved
    if notused_img:
        # count the number of toplevel vs not-toplevel in "notused_img"
        num_toplevel = len(
            [p for p in notused_img if (os.path.sep not in p.name)])
        num_nontoplevel = len(notused_img) - num_toplevel
        # ask the user what "aggression" level they want
        showinfo = [
            "Detected %d unused top-level files and %d unused files in directories."
            % (num_toplevel, num_nontoplevel),
            "Which files do you want to move to 'unused' folder?",
            "1 = Do not move any, 2 = Move only top-level unused, 3 = Move all unused"
        ]
        c = core.MY_SIMPLECHOICE_FUNC((1, 2, 3), showinfo)
        if c == 2:
            move_toplevel_unused_img = True
            move_all_unused_img = False
        elif c == 3:
            move_toplevel_unused_img = True
            move_all_unused_img = True
        else:  # c == 1:
            move_toplevel_unused_img = False
            move_all_unused_img = False

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # DETERMINE NEW NAMES FOR FILES

    # how to remap: build a list of all destinations (lowercase) to see if any proposed change would lead to collision
    all_new_names = set()

    # don't touch the unused_notimg files at all, unless some flag is set

    # not-used top-level image files get moved to 'unused' folder
    # also all spa/sph get renamed to .bmp (but remember these are all unused so i don't need to update them in the pmx)
    for p in notused_img:
        newname = remove_pattern(p.name)
        if ((os.path.sep not in p.name)
                and move_toplevel_unused_img) or move_all_unused_img:
            # this deserves to be moved to 'unused' folder!
            newname = os.path.join(FOLDER_UNUSED, os.path.basename(newname))

        # ensure the extension is lowercase, for cleanliness
        dot = newname.rfind(".")
        newname = newname[:dot] + newname[dot:].lower()
        if CONVERT_SPA_SPH_TO_BMP and newname.endswith((".spa", ".sph")):
            newname = newname[:-4] + ".bmp"
        # if the name I build is not the name it already has, queue it for actual rename
        if newname != p.name:
            # resolve potential collisions by adding numbers suffix to file names
            # first need to make path absolute so get_unused_file_name can check the disk.
            # then check uniqueness against files on disk and files in namelist (files that WILL be on disk)
            newname = core.get_unused_file_name(os.path.join(
                startpath, newname),
                                                namelist=all_new_names)
            # now dest path is guaranteed unique against other existing files & other proposed name changes
            all_new_names.add(newname.lower())
            # make the path no longer absolute: undo adding "startpath" above
            newname = os.path.relpath(newname, startpath)
            p.newname = newname

    # used files get sorted into tex/toon/sph/multi (unless tex and already in a folder that says clothes, etc)
    # all SPH/SPA get renamed to BMP, used or unused
    for p in used_exist:
        newname = remove_pattern(p.name)
        usage_list = list(p.usage)
        if len(p.usage) != 1:
            # this is a rare multiple-use file
            newname = os.path.join(FOLDER_MULTI, os.path.basename(newname))
        elif usage_list[0] == FOLDER_SPH:
            # this is an sph, duh
            if not match_folder_anylevel(
                    p.name, KEEP_FOLDERS_SPH, toponly=True):
                # if its name isn't already good, then move it to my new location
                newname = os.path.join(FOLDER_SPH, os.path.basename(newname))
        elif usage_list[0] == FOLDER_TOON:
            # this is a toon, duh
            if not match_folder_anylevel(
                    p.name, KEEP_FOLDERS_TOON, toponly=True):
                # if its name isn't already good, then move it to my new location
                newname = os.path.join(FOLDER_TOON, os.path.basename(newname))
        elif usage_list[0] == FOLDER_TEX:
            # if a tex AND already in a folder like body, clothes, wear, tex, etc then keep that folder
            if not match_folder_anylevel(
                    p.name, KEEP_FOLDERS_TEX, toponly=True):
                # if its name isn't already good, then move it to my new location
                newname = os.path.join(FOLDER_TEX, os.path.basename(newname))

        # ensure the extension is lowercase, for cleanliness
        dot = newname.rfind(".")
        newname = newname[:dot] + newname[dot:].lower()
        if CONVERT_SPA_SPH_TO_BMP and newname.lower().endswith(
            (".spa", ".sph")):
            newname = newname[:-4] + ".bmp"
        # if the name I build is not the name it already has, queue it for actual rename
        if newname != p.name:
            # resolve potential collisions by adding numbers suffix to file names
            # first need to make path absolute so get_unused_file_name can check the disk.
            # then check uniqueness against files on disk and files in namelist (files that WILL be on disk)
            newname = core.get_unused_file_name(os.path.join(
                startpath, newname),
                                                namelist=all_new_names)
            # now dest path is guaranteed unique against other existing files & other proposed name changes
            all_new_names.add(newname.lower())
            # make the path no longer absolute: undo adding "startpath" above
            newname = os.path.relpath(newname, startpath)
            p.newname = newname

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # NOW PRINT MY PROPOSED RENAMINGS and other findings

    # isolate the ones with proposed renaming
    used_rename = [u for u in used_exist if u.newname is not None]
    notused_img_rename = [u for u in notused_img if u.newname is not None]
    notused_img_norename = [u for u in notused_img if u.newname is None]

    # bonus goal: if ALL files under a folder are unused, replace its name with a star
    # first build dict of each dirs to each file any depth below that dir
    all_dirnames = {}
    for f in relative_all_exist_files:
        d = os.path.dirname(f)
        while d != "":
            try:
                all_dirnames[d].append(f)
            except KeyError:
                all_dirnames[d] = [f]
            d = os.path.dirname(d)
    unused_dirnames = []
    all_notused_searchable = [x.name for x in notused_img_norename
                              ] + [x.name for x in notused_notimg]
    for d, files_under_d in all_dirnames.items():
        # if all files beginning with d are notused (either type), this dir can be replaced with *
        # note: min crashes if input list is empty, but this is guaranteed to not be empty
        dir_notused = min([(f in all_notused_searchable)
                           for f in files_under_d])
        if dir_notused:
            unused_dirnames.append(d)
    # print("allundir", unused_dirnames)
    # now, remove all dirnames that are encompassed by another dirname
    j = 0
    while j < len(unused_dirnames):
        dj = unused_dirnames[j]
        k = 0
        while k < len(unused_dirnames):
            dk = unused_dirnames[k]
            if dj != dk and dk.startswith(dj):
                unused_dirnames.pop(k)
            else:
                k += 1
        j += 1
    # make sure unused_dirnames has the deepest directories first
    unused_dirnames = sorted(unused_dirnames,
                             key=lambda y: y.count(os.path.sep),
                             reverse=True)
    # print("unqundir", unused_dirnames)
    # then as I go to print notused_img_norename or notused_notimg, collapse them?

    # for each section, if it exists, print its names sorted first by directory depth then alphabetically (case insensitive)

    if used_notexist:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC(
            "Found %d references to images that don't exist (no proposed changes)"
            % len(used_notexist))
        for p in sorted(used_notexist, key=lambda y: sortbydirdepth(y.name)):
            # print orig name, usage modes, # used, and # files that use it
            core.MY_PRINT_FUNC("   " + str(p))
    if notused_img_norename:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC(
            "Found %d not-used images in the file tree (no proposed changes)" %
            len(notused_img_norename))
        printme = set()
        for p in notused_img_norename:
            # is this notused-file anywhere below any unused dir?
            t = False
            for d in unused_dirnames:
                if p.name.startswith(d):
                    # add this dir, not this file, to the print set
                    printme.add(os.path.join(d, "***"))
                    t = True
            if not t:
                # if not encompassed by an unused dir, add the filename
                printme.add(p.name)
        # convert set back to sorted list
        printme = sorted(list(printme), key=sortbydirdepth)
        for s in printme:
            core.MY_PRINT_FUNC("   " + s)
    if notused_notimg:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC(
            "Found %d not-used not-images in the file tree (no proposed changes)"
            % len(notused_notimg))
        printme = set()
        for p in notused_notimg:
            # is this notused-file anywhere below any unused dir?
            t = False
            for d in unused_dirnames:
                if p.name.startswith(d):
                    # add this dir, not this file, to the print set
                    printme.add(os.path.join(d, "***"))
                    t = True
            if not t:
                # if not encompassed by an unused dir, add the filename
                printme.add(p.name)
        # convert set back to sorted list
        printme = sorted(list(printme), key=sortbydirdepth)
        for s in printme:
            core.MY_PRINT_FUNC("   " + s)
    # print with all "from" file names left-justified so all the arrows are nicely lined up (unless they use jp characters)
    longest_name_len = 0
    for p in used_rename:
        longest_name_len = max(longest_name_len, len(p.name))
    for p in notused_img_rename:
        longest_name_len = max(longest_name_len, len(p.name))
    if used_rename:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC("Found %d used files to be moved/renamed:" %
                           len(used_rename))
        oldname_list = core.MY_JUSTIFY_STRINGLIST(
            [p.name for p in used_rename])
        newname_list = [p.newname for p in used_rename]
        zipped = list(zip(oldname_list, newname_list))
        zipped_and_sorted = sorted(zipped, key=lambda y: sortbydirdepth(y[0]))
        for o, n in zipped_and_sorted:
            # print 'from' with the case/separator it uses in the PMX
            core.MY_PRINT_FUNC("   {:s} --> {:s}".format(o, n))
    if notused_img_rename:
        core.MY_PRINT_FUNC("=" * 60)
        core.MY_PRINT_FUNC("Found %d not-used images to be moved/renamed:" %
                           len(notused_img_rename))
        oldname_list = core.MY_JUSTIFY_STRINGLIST(
            [p.name for p in notused_img_rename])
        newname_list = [p.newname for p in notused_img_rename]
        zipped = list(zip(oldname_list, newname_list))
        zipped_and_sorted = sorted(zipped, key=lambda y: sortbydirdepth(y[0]))
        for o, n in zipped_and_sorted:
            # print 'from' with the case/separator it uses in the PMX
            core.MY_PRINT_FUNC("   {:s} --> {:s}".format(o, n))
    core.MY_PRINT_FUNC("=" * 60)

    if not (used_rename or notused_img_rename):
        core.MY_PRINT_FUNC("No proposed file changes")
        core.MY_PRINT_FUNC("Aborting: no files were changed")
        return None

    info = [
        "Do you accept these new names/locations?", "1 = Yes, 2 = No (abort)"
    ]
    r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
    if r == 2:
        core.MY_PRINT_FUNC("Aborting: no files were changed")
        return None

    # =========================================================================================================
    # =========================================================================================================
    # =========================================================================================================
    # finally, do the actual renaming:

    # first, create a backup of the folder
    if MAKE_BACKUP_BEFORE_RENAMES:
        r = make_zipfile_backup(startpath, BACKUP_SUFFIX)
        if not r:
            # this happens if the backup failed somehow AND the user decided to quit
            core.MY_PRINT_FUNC("Aborting: no files were changed")
            return None

    # do all renaming on disk and in PMXes, and also handle the print statements
    apply_file_renaming(all_pmx_obj, filerecord_list, startpath)

    # write out
    for this_pmx_name, this_pmx_obj in all_pmx_obj.items():
        # NOTE: this is OVERWRITING THE PREVIOUS PMX FILE, NOT CREATING A NEW ONE
        # because I make a zipfile backup I don't need to feel worried about preserving the old version
        output_filename_pmx = os.path.join(startpath, this_pmx_name)
        # output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
        pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo)

    core.MY_PRINT_FUNC("Done!")
    return None
def main(moreinfo=False):
	# step zero: verify that Pillow exists
	if Image is None:
		core.MY_PRINT_FUNC("ERROR: Python library 'Pillow' not found. This script requires this library to run!")
		core.MY_PRINT_FUNC("This script cannot be ran from the EXE version, the Pillow library is too large to package into the executable.")
		core.MY_PRINT_FUNC("To install Pillow, please use the command 'pip install Pillow' in the Windows command prompt and then run the Python scripts directly.")
		return None
	# print pillow version just cuz
	core.MY_PRINT_FUNC("Using Pillow version '%s'" % Image.__version__)
	
	core.MY_PRINT_FUNC("Please enter name of PMX model file:")
	input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx")
	
	# absolute path to directory holding the pmx
	input_filename_pmx_abs = os.path.normpath(os.path.abspath(input_filename_pmx))
	startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs)
	
	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================
	# first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs and relevant files
	relative_all_exist_files = file_sort_textures.walk_filetree_from_root(startpath)
	core.MY_PRINT_FUNC("ALL EXISTING FILES:", len(relative_all_exist_files))
	# now fill "neighbor_pmx" by finding files without path separator that end in PMX
	# these are relative paths tho
	neighbor_pmx = [f for f in relative_all_exist_files if 
					(f.lower().endswith(".pmx")) and
					(os.path.sep not in f) and
					f != input_filename_pmx_rel]
	core.MY_PRINT_FUNC("NEIGHBOR PMX FILES:", len(neighbor_pmx))
	
	# filter down to just image files
	relevant_exist_files = [f for f in relative_all_exist_files if f.lower().endswith(IMG_EXT)]
	core.MY_PRINT_FUNC("RELEVANT EXISTING FILES:", len(relevant_exist_files))

	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================
	# now ask if I care about the neighbors and read the PMXes into memory
	
	pmx_filenames = [input_filename_pmx_rel]
	
	if neighbor_pmx:
		core.MY_PRINT_FUNC("")
		info = [
			"Detected %d top-level neighboring PMX files, these probably share the same filebase as the target." % len(neighbor_pmx),
			"If files are moved/renamed but the neighbors are not processed, the neighbor texture references will probably break.",
			"Do you want to process all neighbors in addition to the target? (highly recommended)",
			"1 = Yes, 2 = No"]
		r = core.MY_SIMPLECHOICE_FUNC((1, 2), info)
		if r == 1:
			core.MY_PRINT_FUNC("Processing target + all neighbor files")
			# append neighbor PMX files onto the list of files to be processed
			pmx_filenames += neighbor_pmx
		else:
			core.MY_PRINT_FUNC("WARNING: Processing only target, ignoring %d neighbor PMX files" % len(neighbor_pmx))
	# now read all the PMX objects & store in dict alongside the relative name
	# dictionary where keys are filename and values are resulting pmx objects
	all_pmx_obj = {}
	for this_pmx_name in pmx_filenames:
		this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name), moreinfo=moreinfo)
		all_pmx_obj[this_pmx_name] = this_pmx_obj
	
	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================
	# 	for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk
	#	also fill out how much and how each file is used, and unify dupes between files, all that good stuff
	
	filerecord_list = file_sort_textures.categorize_files(all_pmx_obj, relevant_exist_files, moreinfo)
	
	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================
	# DETERMINE NEW NAMES FOR FILES
	
	# first, create a backup of the folder
	# save the name, so that i can delete it if i didn't make any changes
	zipfile_name = ""
	if MAKE_BACKUP_ZIPFILE:
		r = file_sort_textures.make_zipfile_backup(startpath, BACKUP_SUFFIX)
		if not r:
			# this happens if the backup failed somehow AND the user decided to quit
			core.MY_PRINT_FUNC("Aborting: no files were changed")
			return None
		zipfile_name = r
		
	# name used for temporary location
	tempfilename = os.path.join(startpath,"temp_image_file_just_delete_me.png")
	
	pil_cannot_inspect = 0
	pil_cannot_inspect_list = []
	pil_imgext_mismatch = 0
	
	num_recompressed = 0
	
	# list of memory saved by recompressing each file. same order/length as "image_filerecords"
	mem_saved = []
	
	# make image persistient, so I know it always exists and I can always call "close" before open
	im = None
	
	# only iterate over images that exist, obviously
	image_filerecords = [f for f in filerecord_list if f.exists]
	# iterate over the images
	for i, p in enumerate(image_filerecords):
		abspath = os.path.join(startpath, p.name)
		orig_size = os.path.getsize(abspath)

		# if not moreinfo, then each line overwrites the previous like a progress printout does
		# if moreinfo, then each line is printed permanently
		core.MY_PRINT_FUNC("...analyzing {:>3}/{:>3}, file='{}', size={}                ".format(
			i+1, len(image_filerecords), p.name, core.prettyprint_file_size(orig_size)), is_progress=(not moreinfo))
		mem_saved.append(0)

		# before opening, try to close it just in case
		if im is not None:
			im.close()
		# open the image & catch all possible errors
		try:
			im = Image.open(abspath)
		except FileNotFoundError as eeee:
			core.MY_PRINT_FUNC("FILESYSTEM MALFUNCTION!!", eeee.__class__.__name__, eeee)
			core.MY_PRINT_FUNC("os.walk created a list of all filenames on disk, but then this filename doesn't exist when i try to open it?")
			im = None
		except OSError as eeee:
			# this has 2 causes, "Unsupported BMP bitfields layout" or "cannot identify image file"
			if DEBUG:
				print("CANNOT INSPECT!1", eeee.__class__.__name__, eeee, p.name)
			im = None
		except NotImplementedError as eeee:
			# this is because there's some DDS format it can't make sense of
			if DEBUG:
				print("CANNOT INSPECT!2", eeee.__class__.__name__, eeee, p.name)
			im = None
		if im is None:
			pil_cannot_inspect += 1
			pil_cannot_inspect_list.append(p.name)
			continue
			
		if im.format not in IMG_TYPE_TO_EXT:
			core.MY_PRINT_FUNC("WARNING: file '%s' has unusual image format '%s', attempting to continue" % (p.name, im.format))
		# now the image is successfully opened!

		newname = p.name
		base, currext = os.path.splitext(newname)

		# 1, depending on image format, attempt to re-save as PNG
		if im.format not in IM_FORMAT_ALWAYS_SKIP:
			# delete temp file if it still exists
			if os.path.exists(tempfilename):
				try:
					os.remove(tempfilename)
				except OSError as e:
					core.MY_PRINT_FUNC(e.__class__.__name__, e)
					core.MY_PRINT_FUNC("ERROR1: failed to delete temp image file '%s' during processing" % tempfilename)
					break
			# save to tempfilename with png format, use optimize=true
			try:
				im.save(tempfilename, format="PNG", optimize=True)
			except OSError as e:
				core.MY_PRINT_FUNC(e.__class__.__name__, e)
				core.MY_PRINT_FUNC("ERROR2: failed to re-compress image '%s', original not modified" % p.name)
				continue
			# measure & compare file size
			new_size = os.path.getsize(tempfilename)
			diff = orig_size - new_size
			
			# if using a 16-bit BMP format, re-save back to bmp with same name
			is_bad_bmp = False
			if im.format == "BMP":
				try:
					# this might fail, images are weird, sometimes they don't have the attributes i expect
					if im.tile[0][3][0] in KNOWN_BAD_FORMATS:
						is_bad_bmp = True
				except Exception as e:
					if DEBUG:
						print(e.__class__.__name__, e, "BMP THING", p.name, im.tile)

			if diff > (REQUIRED_COMPRESSION_AMOUNT_KB * 1024) \
					or is_bad_bmp\
					or im.format in IM_FORMAT_ALWAYS_CONVERT:
				# if it frees up at least XXX kb, i will keep it!
				# also keep it if it is a bmp encoded with 15-bit or 16-bit colors
				# set p.newname = png, and delete original and move tempname to base.png
				try:
					# delete original
					os.remove(os.path.join(startpath, p.name))
				except OSError as e:
					core.MY_PRINT_FUNC(e.__class__.__name__, e)
					core.MY_PRINT_FUNC("ERROR3: failed to delete old image '%s' after recompressing" % p.name)
					continue
				
				newname = base + ".png"
				# resolve potential collisions by adding numbers suffix to file names
				# first need to make path absolute so get_unused_file_name can check the disk.
				newname = os.path.join(startpath, newname)
				# then check uniqueness against files on disk
				newname = core.get_unused_file_name(newname)
				# now dest path is guaranteed unique against other existing files
				# make the path no longer absolute: undo adding "startpath" above
				newname = os.path.relpath(newname, startpath)
				
				try:
					# move new into place
					os.rename(tempfilename, os.path.join(startpath, newname))
				except OSError as e:
					core.MY_PRINT_FUNC(e.__class__.__name__, e)
					core.MY_PRINT_FUNC("ERROR4: after deleting original '%s', failed to move recompressed version into place!" % p.name)
					continue
				num_recompressed += 1
				p.newname = newname
				mem_saved[-1] = diff
				continue # if succesfully re-saved, do not do the extension-checking below
			# if this is not sufficiently compressed, do not use "continue", DO hit the extension-checking below
			
		# 2, if the file extension doesn't match with the image type, then make it match
		# this only happens if the image was not re-saved above
		if im.format in IMG_TYPE_TO_EXT and currext not in IMG_TYPE_TO_EXT[im.format]:
			newname = base + IMG_TYPE_TO_EXT[im.format][0]
			# resolve potential collisions by adding numbers suffix to file names
			# first need to make path absolute so get_unused_file_name can check the disk.
			newname = os.path.join(startpath, newname)
			# then check uniqueness against files on disk
			newname = core.get_unused_file_name(newname)
			# now dest path is guaranteed unique against other existing files
			# make the path no longer absolute: undo adding "startpath" above
			newname = os.path.relpath(newname, startpath)
			
			# do the actual rename here & now
			try:
				# os.renames creates all necessary intermediate folders needed for the destination
				# it also deletes the source folders if they become empty after the rename operation
				os.renames(os.path.join(startpath, p.name), os.path.join(startpath, newname))
			except OSError as e:
				core.MY_PRINT_FUNC(e.__class__.__name__, e)
				core.MY_PRINT_FUNC("ERROR5: unable to rename file '%s' --> '%s', attempting to continue with other file rename operations"
					% (p.name, newname))
				continue
				
			pil_imgext_mismatch += 1
			p.newname = newname
			continue
	
	# these must be the same length after iterating
	assert len(mem_saved) == len(image_filerecords)
	# if the image is still open, close it
	if im is not None:
		im.close()
	
	# delete temp file if it still exists
	if os.path.exists(tempfilename):
		try:
			os.remove(tempfilename)
		except OSError as e:
			core.MY_PRINT_FUNC(e.__class__.__name__, e)
			core.MY_PRINT_FUNC("WARNING: failed to delete temp image file '%s' after processing" % tempfilename)
	
	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================

	# are there any with proposed renaming?
	if not any(u.newname is not None for u in image_filerecords):
		core.MY_PRINT_FUNC("No proposed file changes")
		# if nothing was changed, delete the backup zip!
		core.MY_PRINT_FUNC("Deleting backup archive")
		if os.path.exists(zipfile_name):
			try:
				os.remove(zipfile_name)
			except OSError as e:
				core.MY_PRINT_FUNC(e.__class__.__name__, e)
				core.MY_PRINT_FUNC("WARNING: failed to delete pointless zip file '%s'" % zipfile_name)
		core.MY_PRINT_FUNC("Aborting: no files were changed")
		return None

	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================

	# finally, do the actual renaming:
	
	# do all renaming in PMXes
	file_sort_textures.apply_file_renaming(all_pmx_obj, image_filerecords, startpath, skipdiskrename=True)
	
	# write out
	for this_pmx_name, this_pmx_obj in all_pmx_obj.items():
		# NOTE: this is OVERWRITING THE PREVIOUS PMX FILE, NOT CREATING A NEW ONE
		# because I make a zipfile backup I don't need to feel worried about preserving the old version
		output_filename_pmx = os.path.join(startpath, this_pmx_name)
		# output_filename_pmx = core.get_unused_file_name(output_filename_pmx)
		pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=moreinfo)
	
	# =========================================================================================================
	# =========================================================================================================
	# =========================================================================================================
	# NOW PRINT MY RENAMINGS and other findings
	
	filerecord_with_savings = zip(image_filerecords, mem_saved)
	changed_files = [u for u in filerecord_with_savings if u[0].newname is not None]

	core.MY_PRINT_FUNC("="*60)
	if pil_cannot_inspect:
		core.MY_PRINT_FUNC("WARNING: failed to inspect %d image files, these must be handled manually" % pil_cannot_inspect)
		core.MY_PRINT_FUNC(pil_cannot_inspect_list)
	if num_recompressed:
		core.MY_PRINT_FUNC("Recompressed %d images! %s of disk space has been freed" % (num_recompressed, core.prettyprint_file_size(sum(mem_saved))))
	if pil_imgext_mismatch:
		core.MY_PRINT_FUNC("Renamed %d images that had incorrect extensions (included below)" % pil_imgext_mismatch)
	oldname_list = [p[0].name for p in changed_files]
	oldname_list_j = core.MY_JUSTIFY_STRINGLIST(oldname_list)
	newname_list = [p[0].newname for p in changed_files]
	newname_list_j = core.MY_JUSTIFY_STRINGLIST(newname_list)
	savings_list = [("" if p[1]==0 else "saved " + core.prettyprint_file_size(p[1])) for p in changed_files]
	zipped = list(zip(oldname_list_j, newname_list_j, savings_list))
	zipped_and_sorted = sorted(zipped, key=lambda y: file_sort_textures.sortbydirdepth(y[0]))
	for o,n,s in zipped_and_sorted:
		# print 'from' with the case/separator it uses in the PMX
		core.MY_PRINT_FUNC("   {:s} --> {:s} | {:s}".format(o, n, s))
		
	core.MY_PRINT_FUNC("Done!")
	return None
Esempio n. 25
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