def make_point_from_slope(slope: float) -> Tuple[int, int]: """ Create a depart-point (around 0,0) when given a desired slope. The distance to the point is based on global options. :param slope: float slope, from 0-inf :return: tuple(x,y) ints from 0-127 """ assert slope >= 0 if CONTROL_POINT_PLACING_METHOD == 1: # max distance is half of sqrt2, then the circles centered at each corner touch in the middle dist = CONTROL_POINT_ECCENTRICITY * MAX_CIRCLE_RADIUS # the point is on the circle with radius "dist" centered at 0,0 # and where that circle intercepts the desired slope line # circle: use pythagoras + algebra! # dist^2 = x^2 + y^2 # y = x * slope # dist^2 = x^2 + (x*slope)^2 # dist^2 = xx + xxss # dist^2 = xx(1 + ss) # dist^2 / (1+ss) = xx # sqrt[dist^2 / (1+ss)] = x x = math.sqrt((dist**2) / (1 + (slope**2))) y = x * slope else: #elif CONTROL_POINT_PLACING_METHOD == 2: # the point is on the downward-slanting diagonal line with y-intercept "dist" # y = x * slope # y = -x + dist # x * s = -x + dist # x * s = x(-1 + dist) # ????? # x = d / (s + 1), thank you wolframalpha dist = CONTROL_POINT_ECCENTRICITY x = dist / (slope + 1) y = x * slope # clamp just to be extra safe x = core.clamp(x, 0.0, 1.0) y = core.clamp(y, 0.0, 1.0) # convert [0-1] to nearest int in [0-127] range x = round(x * 127) y = round(y * 127) return x, y
def transfer_bone_weights(pmx: pmxstruct.Pmx, to_bone: int, from_bone: int, scalefactor=1.0): # TODO: test this! ensure that this math is logically correct! not 100% convinced if to_bone == from_bone: return # !!! WARNING: result is not normal, need to normalize afterward !!! # clamp just cuz scalefactor = core.clamp(scalefactor, 0.0, 1.0) # for each vertex, determine if that vert is controlled by from_bone for d, vert in enumerate(pmx.verts): w = vert.weight if vert.weighttype == pmxstruct.WeightMode.BDEF1: # BDEF1 if w[0][0] == from_bone: # match! if scalefactor == 1: w[0][0] = to_bone else: # TODO: rethink this? test this? not 100% convinced this is correct # if the from_bone had 100% weight but only a .5 factor, then this should only move half as much as the to_bone # the other half of the weight would come from the parent of the to_bone, i suppose? to_bone_parent = pmx.bones[to_bone].parent_idx if to_bone_parent == -1: # if to_bone is root and has no parent, w[0][0] = to_bone else: # if to_parent is valid, convert to BDEF2 vert.weighttype = pmxstruct.WeightMode.BDEF2 vert.weight = [[to_bone, scalefactor], [to_bone_parent, 1 - scalefactor]] elif vert.weighttype in (pmxstruct.WeightMode.BDEF2, pmxstruct.WeightMode.SDEF): # BDEF2, SDEF # (b1, b2, b1w) # replace the from_bone ref with to_bone, but also need to modify the value # a = b1w out, z = b1w in, f = scalefactor # a = z*f / (z*f + (1-z)) if w[0][0] == from_bone: w[0][0] = to_bone z = w[0][1] newval = (z * scalefactor) / (z * scalefactor + (1 - z)) w[0][1] = newval w[1][1] = 1 - newval elif w[1][0] == from_bone: w[1][0] = to_bone z = 1 - w[0][1] newval = 1 - (z * scalefactor) / (z * scalefactor + (1 - z)) w[0][1] = newval w[1][1] = 1 - newval elif vert.weighttype in (pmxstruct.WeightMode.BDEF4, pmxstruct.WeightMode.QDEF): # BDEF4, QDEF # (b1, b2, b3, b4, b1w, b2w, b3w, b4w) for pair in vert.weight: if pair[0] == from_bone and pair[1] != 0: pair[0] = to_bone pair[1] *= scalefactor # done! no return, modifies PMX list directly return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # input_filename_pmx = "../../python_scripts/grasstest_better.pmx" pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) ################################## # user flow: # first ask whether they want to add armtwist, yes/no # second ask whether they want to add legtwist, yes/no # then do it # then write out to file ################################## working_queue = [] s = core.MY_SIMPLECHOICE_FUNC((1, 2), [ "Do you wish to add magic twistbones to the ARMS?", "1 = Yes, 2 = No" ]) if s == 1: # add upperarm set and lowerarm set to the queue working_queue.append(armset) working_queue.append(wristset) pass s = core.MY_SIMPLECHOICE_FUNC((1, 2), [ "Do you wish to add magic twistbones to the LEGS?", "1 = Yes, 2 = No" ]) if s == 1: # TODO detect whether d-bones exist or not # add legs or d-legs set to the queue pass if not working_queue: core.MY_PRINT_FUNC("Nothing was changed") core.MY_PRINT_FUNC("Done") return None # for each set in the queue, for boneset in working_queue: # boneset = (start, end, preferred, oldrigs, bezier) for side in [jp_l, jp_r]: # print(side) # print(boneset) # 1. first, validate that start/end exist, these are required # NOTE: remember to prepend 'side' before all jp names! start_jp = side + boneset[0] start_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == start_jp) if start_idx is None: core.MY_PRINT_FUNC( "ERROR: standard bone '%s' not found in model, this is required!" % start_jp) continue end_jp = side + boneset[1] end_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == end_jp) if end_idx is None: core.MY_PRINT_FUNC( "ERROR: standard bone '%s' not found in model, this is required!" % end_jp) continue # 2. determine whether the 'preferredparent' exists and therefore what to acutally use as the parent parent_jp = side + boneset[2] parent_idx = core.my_list_search(pmx.bones, lambda x: x.name_jp == parent_jp) if parent_idx is None: parent_idx = start_idx # 3. attempt to collapse known armtwist rig names onto 'parent' so that the base case is further automated # for each bonename in boneset[3], if it exists, collapse onto boneidx parent_idx for bname in boneset[3]: rig_idx = core.my_list_search( pmx.bones, lambda x: x.name_jp == side + bname) if rig_idx is None: continue # if not found, try the next # when it is found, what 'factor' do i use? # print(side+bname) if pmx.bones[rig_idx].inherit_rot and pmx.bones[ rig_idx].inherit_parent_idx == parent_idx and pmx.bones[ rig_idx].inherit_ratio != 0: # if using partial rot inherit AND inheriting from parent_idx AND ratio != 0, use that # think this is good, if twistbones exist they should be children of preferred f = pmx.bones[rig_idx].inherit_ratio elif pmx.bones[rig_idx].parent_idx == parent_idx: # this should be just in case? f = 1 elif pmx.bones[rig_idx].parent_idx == start_idx: # this should catch magic armtwist bones i previously created f = 1 else: core.MY_PRINT_FUNC( "Warning, found unusual relationship when collapsing old armtwist rig, assuming ratio=1" ) f = 1 transfer_bone_weights(pmx, parent_idx, rig_idx, f) pass # also collapse 'start' onto 'preferredparent' if it exists... want to transfer weight from 'arm' to 'armtwist' # if start == preferredparent this does nothing, no harm done transfer_bone_weights(pmx, parent_idx, start_idx, scalefactor=1) # 4. run the weight-cleanup function normalize_weights(pmx) # 5. append 3 new bones to end of bonelist # armYZ gets pos = start pos & parent = start parent basename_jp = pmx.bones[start_idx].name_jp armYZ_new_idx = len(pmx.bones) # armYZ = [basename_jp + yz_suffix, local_translate(basename_jp + yz_suffix)] # name_jp,en # armYZ += pmx[5][start_idx][2:] # copy the whole rest of the bone # armYZ[10:12] = [False, False] # visible=false, enabled=false # armYZ[12:14] = [True, [armYZ_new_idx + 1]] # tail type = tail, tail pointat = armYZend # armYZ[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZ[21:25] = [False, [], False, []] # disable ext parent + ik armYZ = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix, name_en=local_translate(basename_jp + yz_suffix), pos=pmx.bones[start_idx].pos, parent_idx=pmx.bones[start_idx].parent_idx, deform_layer=pmx.bones[start_idx].deform_layer, deform_after_phys=pmx.bones[start_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[start_idx].localaxis_x, localaxis_z=pmx.bones[start_idx].localaxis_z, tail_type=True, tail=armYZ_new_idx + 1, has_rotate=True, has_translate=True, has_visible=False, has_enabled=True, has_ik=False, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_externalparent=False, ) # armYZend gets pos = end pos & parent = armYZ # armYZend = [basename_jp + yz_suffix + "先", local_translate(basename_jp + yz_suffix + "先")] # name_jp,en # armYZend += pmx[5][end_idx][2:] # copy the whole rest of the bone # armYZend[5] = armYZ_new_idx # parent = armYZ # armYZend[10:12] = [False, False] # visible=false, enabled=false # armYZend[12:14] = [True, [-1]] # tail type = tail, tail pointat = none # armYZend[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZend[21:25] = [False, [], False, []] # disable ext parent + ik armYZend = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix + "先", name_en=local_translate(basename_jp + yz_suffix + "先"), pos=pmx.bones[end_idx].pos, parent_idx=armYZ_new_idx, deform_layer=pmx.bones[end_idx].deform_layer, deform_after_phys=pmx.bones[end_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[end_idx].localaxis_x, localaxis_z=pmx.bones[end_idx].localaxis_z, tail_type=True, tail=-1, has_rotate=True, has_translate=True, has_visible=False, has_enabled=True, has_ik=False, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_externalparent=False, ) # # elbowIK gets pos = end pos & parent = end parent # armYZIK = [basename_jp + yz_suffix + "IK", local_translate(basename_jp + yz_suffix + "IK")] # name_jp,en # armYZIK += pmx[5][end_idx][2:] # copy the whole rest of the bone # armYZIK[10:12] = [False, False] # visible=false, enabled=false # armYZIK[12:14] = [True, [-1]] # tail type = tail, tail pointat = none # armYZIK[14:19] = [False, False, [], False, []] # disable partial inherit + fixed axis # # local axis is copy # armYZIK[21:23] = [False, []] # disable ext parent # armYZIK[23] = True # ik=true # # add the ik info: [target, loops, anglelimit, [[link_idx, []], [link_idx, []]] ] # armYZIK[24] = [armYZ_new_idx+1, newik_loops, newik_angle, [[armYZ_new_idx, []]]] armYZIK = pmxstruct.PmxBone( name_jp=basename_jp + yz_suffix + "IK", name_en=local_translate(basename_jp + yz_suffix + "IK"), pos=pmx.bones[end_idx].pos, parent_idx=pmx.bones[end_idx].parent_idx, deform_layer=pmx.bones[end_idx].deform_layer, deform_after_phys=pmx.bones[end_idx].deform_after_phys, has_localaxis=True, localaxis_x=pmx.bones[end_idx].localaxis_x, localaxis_z=pmx.bones[end_idx].localaxis_z, tail_type=True, tail=-1, has_rotate=True, has_translate=True, has_visible=False, has_externalparent=False, has_enabled=True, inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_ik=True, ik_target_idx=armYZ_new_idx + 1, ik_numloops=newik_loops, ik_angle=newik_angle, ik_links=[pmxstruct.PmxBoneIkLink(idx=armYZ_new_idx)]) # now append them to the bonelist pmx.bones.append(armYZ) pmx.bones.append(armYZend) pmx.bones.append(armYZIK) # 6. build the bezier curve bezier_curve = core.MyBezier(boneset[4][0], boneset[4][1], resolution=50) # 7. find relevant verts & determine unbounded percentile for each (verts, percentiles, centers) = calculate_percentiles(pmx, start_idx, end_idx, parent_idx) if moreinfo: core.MY_PRINT_FUNC( "Blending between bones '{}'/'{}'=ZEROtwist and '{}'/'{}'=FULLtwist" .format(armYZ.name_jp, armYZ.name_en, pmx.bones[parent_idx].name_jp, pmx.bones[parent_idx].name_en)) core.MY_PRINT_FUNC( " Found %d potentially relevant vertices" % len(verts)) # 8. use X or Y to choose border points, print for debugging, also scale the percentiles # first sort ascending by percentile value vert_zip = list(zip(verts, percentiles, centers)) vert_zip.sort(key=lambda x: x[1]) verts, percentiles, centers = zip(*vert_zip) # unzip # X. highest point mode # "liberal" endpoints: extend as far as i can, include all good stuff even if i include some bad stuff with it # start at each end and work inward until i find a vert controlled by only parent_idx i_min_liberal = 0 i_max_liberal = len(verts) - 1 i_min_conserv = -1 i_max_conserv = len(verts) for i_min_liberal in range( 0, len(verts)): # start at head and work down, if pmx.verts[verts[ i_min_liberal]].weighttype == 0: # if the vertex is BDEF1 type, break # then stop looking, p_min_liberal = percentiles[ i_min_liberal] # and save the percentile it found. for i_max_liberal in reversed(range( 0, len(verts))): # start at tail and work up, if pmx.verts[verts[ i_max_liberal]].weighttype == 0: # if the vertex is BDEF1 type, break # then stop looking, p_max_liberal = percentiles[ i_max_liberal] # and save the percentile it found. # Y. lowest highest point mode # "conservative" endpoints: define ends such that no bad stuff exists within bounds, even if i miss some good stuff # start in the middle and work outward until i find a vert NOT controlled by only parent_idx, then back off 1 # where is the middle? use "bisect_left" middle = core.bisect_left(percentiles, 0.5) for i_min_conserv in reversed( range(middle - 1)): # start in middle, work toward head, if pmx.verts[verts[ i_min_conserv]].weighttype != 0: # if the vertex is NOT BDEF1 type, break # then stop looking, i_min_conserv += 1 # and step back 1 to find the last vert that was good BDEF1, p_min_conserv = percentiles[ i_min_conserv] # and save the percentile it found. for i_max_conserv in range( middle + 1, len(verts)): # start in middle, work toward tail, if pmx.verts[verts[ i_max_conserv]].weighttype != 0: # if the vertex is NOT BDEF1 type, break # then stop looking, i_max_conserv -= 1 # and step back 1 to find the last vert that was good BDEF1, p_max_conserv = percentiles[ i_max_conserv] # and save the percentile it found. foobar = False if not (i_min_liberal <= i_min_conserv <= i_max_conserv <= i_max_liberal): core.MY_PRINT_FUNC( "ERROR: bounding indexes do not follow the expected relationship, results may be bad!" ) foobar = True if foobar or moreinfo: core.MY_PRINT_FUNC( " Max liberal bounds: idx = %d to %d, %% = %f to %f" % (i_min_liberal, i_max_liberal, p_min_liberal, p_max_liberal)) core.MY_PRINT_FUNC( " Max conservative bounds: idx = %d to %d, %% = %f to %f" % (i_min_conserv, i_max_conserv, p_min_conserv, p_max_conserv)) # IDEA: WEIGHTED BLEND! sliding scale! avg_factor = core.clamp(ENDPOINT_AVERAGE_FACTOR, 0.0, 1.0) if p_min_liberal != p_min_conserv: p_min = (p_min_liberal * avg_factor) + (p_min_conserv * (1 - avg_factor)) else: p_min = p_min_liberal if p_max_liberal != p_max_conserv: p_max = (p_max_liberal * avg_factor) + (p_max_conserv * (1 - avg_factor)) else: p_max = p_max_liberal # clamp just in case p_min = core.clamp(p_min, 0.0, 1.0) p_max = core.clamp(p_max, 0.0, 1.0) if moreinfo: i_min = core.bisect_left(percentiles, p_min) i_max = core.bisect_left(percentiles, p_max) core.MY_PRINT_FUNC( " Compromise bounds: idx = %d to %d, %% = %f to %f" % (i_min, i_max, p_min, p_max)) # now normalize the percentiles to these endpoints p_len = p_max - p_min percentiles = [(p - p_min) / p_len for p in percentiles] # 9. divide weight between preferredparent (or parent) and armYZ vert_zip = list(zip(verts, percentiles, centers)) num_modified, num_bleeders = divvy_weights( pmx=pmx, vert_zip=vert_zip, axis_limits=(pmx.bones[start_idx].pos, pmx.bones[end_idx].pos), bone_hasweight=parent_idx, bone_getsweight=armYZ_new_idx, bezier=bezier_curve) if moreinfo: core.MY_PRINT_FUNC( " Modified %d verts to use blending, %d are questionable 'bleeding' points" % (num_modified, num_bleeders)) pass pass # 10. run final weight-cleanup normalize_weights(pmx) # 11. write out output_filename_pmx = input_filename_pmx[0:-4] + "_magictwist.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def get_corner_sharpness_factor( quatA: Tuple[float, float, float, float], quatB: Tuple[float, float, float, float], quatC: Tuple[float, float, float, float]) -> float: """ Calculate a [0.0-1.0] factor indicating how "sharp" the corner is at B. By "corner" I mean the directional change when A->B stops and B->C begins. If they are going the same angular "direction", then return 1.0. If they are going perfectly opposite directions, return 0.0. Otherwise return something in between. The option ROTATION_CORNER_SHARPNESS_FACTOR_MODE controls what the transfer curve looks like from angle to factor. :param quatA: quaterinon WXYZ for frame A :param quatB: quaterinon WXYZ for frame B :param quatC: quaterinon WXYZ for frame C :return: float [0.0-1.0] """ # to compensate for the angle difference, both will be slowed by some amount # IDENTICAL IMPACT # first, find the deltas between the quaternions deltaquat_AB = core.hamilton_product(core.my_quat_conjugate(quatA), quatB) deltaquat_BC = core.hamilton_product(core.my_quat_conjugate(quatB), quatC) # to get sensible results below, ignore the "W" component and only use the XYZ components, treat as 3d vector deltavect_AB = deltaquat_AB[1:4] deltavect_BC = deltaquat_BC[1:4] # second, find the angle between these two deltas # use the plain old "find the angle between two vectors" formula t = core.my_euclidian_distance(deltavect_AB) * core.my_euclidian_distance( deltavect_BC) if t == 0: # this happens when one vector has a length of 0 ang_d = 0 else: # technically the clamp shouldn't be necessary but floating point inaccuracy caused it to do math.acos(1.000000002) which crashed lol shut_up = core.my_dot(deltavect_AB, deltavect_BC) / t shut_up = core.clamp(shut_up, -1.0, 1.0) ang_d = math.acos(shut_up) # print(math.degrees(ang_d)) # if ang = 0, perfectly colinear, factor = 1 # if ang = 180, perfeclty opposite, factor = 0 factor = 1 - (math.degrees(ang_d) / 180) # print(factor) # ANGLE_SHARPNESS_FACTORS.append(factor) if ROTATION_CORNER_SHARPNESS_FACTOR_MODE == 1: # disabled out_factor = 1 elif ROTATION_CORNER_SHARPNESS_FACTOR_MODE == 2: # linear out_factor = factor elif ROTATION_CORNER_SHARPNESS_FACTOR_MODE == 3: # square root out_factor = math.sqrt(factor) elif ROTATION_CORNER_SHARPNESS_FACTOR_MODE == 4: # piecewise floored, (0,.5) to (.5,1) out_factor = 0.5 + factor out_factor = core.clamp(out_factor, 0.0, 1.0) else: out_factor = 1 out_factor = core.clamp(out_factor, 0.0, 1.0) return out_factor
def rotation_calculate_ideal_bezier_slope( A: Optional[Tuple[int, Sequence[float]]], B: Tuple[int, Sequence[float]], C: Optional[Tuple[int, Sequence[float]]]) -> Tuple[float, float]: """ Calculate the ideal bezier slope for rotation interpolation curve. There is only one Bezier curve for the entire 3d rotation, meaning it can vary the speed along the path between keyframes but cannot deviate from that path. Part 1 is speeding/slowing to match angular speed when approaching/leaving a keyframe. Part 2 is detecting the "sharpness" of the corners and slowing down when approaching/leaving a sharp corner. :param A: tuple(frame, euler xyz) or None :param B: tuple(frame, euler xyz) :param C: tuple(frame, euler xyz) or None :return: ideal bezier slopes, 2 floats, tuple(approach,depart) """ if A is None and C is None: # both sides are cutpoints return 1, 1 if A is None: # AB is an endpoint! # mark B-approach as a cutpoint (1) and B-depart as a cutpoint border (-1) return 1, -1 if C is None: # BC is an endpoint! # mark B-approach as a cutpoint border (-1) and B-depart as a cutpoint (1) return -1, 1 # now i can proceed knowing that A and C are guaranteed to not be None quatA = core.euler_to_quaternion(A[1]) quatB = core.euler_to_quaternion(B[1]) quatC = core.euler_to_quaternion(C[1]) # first, calc angle between each quat to get slerp "length" # technically the clamp shouldn't be necessary but floating point inaccuracy caused it to die asdf = abs(core.my_dot(quatA, quatB)) asdf = core.clamp(asdf, -1.0, 1.0) angdist_AB = math.acos(asdf) asdf = abs(core.my_dot(quatB, quatC)) asdf = core.clamp(asdf, -1.0, 1.0) angdist_BC = math.acos(asdf) # do some rounding to make extremely small numbers become zero if angdist_AB < CLOSE_TO_ZERO: angdist_AB = 0 if angdist_BC < CLOSE_TO_ZERO: angdist_BC = 0 # print(math.degrees(angdist_AB), math.degrees(angdist_BC)) # use framedelta to turn the "length" into "speed" # this is also the "truespace slope" of the approach/depart # cannot be negative, can be zero angslope_AB = angdist_AB / (B[0] - A[0]) angslope_BC = angdist_BC / (C[0] - B[0]) # second, average/compromise them to get the "desired truespace slope" if HOW_TO_FIND_DESIRED_SLOPE_FOR_ROTATION == 1: # desired = angle bisector method angslope_AB, angslope_BC = calculate_slope_bisectors( angslope_AB, angslope_BC, AVERAGE_SLOPES_BY_HOW_MUCH) else: # elif HOW_TO_FIND_DESIRED_SLOPE_FOR_POSITION == 2: # desired = total angular distance over total time total_slope = (angdist_AB + angdist_BC) / (C[0] - A[0]) blend = AVERAGE_SLOPES_BY_HOW_MUCH angslope_AB = (blend * total_slope) + ((blend - 1) * angslope_AB) angslope_BC = (blend * total_slope) + ((blend - 1) * angslope_BC) # third, determine how sharp the corner is [0-1]. 3d rotations are wierd. # reduce the slopes by this factor. factor = get_corner_sharpness_factor(quatA, quatB, quatC) angslope_AB *= factor angslope_BC *= factor if angdist_AB != 0 and angdist_BC != 0 and B[0] - A[0] != 0 and C[0] - B[ 0] != 0: # print(factor) ANGLE_SHARPNESS_FACTORS.append(factor) # fourth, scale the desired truespace slope to the bezier scale # also handle any corner cases out1, out2 = desired_truespace_slope_to_bezier_slope( (B[0] - A[0], angdist_AB), angslope_AB, (C[0] - B[0], angdist_BC), angslope_BC) # return exactly 1 approach slope and one depart slope return out1, out2