Best way to have a walk cycle on a path with these requirements?

I have a Rigify character performing a walk cycle. I’ve been able to determine exactly how many Blender units the character’s root bone has to walk on the Y axis for a certain number of frames so that the character’s feet don’t slide (e.g. “for every 13 frames, move the root bone forward 0.25 Blender units”). I’ll call that value the “exact forward amount”. Using the “exact forward amount”, I can make the character walk in a straight line on the ground along the Y axis perfectly – the walk looks exactly right and there’s no floatiness whatsoever.

Now I want to make this character walk along a path. I’ve watched several Blender YouTube tutorials (some with what appears to be really terrible advice) and am not sure of the best approach to make the character walk along a path with these particular requirements:

  • I do not want to use the NLA editor. I want to use the Dope Sheet and copy/paste my walk cycle (and tweak it) as needed.

  • I want to move the root bone of the armature, not the entire armature object. In other words, I want to leave the armature object itself at 0,0,0 while the root bone moves along the path object.

  • I want to use the “exact forward amount” value to move the character along the path a certain amount between two frames. I do NOT want to have to estimate a value between 0 and 1 indicating the relative location of where the character is along a path object.

  • I do not want to start walking on frame 1. I want to control when I make the character start and stop walking on the path.

  • Bonus, but not necessary requirement: I would love to be able to have a character walk along a path, then leave the path (perhaps not following on a path at all), then start following a different path object when needed. This is a pie-in-the-sky sort of wish, though. I don’t know if this is feasible or relatively painless to do.

Any suggestions?

You can’t accurately measure a distance on a curve when the curve isn’t straight.

Good luck!

That is not quite correct. The Blender Python API provides a function to calculate the length of a path/curve. But because it is a function and not a property I haven’t found an easy way to access it inside a driver. But you can use AnimationNodes to access the length.

If you can access the length value through the API, then you can calculate the time it takes to traverse the curve. If you know how long it is and you know how much time each step takes for your character, it should be a simple matter.

Can’t you access the length at initiation, then store the value in a property and access that through the driver?

Thanks, guys. I figured it out given the ideas upthread as well as using some code from MacKracken’s Arc Length.

I wrote a Python script (pretty rough, but it works) where I set the name of the curve object, the distance for a full stride, how many frames it takes to go a full stride, the start frame, and the desired end frame. Then it’ll print out the values needed for the Follow Path constraint’s Offset parameter. If I put in too many frames and the calculations happened to overshoot the distance on the path (so much that the Offset value is greater than 1), then the script will show a different end frame where we are as close to under 1 as possible.

I’ve tested this and my character walks perfectly along the path – no sliding at all. (EDIT: Well, maybe not “no sliding”…it looks like I’ll need to compensate a bit on a steep curve up or down…and presumably on a hard turn. But for relatively straight paths, it appears to work well.) It’s exactly what I was looking to do. As I thought about it, I could make the character follow different paths simply by reducing the influence of the Follow Path constraint as needed. (Of course, I would have to re-position the character so it would look seamless, but it would be possible. I may not end up doing this, though…it’s probably more hassle than it’s worth.)

I plan to improve this script, where it’ll automatically insert the keyframes in the Follow Path constraint (as well as provide an option to not start at the very beginning of the path), but this is sufficient for now.

Here’s the code in case if anyone finds it useful. Note that this expects to be run from Blender 2.8’s Scripting workspace, where a Python Console window is present.

from console_python import get_console
from contextlib import redirect_stdout
from mathutils import *
from bpy.props import *
import bpy
import math

###############################################################################
# Calculate Path Offset Value
#
# Figure out the Follow Path constraint's Offset value that we need in order to
# use a walk cycle along a path.

PATH_NAME = 'NurbsPath'                     # The name of the path we want to walk
FULL_STRIDE = 0.25                          # How far is our full stride in Blender units?
FRAMES_TO_GO_FULL_STRIDE = 13               # How many frames do we need to go per one full stride?
FRAME_START = 101                           # When do we start walking on the path?
FRAME_END = 520                             # When do we stop walking on the path?


###############################################################################
# Version History
#
# 1.0.0 - 2020-08-03: Original version
#

###############################################################################
SCRIPT_NAME = "calculate_path_offset_value"
###############################################################################
    
def add_scrollback(override, text, text_type): # Necessary for printing to the console
    append = bpy.ops.console.scrollback_append
    
    for l in text.split("\n"):
	txt = l.replace("\t", "    ")
	append(override, text=txt, type=text_type)

def print_msg(text): # Necessary for printing to the console
    con = stdout = stderr = None
    for area in bpy.context.screen.areas:    
	if area.type == 'CONSOLE':
	    hsh = hash(area.regions[-1])
	    con, stdout, stderr = get_console(hsh)
	    break

    stdout.seek(0)
    stdout.truncate(0)

    with redirect_stdout(stdout):
	con.push('print("' + text + '")')

    out = stdout.getvalue()
    if out:
	override = {'area': area, 'region': area.regions[-1], 'space_data': area.spaces.active}
	add_scrollback(override, out, 'OUTPUT')

#calcs a nurbs knot vector
def knots(n, order, type=0):#0 uniform 1 endpoints 2 bezier
    kv = []    
    t = n+order
    if type==0:
	for i in range(0, t):
	    kv.append(1.0*i)

    elif type==1:
	k=0.0
	for i in range(1, t+1):
	    kv.append(k)
	    if i>=order and i<=n:
		k+=1.0
    elif type==2:
	if order==4:
	    k=0.34
	    for a in range(0,t):
		if a>=order and a<=n: k+=0.5
		kv.append(floor(k))
		k+=1.0/3.0
    
	elif order==3:
	    k=0.6
	    for a in range(0, t):
		if a >=order and a<=n: k+=0.5
		kv.append(floor(k))	
    
    ##normalize the knot vector
    for i in range(0, len(kv)):
	kv[i]=kv[i]/kv[-1]
	
    return kv

#nurbs curve evaluation
def C(t, order, points, weights, knots):
    #c = Point([0,0,0])
    c = Vector()
    rational = 0
    i = 0
    while i < len(points):
	b = B(i, order, t, knots)
	p = points[i] * (b * weights[i])
	c = c + p
	rational = rational + b*weights[i]
	i = i + 1
	
    return c * (1.0/rational)

#nurbs basis function
def B(i,k,t,knots):
    ret = 0
    if k>0:
	n1 = (t-knots[i])*B(i,k-1,t,knots)
	d1 = knots[i+k] - knots[i]
	n2 = (knots[i+k+1] - t) * B(i+1,k-1,t,knots)
	d2 = knots[i+k+1] - knots[i+1]
	if d1 > 0.0001 or d1 < -0.0001:
	    a = n1 / d1
	else:
	    a = 0
	if d2 > 0.0001 or d2 < -0.0001:
	    b = n2 / d2
	else:
	    b = 0
	ret = a + b
	#print "B i = %d, k = %d, ret = %g, a = %g, b = %g\n"%(i,k,ret,a,b)
    else:
	if knots[i] <= t and t <= knots[i+1]:
	    ret = 1
	else:
	    ret = 0
    return ret

#gets nurbs polygon control points on global coordinates
def getnurbspoints(spl, mw):
    pts = []
    ws = []
    for p in spl.points:
	v = Vector(p.co[0:3]) @ mw
	pts.append(v)
	ws.append(p.weight)
    return pts , ws

#calculates a global parameter t along all control points
#t=0 begining of the curve
#t=1 ending of the curve
def calct(obj, t):    
    spl=None
    mw = obj.matrix_world
    if obj.data.splines.active==None:
	if len(obj.data.splines)>0:
	    spl=obj.data.splines[0]
    else:
	spl = obj.data.splines.active
    
    if spl==None:
	return False
    
    if spl.type=="BEZIER":
	points = spl.bezier_points
	nsegs = len(points)-1
	
	d = 1.0/nsegs
	seg = int(t/d)
	t1 = t/d - int(t/d)
	
	if t==1:
	    seg-=1
	    t1 = 1.0
	
	p = getbezpoints(spl,mw, seg)
	
	coord = cubic(p, t1)
	
	return coord

    elif spl.type=="NURBS":
	data = getnurbspoints(spl, mw)
	pts = data[0]
	ws = data[1]
	order = spl.order_u
	n = len(pts)
	ctype = spl.use_endpoint_u
	kv = knots(n, order, ctype)
	
	coord = C(t, order-1, pts, ws, kv)
	
	return coord


def get_path_length(obj):
    length = 0.0
    
    if obj.type=="CURVE":
	prec = 1000 #precision
	inc = 1/prec #increments
	
	### TODO: set a custom precision value depending the number of curve points
	#that way it can gain on accuracy in less operations.
	
	#subdivide the curve in 1000 lines and sum its magnitudes
	for i in range(0, prec):
	    ti = i*inc 
	    tf = (i+1)*inc 
	    a = calct(obj, ti) 
	    b = calct(obj, tf)
	    r = (b-a).magnitude 
	    length+=r
	
    return length

    
###############################################################################
# Main program
###############################################################################

print_msg('**********************************')
print_msg(SCRIPT_NAME + ' - START')

path_length = get_path_length(bpy.data.objects[PATH_NAME])
stride_length = FULL_STRIDE / path_length
total_frames = (FRAME_END - FRAME_START) 
end_value = (total_frames / FRAMES_TO_GO_FULL_STRIDE) * stride_length

print_msg('  ----- RESULTS -----')
print_msg('    ' + str(PATH_NAME) + ' has a length of ' + str(path_length))
print_msg('    One full stride length for a Follow Path constraint Offset: ' + str(stride_length))

if (end_value > 1.0):
    print_msg('    *** WARNING! INPUT PARAMETERS OVERSHOOT THE OFFSET VALUE! ***')
    print_msg('    *** We are going to modify which frame where the animation ends so that we can end with a useable Offset value. ***')
    while (end_value > 1.0):
	end_value -= stride_length / FRAMES_TO_GO_FULL_STRIDE
	FRAME_END -= 1

print_msg('    FRAME ' + str(FRAME_START) + ' Offset: 0')
print_msg('    FRAME ' + str(FRAME_END) + ' Offset: ' + str(end_value))
    
print_msg('  -------------------')
print_msg(SCRIPT_NAME + ' - END')
print_msg('**********************************')
print_msg('Done running script ' + SCRIPT_NAME)

How about making a demo to show us how it looks? You might have solved a problem for a lot of people.