예제 #1
0
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
예제 #2
0
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
예제 #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")
    # 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
예제 #4
0
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
예제 #5
0
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