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)
1 Like

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

Hello @simplecarnival
Thanks for posting the code.

Been playing with your script and have a question.
How are you getting the “Blender Units” accurately (and by blender units, you mean the gird squares, right) ?

In case other (new scripters) try this, i hade to rework the indentations thorugh out the code, kept erroring out… just alittle time invested, no big deal.

How are you getting the “Blender Units” accurately (and by blender units, you mean the gird squares, right) ?

If you’re asking about FULL_STRIDE, that’s easy. Assuming your Rigify’s character’s leg’s position starts at 0,0,0, move the character’s leg forward a comfortable distance in its local Y direction. The new Y value is your half stride value. Multiply that by two, and that’s how you get the FULL_STRIDE value.

1 Like

Yup, FULL-STRIDE, got it
And a “Blender Unit” is what ever we have our units set to, imperial or metric, correct ?

18" inch
Or
441 mm

Hi @SidewaysUpJoe

“Blender Units” means the value when you set the Scene Properties tab -> Units -> Unit System dropdown to “None”. (In pre-2.8 versions of Blender it used to be called “Blender Units”. Now it’s just called “None”.)

My code doesn’t do any manipulation of the unit type – it just expects to see the value in the same unit system that Blender works with internally.

If you want to feed in Metric or Imperial values for FULL_STRIDE, then you’ll have to change to code to support it.

Hope this helps!

1 Like

Also, just saw this the other day.

1 Like

I’m glad you got this all figured out.

I believe there’s a scriptless solution, if you or anyone else is still interested.

  1. Make a mesh. Disable “display in renders”. Make an empty and vertex triangle parent* it to this mesh. Parent the mesh to your path with “curve deform.”

  2. Translate your empty’s loc/rot to your root bone via a bone constraint (couple of options, I’d probably just go with “child of”.)

  3. To move the root bone a fixed distance along the curve, move the non-rendering mesh object a fixed distance in the X axis.

  4. To stop and follow a different path, create a new mesh object + empty for your new path as above. Give your root bone constraints to follow this other path. Animate influence and use “apply as visual transform” to transition.

*edit: vertex parenting can create skew. Probably not noticeable on reasonable paths and vertex parents, but any amount can be a problem. I would probably copy location, damped track, locked track vertex groups instead, although there are probably other constraints that could do it, that I don’t trust as much as those three.

1 Like

Thanks @a59303 and @bandages – that is a good approach/idea. I am totally fine with NOT using the Offset Factor and instead using a method like what you’ve posted.

@bandages – I have your solution working with doing the vertex parenting in step 1. I’d like to use your suggestion of using Copy Location, Damped Track, or Locked Track instead of vertex parenting. However, unless I’m doing something wrong, it doesn’t seem that any of these modifiers will attach an Empty to a mesh object that is moving with a Curve modifier. The best that I can do is use a Copy Location constraint on the Empty object…which makes the Empty move in a straight line while the triangle mesh moves along the curve. The two objects do not stay together in the way that an Empty that is parented to a triangle mesh object (moving along a curve) does.

If vertex parenting is my only option here, that’s OK, but I’d like to try your recommendation if I can make it work somehow. Any suggestions?

Mark target verts with vertex groups, and specify those groups in the constraint.

You don’t want to be constraining to object locations, which don’t follow the curve, but to vertex locations, which do.

Hmmm…I think I’m doing something wrong. The Empty still moves along the path that the walker object would have moved if the walker object didn’t have the Curve modifier:

I wasn’t sure what to set the vertex groups to – I tried a few different things, but they didn’t seem to make any difference. So I just created one vertex group that had the forward-most vertex of the triangle and called it “z”:

Here’s the .blend file. Any idea what’s wrong? FWIW, I’m using the new Blender 2.90.1 release.
walk test 08 vertex group for blenderartists.blend (654.1 KB)

What’s wrong is that your damped track and your locked track are undefined, because you’re tracking the same position that you’re at. Same as if you were to use a bone to track a coincident bone location.

Or, maybe, because you didn’t bother to assign any vertices to those groups.

What I’m really doing is using my mesh to create a set of basis vectors for the empty: one vertex group represents where the origin of the empty should be, one vertex group represents where the Z axis should point to, and one group represents where the Y axis should point to. (The X axis is then implicitly defined by virtue of being orthogonal to both Z and Y.)

Vertex group assignments don’t show up well on pics, so this file might help:

basisvecs.blend (108.0 KB)

Edit: looked at file, yeah, you don’t have any actual vertices assigned to the group. At least, if loading a 2.91 file in 2.9 is valid.

1 Like

Your .blend file was very helpful – I was able to get it working based on your file. (And yes, I forgot to add vertices to my vertex groups :flushed:)

Thanks, @bandages!

@bandages – I now have everything working as a proof of concept. Specifically, I have it working with two curves, so my character can follow one curve and then (after animating the rig’s root bone Child Of constraints’ influence values) start walking on the other path. The only thing I don’t quite understand is your “apply as visual transform” suggestion:

Let’s say you have two constraints on the armature root bone: Child Of A and Child Of B. Child Of A applies to the walker on Curve A, and Child Of B applies to the walker on Curve B. When the character is walking on Curve A, Child Of A’s Influence parameter is set to 1 and Child Of B’s Influence parameter is set to 0.

When you get to the end of Curve A, you’re going to want to transition between the end of Curve A and the beginning of Curve B. If Curve B starts at essentially the same point as where Curve A ends, then it’s easy – on the last frame where you need to be on Curve A, have Child Of A’s Influence set to 1 and Child of B’s influence set to 0. Then on the next frame, have Child of A’s Influence set to 0 and Child of B’s influence set to 1. I understand and can implement that scenario.

What I’m unclear on is exactly how “Apply Visual Transform” works – especially in the context of your suggestion. I’ve searched BlenderArtists and looked at Blender’s manual and I’m still not clear how this would work. Could you please provide a little more detail? Thanks!

“Apply as visual transform” is a cool action that writes your current constrained transform. It is almost always followed by disabling constraints, and usually there’s a brief period where everything is all wrong between applying visual transform and disabling constraints.

So let’s say it’s frame 10, the last frame we want to follow the first path. You have two child of’s, one at 1.0 and one at 0.0. How do we get rid of the child of and keep our current position? Go to frame 11, same influences. Apply visual transform, then set influence to 0.0, 0.0, then keyframe locrotscale and influences.

If you have constant motion, this can leave you with a bit of a blip, because you’ll have two duplicated frames with the same location. The easiest thing to do is to turn off snapping and move the keyframes 0.9 back in the X axis, or 0.99 back, or 0.999 back-- whatever it takes to look good enough. (If it has to be perfect, yes, lol, there’s a solution to that, but it never has to be perfect.)

So now at frame 10, we’re following the path, and at frame 10.1 we’re off the path, right? The easiest way to get on the new path is to just set the inverse. (Hopefully you don’t want to have multiple relationships with a single path.) So go to frame 11, set influence of the second constraint to 1.0, then clear and set inverse on the constraint. Keyframe location and influence.

Again, to avoid the blip, you’ll probably want to move the keyframes back, let’s say, from 11.0 to 10.2.

So now we’re child of the first empty at frame 10, child of nothing but at the same location at frame 10.1, and child of the second empty, but still at the exact same location, at frame 10.2.

Does that make sense?

Note that there’s also visual keyframing. Presumably, visual keyframing does the same thing as applying visual transform and then keyframing. But I’ve had some issues with it in the past, where it didn’t handle some particular situation properly, so I always apply transform and keyframe manually.

Obviously, you don’t want future keyframes changing your loc (or influence) on the frame advances. If you do have those, copy your initial keyframes to someplace in the near future temporarily to hold the values.

Note also that if you scale this up at all, to slow down the animation, you’re going to see wonky inbetween frames. “Constant” interpolation might help with that, but I’m not sure, I don’t bother. It’s best to know the frame rate you want to target ahead of time. (But you can always move the important keyframes to smaller intervals.) Likewise, it might screw up something like motion blur.

1 Like

Yes, it does – I got this to work. Thanks for the detailed clarification, @bandages!