def swing_twist_decompose(quat_in, axis): """ Decompose the rotation on to 2 parts. 1. Twist - rotation around the "direction" vector 2. Swing - rotation around axis that is perpendicular to "direction" vector The rotation can be composed back by quat_in = swing * twist has singularity in case of swing_rotation close to 180 degrees rotation. if the input quaternion is of non-unit length, the outputs are non-unit as well otherwise, outputs are both unit output = (swing, twist) """ # vector3 quat_rotation_axis( quat_in.x, quat_in.y, quat_in.z ); // rotation axis # quat rotation axis quat_rotation_axis = quat_in[1:4] # vector3 p = projection( quat_rotation_axis, axis ); // return projection x on to y (parallel component) p = core.my_projection(quat_rotation_axis, axis) # twist.set( p.x, p.y, p.z, quat_in.w ); // but i use them as W X Y Z twist = [quat_in[0], p[0], p[1], p[2]] # twist.normalize(); length = core.my_euclidian_distance(twist) twist = [t / length for t in twist] # swing = quat_in * twist.conjugated(); twist_conjugate = core.my_quat_conjugate(twist) swing = core.hamilton_product(quat_in, twist_conjugate) return swing, twist
def rotate3d(origin, angle_quat, point_in): # "rotate around a point in 3d space" # subtract "origin" to move the whole system to rotating around 0,0,0 point = [p - o for p, o in zip(point_in, origin)] # might need to scale the point down to unit-length??? # i'll do it just to be safe, it couldn't hurt length = core.my_euclidian_distance(point) if length != 0: point = [p / length for p in point] # set up the math as instructed by math.stackexchange p_vect = [0] + point r_prime_vect = core.my_quat_conjugate(angle_quat) # r_prime_vect = [angle_quat[0], -angle_quat[1], -angle_quat[2], -angle_quat[3]] # P' = R * P * R' # P' = H( H(R,P), R') temp = core.hamilton_product(angle_quat, p_vect) p_prime_vect = core.hamilton_product(temp, r_prime_vect) # note that the first element of P' will always be 0 point = p_prime_vect[1:4] # might need to undo scaling the point down to unit-length??? point = [p * length for p in point] # re-add "origin" to move the system to where it should have been point = [p + o for p, o in zip(point, origin)] return point
def morph_winnow(pmx: pmxstruct.Pmx, moreinfo=False): total_num_verts = 0 total_vert_dropped = 0 total_morphs_affected = 0 morphs_now_empty = [] # for each morph: for d, morph in enumerate(pmx.morphs): # if not a vertex morph, skip it if morph.morphtype != 1: continue # for each vert in this vertex morph: i = 0 this_vert_dropped = 0 # lines dropped from this morph total_num_verts += len(morph.items) while i < len(morph.items): vert = morph.items[i] vert: pmxstruct.PmxMorphItemVertex # determine if it is worth keeping or deleting # first, calculate euclidian distance length = core.my_euclidian_distance(vert.move) if length < WINNOW_THRESHOLD: morph.items.pop(i) this_vert_dropped += 1 else: i += 1 if len(morph.items) == 0: # mark newly-emptied vertex morphs for later removal morphs_now_empty.append(d) # increment tracking variables if this_vert_dropped != 0: if moreinfo: core.MY_PRINT_FUNC( "morph #{:<3} JP='{}' / EN='{}', removed {} vertices". format(d, morph.name_jp, morph.name_en, this_vert_dropped)) total_morphs_affected += 1 total_vert_dropped += this_vert_dropped if total_vert_dropped == 0: core.MY_PRINT_FUNC("No changes are required") return pmx, False core.MY_PRINT_FUNC( "Dropped {} / {} = {:.1%} vertices from among {} affected morphs". format(total_vert_dropped, total_num_verts, total_vert_dropped / total_num_verts, total_morphs_affected)) if morphs_now_empty and DELETE_NEWLY_EMPTIED_MORPHS: core.MY_PRINT_FUNC( "Deleted %d morphs that had all of their vertices below the threshold" % len(morphs_now_empty)) rangemap = delme_list_to_rangemap(morphs_now_empty) pmx = apply_morph_remapping(pmx, morphs_now_empty, rangemap) return pmx, True
def normalize_normals(pmx: pmxstruct.Pmx) -> Tuple[int, List[int]]: """ Normalize normal vectors for each vertex in the PMX object. Return # of verts that were modified, and also a list of all vert indexes that have 0,0,0 normals and need special handling. :param pmx: PMX list-of-lists object :return: # verts modified + list of all vert idxs that have 0,0,0 normals """ norm_fix = 0 normbad = [] for d, vert in enumerate(pmx.verts): # normalize the normal if vert.norm == [0, 0, 0]: # invalid normals will be taken care of below normbad.append(d) else: norm_L = core.my_euclidian_distance(vert.norm) if round(norm_L, 6) != 1.0: norm_fix += 1 vert.norm = [n / norm_L for n in vert.norm] # printing is handled outside return norm_fix, normbad
def calculate_percentiles(pmx: pmxstruct.Pmx, bone_arm: int, bone_elbow: int, bone_hasweight: int): retme_verts = [] retme_percents = [] retme_centers = [] axis_start = pmx.bones[bone_arm].pos axis_end = pmx.bones[bone_elbow].pos # 1. determine the axis from arm to elbow deltax, deltay, deltaz = [e - s for e, s in zip(axis_end, axis_start)] startx, starty, startz = axis_start axis_length = core.my_euclidian_distance((deltax, deltay, deltaz)) # 2. determine y-rot and z-rot needed to make elbow have same Y/Z, elbowX > armX... first y-rotate, THEN z-rotate # ay = -atan2(dz, dx) theta_y = -math.atan2(deltaz, deltax) # apply 2d Y-rotation inter1x, inter1z = core.rotate2d(origin=(startx, startz), angle=theta_y, point=(axis_end[0], axis_end[2])) # az = -atan2(dy, dx)........ x position has changed, delta needs recalculated! y delta was untouched tho theta_z = -math.atan2(deltay, inter1x - startx) # now have found theta_y and theta_z # 3. collect all vertices controlled by bone_hasweight for d, vert in enumerate(pmx.verts): weighttype = vert.weighttype w = vert.weight weightlen = weight_type_to_len[weighttype] if bone_hasweight in w[0:weightlen]: # if this vert is controlled by bone_hasweight, then it is relevant! save it! # most of the verts aren't going to have 0 weight for a bone, dont worry about checking that retme_verts.append(d) # 4. calculate how far along the axis each vertex lies # percentile: 0.0 means at the startpoint, 1.0 means at the endpoint # apply 2d Y-rotation: ignore Y-axis, this will cause Z==Z inter1x, inter1z = core.rotate2d(origin=(startx, startz), angle=theta_y, point=(vert[0], vert[2])) # apply 2d Z-rotation: ignore Z-axis, this will cause Y==Y finalx, finaly = core.rotate2d(origin=(startx, starty), angle=theta_z, point=(inter1x, vert[1])) # calculate the actual percentile v_percentile = (finalx - startx) / axis_length retme_percents.append(v_percentile) # 5. calculate the centerpoint for if this bone is determined to use SDEF # c should lie on this start-end axis # to get c, after applying yrot zrot to a vertex, keep x unchanged and set y/z to match the startbone pos (point being rotated around) # center_before = finalx, starty, startz # then apply trig in reverse order: zrot, yrot # apply 2d Z-rotation: ignore Z-axis inter2x, inter2y = core.rotate2d(origin=(startx, starty), angle=-theta_z, point=(finalx, starty)) # apply 2d Y-rotation: ignore Y-axis centerx, centerz = core.rotate2d(origin=(startx, startz), angle=-theta_y, point=(inter2x, startz)) center = centerx, inter2y, centerz # result is the point along the axis that is closest to the vertex retme_centers.append(center) pass # close the loop return retme_verts, retme_percents, retme_centers
def repair_invalid_normals(pmx: pmxstruct.Pmx, normbad: List[int]) -> int: """ Repair all 0,0,0 normals in the model by averaging the normal vector for each face that vertex is a member of. It is theoretically possible for a vertex to be a member in two faces with exactly opposite normals, and therefore the average would be zero; in this case one of the faces is arbitrarily chosen and its normal is used. Therefore, after this function all invalid normals are guaranteed to be fixed. Returns the number of times this fallback method was used. :param pmx: PMX list-of-lists object :param normbad: list of vertex indices so I don't need to walk all vertices again :return: # times fallback method was used """ normbad_err = 0 # create a list in parallel with the faces list for holding the perpendicular normal to each face facenorm_list = [list() for i in pmx.faces] # create a list in paralle with normbad for holding the set of faces connected to each bad-norm vert normbad_linked_faces = [list() for i in normbad] # goal: build the sets of faces that are associated with each bad vertex # first, flatten the list of face-vertices, probably faster to search that way flatlist = [item for sublist in pmx.faces for item in sublist] # second, for each face-vertex, check if it is a bad vertex # (this takes 70% of time) for d, facevert in enumerate(flatlist): core.print_progress_oneline(.7 * d / len(flatlist)) # bad vertices are unique and in sorted order, can use binary search to further optimize whereinlist = core.binary_search_wherein(facevert, normbad) if whereinlist != -1: # if it is a bad vertex, int div by 3 to get face ID (normbad_linked_faces[whereinlist]).append(d // 3) # for each bad vert: # (this takes 30% of time) for d, (badvert_idx, badvert_faces) in enumerate(zip(normbad, normbad_linked_faces)): newnorm = [0, 0, 0] # default value in case something goes wrong core.print_progress_oneline(.7 + (.3 * d / len(normbad))) # iterate over the faces it is connected to for face_id in badvert_faces: # for each face, does the perpendicular normal already exist in the parallel list? if not, calculate and save it for reuse facenorm = facenorm_list[face_id] if not facenorm: # need to calculate it! use cross product or whatever # q,r,s order of vertices is important! q = pmx.verts[pmx.faces[face_id][0]].pos r = pmx.verts[pmx.faces[face_id][1]].pos s = pmx.verts[pmx.faces[face_id][2]].pos # qr, qs order of vertices is critically important! qr = [r[i] - q[i] for i in range(3)] qs = [s[i] - q[i] for i in range(3)] facenorm = core.my_cross_product(qr, qs) # then normalize the fresh normal norm_L = core.my_euclidian_distance(facenorm) try: facenorm = [n / norm_L for n in facenorm] except ZeroDivisionError: # this should never happen in normal cases # however it can happen when the verts are at the same position and therefore their face has zero surface area facenorm = [0, 1, 0] # then save the result so I don't have to do this again facenorm_list[face_id] = facenorm # once I have the perpendicular normal for this face, then accumulate it (will divide later to get avg) for i in range(3): newnorm[i] += facenorm[i] # error case check, theoretically possible for this to happen if there are no connected faces or their normals exactly cancel out if newnorm == [0, 0, 0]: if len(badvert_faces) == 0: # if there are no connected faces, set the normal to 0,1,0 (same handling as PMXE) pmx.verts[badvert_idx].norm = [0, 1, 0] else: # if there are faces that just so happened to perfectly cancel, choose the first face and use its normal pmx.verts[badvert_idx].norm = facenorm_list[badvert_faces[0]] normbad_err += 1 continue # when done accumulating, divide by # to make an average # zerodiv err not possible: if there are no connected faces then it will hit [0,0,0] branch above newnorm = [n / len(badvert_faces) for n in newnorm] # then normalize this, again norm_L = core.my_euclidian_distance(newnorm) newnorm = [n / norm_L for n in newnorm] # finally, apply this new normal pmx.verts[badvert_idx].norm = newnorm return normbad_err
def main(moreinfo=True): # the goal: extract rotation around the "arm" bone local X? axis and transfer it to rotation around the "armtwist" bone local axis # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("") # get bones realbones = pmx.bones twistbone_axes = [] # then, grab the "twist" bones & save their fixed-rotate axes, if they have them # fallback plan: find the arm-to-elbow and elbow-to-wrist unit vectors and use those for i in range(len(jp_twistbones)): r = core.my_list_search(realbones, lambda x: x.name_jp == jp_twistbones[i], getitem=True) if r is None: core.MY_PRINT_FUNC("ERROR1: twist bone '{}'({}) cannot be found model, unable to continue. Ensure they use the correct semistandard names, or edit the script to change the JP names it is looking for.".format(jp_twistbones[i], eng_twistbones[i])) raise RuntimeError() if r.has_fixedaxis: # this bone DOES have fixed-axis enabled! use the unit vector in r[18] twistbone_axes.append(r.fixedaxis) else: # i can infer local axis by angle from arm-to-elbow or elbow-to-wrist start = core.my_list_search(realbones, lambda x: x.name_jp == jp_sourcebones[i], getitem=True) if start is None: core.MY_PRINT_FUNC("ERROR2: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_sourcebones[i]) raise RuntimeError() end = core.my_list_search(realbones, lambda x: x.name_jp == jp_pointat_bones[i], getitem=True) if end is None: core.MY_PRINT_FUNC("ERROR3: semistandard bone '%s' is missing from the model, unable to infer axis of rotation" % jp_pointat_bones[i]) raise RuntimeError() start_pos = start.pos end_pos = end.pos # now have both startpoint and endpoint! find the delta! delta = [b - a for a,b in zip(start_pos, end_pos)] # normalize to length of 1 length = core.my_euclidian_distance(delta) unit = [t / length for t in delta] twistbone_axes.append(unit) # done extracting axes limits from bone CSV, in list "twistbone_axes" core.MY_PRINT_FUNC("...done extracting axis limits from PMX...") ################################################################################### # prompt VMD file name core.MY_PRINT_FUNC("Please enter name of VMD dance input file:") input_filename_vmd = core.MY_FILEPROMPT_FUNC(".vmd") # next, read/use/prune the dance vmd nicelist_in = vmdlib.read_vmd(input_filename_vmd, moreinfo=moreinfo) # sort boneframes into individual lists: one for each [Larm + Lelbow + Rarm + Relbow] and remove them from the master boneframelist # frames for all other bones stay in the master boneframelist all_sourcebone_frames = [] for sourcebone in jp_sourcebones: # partition & writeback temp, nicelist_in.boneframes = core.my_list_partition(nicelist_in.boneframes, lambda x: x.name == sourcebone) # all frames for "sourcebone" get their own sublist here all_sourcebone_frames.append(temp) # verify that there is actually arm/elbow frames to process sourcenumframes = sum([len(x) for x in all_sourcebone_frames]) if sourcenumframes == 0: core.MY_PRINT_FUNC("No arm/elbow bone frames are found in the VMD, nothing for me to do!") core.MY_PRINT_FUNC("Aborting: no files were changed") return None else: core.MY_PRINT_FUNC("...source contains " + str(sourcenumframes) + " arm/elbow bone frames to decompose...") if USE_OVERKEY_BANDAID: # to fix the path that the arms take during interpolation we need to overkey the frames # i.e. create intermediate frames that they should have been passing through already, to FORCE it to take the right path # i'm replacing the interpolation curves with actual frames for sublist in all_sourcebone_frames: newframelist = [] sublist.sort(key=lambda x: x.f) # ensure they are sorted by frame number # for each frame for i in range(1, len(sublist)): this = sublist[i] prev = sublist[i-1] # use interpolation curve i to interpolate from i-1 to i # first: do i need to do anything or are they already close on the timeline? thisframenum = this.f prevframenum = prev.f if (thisframenum - prevframenum) <= OVERKEY_FRAME_SPACING: continue # if they are far enough apart that i need to do something, thisframequat = core.euler_to_quaternion(this.rot) prevframequat = core.euler_to_quaternion(prev.rot) # 3, 7, 11, 15 = r_ax, r_ay, r_bx, r_by bez = core.MyBezier((this.interp[3], this.interp[7]), (this.interp[11], this.interp[15]), resolution=50) # create new frames at these frame numbers, spacing is OVERKEY_FRAME_SPACING for interp_framenum in range(prevframenum + OVERKEY_FRAME_SPACING, thisframenum, OVERKEY_FRAME_SPACING): # calculate the x time percentage from prev frame to this frame x = (interp_framenum - prevframenum) / (thisframenum - prevframenum) # apply the interpolation curve to translate X to Y y = bez.approximate(x) # interpolate from prev to this by amount Y interp_quat = core.my_slerp(prevframequat, thisframequat, y) # begin building the new frame newframe = vmdstruct.VmdBoneFrame( name=this.name, # same name f=interp_framenum, # overwrite frame num pos=list(this.pos), # same pos (but make a copy) rot=list(core.quaternion_to_euler(interp_quat)), # overwrite euler angles phys_off=this.phys_off, # same phys_off interp=list(core.bone_interpolation_default_linear) # overwrite interpolation ) newframelist.append(newframe) # overwrite thisframe interp curve with default too this.interp = list(core.bone_interpolation_default_linear) # overwrite custom interpolation # concat the new frames onto the existing frames for this sublist sublist += newframelist # re-count the number of frames for printing purposes totalnumframes = sum([len(x) for x in all_sourcebone_frames]) overkeyframes = totalnumframes - sourcenumframes if overkeyframes != 0: core.MY_PRINT_FUNC("...overkeying added " + str(overkeyframes) + " arm/elbow bone frames...") core.MY_PRINT_FUNC("...beginning decomposition of " + str(totalnumframes) + " arm/elbow bone frames...") # now i am completely done reading the VMD file and parsing its data! everything has been distilled down to: # all_sourcebone_frames = [Larm, Lelbow, Rarm, Relbow] plus nicelist_in[1] ################################################################################### # begin the actual calculations # output array new_twistbone_frames = [] # progress tracker curr_progress = 0 # for each sourcebone & corresponding twistbone, for (twistbone, axis_orig, sourcebone_frames) in zip(jp_twistbones, twistbone_axes, all_sourcebone_frames): # for each frame of the sourcebone, for frame in sourcebone_frames: # XYZrot = 567 euler quat_in = core.euler_to_quaternion(frame.rot) axis = list(axis_orig) # make a copy to be safe # "swing twist decomposition" # swing = "local" x rotation and nothing else # swing = sourcebone, twist = twistbone (swing, twist) = swing_twist_decompose(quat_in, axis) # modify "frame" in-place # only modify the XYZrot to use new values new_sourcebone_euler = core.quaternion_to_euler(swing) frame.rot = list(new_sourcebone_euler) # create & store new twistbone frame # name=twistbone, framenum=copy, XYZpos=copy, XYZrot=new, phys=copy, interp16=copy new_twistbone_euler = core.quaternion_to_euler(twist) newframe = vmdstruct.VmdBoneFrame( name=twistbone, f=frame.f, pos=list(frame.pos), rot=list(new_twistbone_euler), phys_off=frame.phys_off, interp=list(frame.interp) ) new_twistbone_frames.append(newframe) # print progress updates curr_progress += 1 core.print_progress_oneline(curr_progress / totalnumframes) ###################################################################### # done with calculations! core.MY_PRINT_FUNC("...done with decomposition, now reassembling output...") # attach the list of newly created boneframes, modify the original input for sublist in all_sourcebone_frames: nicelist_in.boneframes += sublist nicelist_in.boneframes += new_twistbone_frames core.MY_PRINT_FUNC("") # write out the VMD output_filename_vmd = "%s_twistbones_for_%s.vmd" % \ (input_filename_vmd[0:-4], core.get_clean_basename(input_filename_pmx)) output_filename_vmd = core.get_unused_file_name(output_filename_vmd) vmdlib.write_vmd(output_filename_vmd, nicelist_in, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
def 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 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 L = core.my_euclidian_distance(v.norm) if L != 0: v.norm = [n / L for n in v.norm] # c, r0, r1 params of every SDEF vertex if v.weighttype == 3: 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 L = core.my_euclidian_distance(b.fixedaxis) if L != 0: b.fixedaxis = [n / L for n in 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 L = core.my_euclidian_distance(b.localaxis_x) if L != 0: b.localaxis_x = [n / L for n in b.localaxis_x] L = core.my_euclidian_distance(b.localaxis_z) if L != 0: b.localaxis_z = [n / L for n in b.localaxis_z] for m in pmx.morphs: # vertex morph and bone morph (only translate, not rotate) if m.morphtype in (1, 2): 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
def normalize(foo): LLL = core.my_euclidian_distance(foo) return [t / LLL for t in foo]