3
$\begingroup$

My apologies if this question doesn't make sense, I'm brand new to animation.

I've been fiddling around with the F-Curves and would like to subdivide two F-Curves to have another keyframe between two curves without affecting the overall shape of the original curve.

For example, here is my curve:

The reason this can't be solved by just keyframing the midpoint (frame 200) is because:

A. The midpoint of the keyframes isn't always the midpoint of the curve, like in this photo:

And B. Even if the middle of the curve is the midpoint of the two keyframes, adding a keyframe changes the curves shape:

Even though I could change the added keyframe to vector, I'd have to manually line it up with the old line, and the process of doing that is too lengthy/not perfectly accurate.

So, how do I subdivide F-curves and end up with the same line I had before, yet another handle in the middle?

$\endgroup$
2
$\begingroup$

Subdividing Bezier curves is normally done using De Casteljau's algorithm. However, fcurves aren't pure Bezier curves. It is not hard to make a bezier curve "run backwards" along the time axis. Since this would create ambiguities in the animation system, blender prevents that by using modified handle coordinates whenever the handles could make the curve backtrack in the time axis. Once we understand that math, we have what is needed to properly subdivide the fcurve.

Here is an excerpt from http://web.purplefrog.com/~thoth/blender/python-cookbook/subdivide-fcurve.html which demonstrates the technique:

def correct_bezpart(p):
    if p[1].x <= p[2].x:
        return p

    scale = (p[3].x - p[0].x) / ( (p[1].x - p[0].x) + (p[3].x - p[2].x) )

    p = list(p)

    p[1] = p[0] + scale * (p[1]-p[0])
    p[2] = p[3] + scale * (p[2]-p[3])

    return p


def interp(v0, t, v1):
    return (1-t)*v0 + t*v1

def de_casteljeu( p , t):
    q = [0,0,0,0]
    r = [0,0,0,0]

    q[0] = p[0]
    r[3] = p[3]

    q[1] = interp(p[0], t, p[1])
    x    = interp(p[1], t, p[2])
    r[2] = interp(p[2], t, p[3])

    #print([ r[2], '= interp(', p[2], t, p[3], ")" ])

    q[2] = interp(q[1], t, x)
    r[1] = interp(x, t, r[2])

    q[3] = r[0] = interp(q[2], t, r[1])

    return (q,r)



def subdivide_fcurve(fc, frame):

    orig_keyframe_count = len(fc.keyframe_points)

    fc.keyframe_points.add(1) # do this before we have any keyframe_points[i] references that would be invalidated
    kp9 = fc.keyframe_points[orig_keyframe_count]

    for i in range(1, orig_keyframe_count):
        kp0 = fc.keyframe_points[i-1]
        kp1 = fc.keyframe_points[i]
        if (kp1.co.x >=frame):
            break

    p = (kp0.co, kp0.handle_right, kp1.handle_left, kp1.co)
    p = correct_bezpart(p)

    t = tForFrame(frame, p[0].x, p[1].x, p[2].x, p[3].x)

    q,r = de_casteljeu(p,t)

    kp0.handle_right_type='FREE'
    kp9.handle_left_type='FREE'
    kp9.handle_right_type='FREE'
    kp1.handle_left_type='FREE'

    kp0.handle_right = q[1]
    kp9.handle_left = q[2]
    kp9.co = q[3]
    kp9.handle_right = r[1]
    kp1.handle_left = r[2]

    fc.update()

#


def bez_root_score(t):
    return max(abs(t.imag), 0-t.real, t.real-1)


def favorite_root(roots):
    """ Since we are confident that at least one of the roots is real, pick the one that has the imaginary component closest to zero
    """
    fav = roots[0]
    score = bez_root_score(fav)

    for i in range(1, len(roots)):
        if bez_root_score(roots[i]) < score:
            fav = roots[i]
            score = bez_root_score(roots[i])
    return fav


def tForFrame(fr, p0, p1, p2, p3):
    """
    fr = (1-t)**3 *p0 + 3*(1-t)**2 *t *p1 + 3*(1-t)* t**2 * p2 + t**3 *p3
    """

    import numpy

    coefficients = [
        -p0+3*p1-3*p2+p3,
        3*p0-6*p1+3*p2,
        -3*p0+3*p1,
        p0-fr
    ]

    roots = numpy.roots(coefficients)
    #print(roots)

    rval = favorite_root(roots).real

    sanity = bez(rval, p0,p1,p2,p3)
    if abs(sanity-fr) >1e-6:
        print(["defective", sanity, fr])
        print(roots)
        print([rval, "for", fr, p0, p1,p2,p3])

    if rval<0 or rval>1:
        print(["wiggy, bez(",rval,p0, p1,p2,p3,") = ",fr])
        print(roots)
        print([rval, "for", fr, p0, p1,p2,p3])
    return rval


def bez(t, p0, p1, p2, p3):
    s = 1-t

    return s*s*s*p0 + 3*s*s*t*p1 + 3*s*t*t*p2 + t*t*t*p3

def mission2(obj):
    create_sample_fcurve(obj)

    subdivide_fcurve(obj.animation_data.action.fcurves[1], 33)


#
#

random.seed=4262

mission2(bpy.context.active_object)
$\endgroup$
1
$\begingroup$

Use the N Panel on the Graph Editor; the third tab down is View, which has input boxes for precision. Cursor X should equal the difference between the two points, divided by 2; Cursor Y should be the difference between the two points, divided by 2.

Cursor Controls in View Panel

No need to remember the numbers, either. The 2D cursor in Graph mode doesn't update for a right click. Select a point, flip back to the F-Curve tab to Copy whichever value you need. Left-clicking the cursor input boxes will highlight the value inside, allowing for a precise calculation. (Value 1 minus Value 2)/2 and hit Enter. If there's an offset from the origin of the graph, that will also need to be re-added after dividing.

Zoom in with the mouse; hold shift, left-click and drag on the input boxes to precision place the cursor, before adding the keyframe with I key.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.