Last active
July 8, 2018 11:54
-
-
Save lcrs/9be80473fecad6fd0552fd7701213227 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
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
This is fantastic! 😃 I think there are two possible scenarios where this can yield false positives or undesired results:
nextclosest
isn’t adjacent toclosest
, 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.