Skip to content

Instantly share code, notes, and snippets.

@lcrs
Last active July 8, 2018 11:54
Show Gist options
  • Save lcrs/9be80473fecad6fd0552fd7701213227 to your computer and use it in GitHub Desktop.
Save lcrs/9be80473fecad6fd0552fd7701213227 to your computer and use it in GitHub Desktop.
# Retime a tracked object to better fit a target curve
# Select two Transform nodes: the first with a jerky tracked curve, the
# second with a smooth target curve
# Group effort by Lewis Saunders, Unai Martínez Barredo, Erwan Leroy
# Make a function to calculate the distance between two points
def distance(a, b):
return pow((a[0]-b[0])**2 + (a[1]-b[1])**2, 0.5)
# Find the closest point to p on the line ab
# Also returns the weight of that point along ab
# https://stackoverflow.com/questions/3120357/get-closest-point-to-a-line
def interp(a, b, p):
if a == b:
return None
ap = (p[0]-a[0], p[1]-a[1])
ab = (b[0]-a[0], b[1]-a[1])
w = min(1, max(0, (ap[0]*ab[0] + ap[1]*ab[1]) / (ab[0]**2 + ab[1]**2)))
return a[0]+ab[0]*w, a[1]+ab[1]*w, w
# Here we store the first node in your selection (the latest node you
# selected) as 'jerky', so that it can be accessed later in the code more
# easily
jerky = nuke.selectedNodes()[1]
# Same for the second node in your selection, but we call it smooth. That
# would be the second last node you selected
smooth = nuke.selectedNodes()[0]
# Find the first and last frame of your comp and the length in frames
first = int(nuke.Root()['first_frame'].value())
last = int(nuke.Root()['last_frame'].value())
clen = last - first + 1
crange = range(clen)
# We make two new empty list variables
jerkyc = []
smoothc = []
# Now for each frame (that we arbitrarily decided to call i) in the range, we
# look at the value of the curves, and add them to the lists defined above
for i in crange:
j = jerky['translate'].valueAt(first + i)
s = smooth['translate'].valueAt(first + i)
jerkyc.append(j)
smoothc.append(s)
# Create an oflow node
oflow = nuke.createNode('OFlow2', 'timing2 Frame')
# Set the frame knob animated
oflow['timingFrame2'].setAnimated()
# Go through each frame in the range again...
for i in crange:
# Take the original list of the jerky values and sort them by
# closeness to the value of the smooth curve, then keep the index of
# the closest one
closest = sorted(crange, key=lambda x: distance(jerkyc[x], smoothc[i]))[0]
before = None
after = None
# If there are points on the jerky curve before or after the closest one,
# check if there's a closer match on the lines connecting the closest point
# to them
if closest > 0:
before = interp(jerkyc[closest], jerkyc[closest-1], smoothc[i])
if closest < clen-1:
after = interp(jerkyc[closest], jerkyc[closest+1], smoothc[i])
# If there were points in both directions, figure out which one matched
# best, and the offset from the closest frame
if before != None and after != None:
befored = distance(before, smoothc[i])
afterd = distance(after, smoothc[i])
offset = (-before[2], after[2])[(befored, afterd).index(min(befored, afterd))]
elif before:
offset = -before[2]
else:
offset = after[2]
# Set the value of the timing curve on this frame
oflow['timingFrame2'].setValueAt(first+closest+offset, first+i)
@UnaiM
Copy link

UnaiM commented Jul 7, 2018

This is fantastic! 😃 I think there are two possible scenarios where this can yield false positives or undesired results:

  • It might find a closest frame too far away in time from the current one, while it might be more desirable to use a frame that’s a bit less spot-on in distance but closer in time. This could help if the movement is somewhat cyclical, or the object goes through the same spot at different times.
  • Similarly (and probably more importantly), it might be the case that nextclosest isn’t adjacent to closest, and thus when doing the weighted average, it ends up returning a frame that has nothing to do with what we want.

I’m going to try and implement solutions for this.

@UnaiM
Copy link

UnaiM commented Jul 8, 2018

Brilliant Lewis! I had no idea selectedNodes() worked in reverse, that’s weird considering lists in Python perform very badly when inserted to their beginning compared to appending at the end. And I should have noticed the wrong order of arguments in setValueAt before— it was showing me random gaps between blue keys in the viewport timeline and I didn’t know why, so I assumed it was a bug…

The only thing I’m spotting now is that we’re calling selectedNodes() and Root() twice, but I don’t know if optimising that will yield a huge difference— since I’m on Non-Commercial, I would assume it would push back the 10 node read limit though. So I’m going to change that 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment