retiming is an essential headache.
retiming using an expression.
The expression used to retime an animated knob depends on the node that’s doing the retiming. Different retime nodes have timing knobs with different names which must be accounted for in the expression. It’s quite simple to set up these expressions. First click on the knob in the node that you need retimed and press “=” to begin editing that knob’s expression. Next, type the expression which will vary depending on the type of retime node that you’re using. This may be confusing for those who aren’t comfortable with Nuke expressions so I’ve listed what the retiming expression is for a handful of Nuke nodes.
This one is pretty straightforward, the knob we’re looking for is the “first_frame” knob in the FrameHold node. This knob tells Nuke which frame we’re going to be freezing on and as such adding this to any animated knob, will freeze it’s animation on the designated frame. This is useful for setting up camera projections without having to clone or copy/paste multiple FrameHolds.
TimeOffset will be offsetting the values of the animation by a specified value. Here, we need to grab the current frame using “frame” and add the offset value from the TimeOffset node. The output would be the same as if you placed a TimeOffset node downstream of the node with the animation.
TimeWarp, Kronos, and OFlow
The TimeWarp, Kronos, and OFlow nodes all use a similar syntactic method to each other, but require different expressions based on the timing method selected in the node. For these expressions, Nuke will be looking at the frame number specified by the TimeWarp, Kronos, or OFlow, and use the input value at the specified frame. If the frame is non-integer, Nuke will interpolate the timing curve and create a new value. In short, Nuke will look for the retime node’s “timing lookup” knob and grab the value from the curve at the frame specified by the lookup.
It gets slightly more involved with OFlow and Kronos because these nodes have multiple methods of retiming their inputs. The expression used to retime an animation from a Kronos or OFlow node will depend on the value of the “Timing” knob in the OFlow or Kronos.
the meat of the code.
This script isn’t the most complicated piece of python (although it was much more complicated than I had expected, more on that later) in short, we look at the user’s current selection of nodes, pick out the node doing the retiming, then loop through the rest of the nodes looking for animated knobs and setting an expression on that knob based on the selected retime node
The primary method of the script
We start in the retimeKeyframes() function which looks at the users current selection and does some validation checks to see that the user has something selected and that the first node they selected is a node that can do some retiming (framehold, kronos, oflow, timeoffset, timewarp). We then call the getAnimatedNodes() function which loops through the selection and picks out any nodes that have some animated knob on them. This function looks at every knob in each node in the users selection and checks for two conditionals. First, the knob that we're looking at has to be the type of knob to be retimed, we’re mainly looking for knobs that store numerical values. Secondly, we need to see if that knob is animated using the knob.isAnimated() function which will return True if that knob has some keyframes on it. If both of those conditions are true, we’ll add that node to a list and return that list.
Getting the animated nodes
Next, we’ll loop through each node from the list we gathered and set the expression on the animated knobs. In this loop, we’ll look at each knob in the node and see if that knob has some animation on it. If so, we’ll have to then loop through each curve in that knob (what I call sub-knobs). We can do this with the built in method node[knob].animations(). This method returns a list of all the sub-knobs that have keyframe animation attached to them. For example, a transform node has a knob called ‘translate’. This translate knob is what Nuke calls an ‘XY_knob’ which contains two fields, one for the x value and one for the y value. If you tried to use knob.setExpression() on the XY_knob itself, Nuke will have no problem with this and will set the expression on BOTH the x and the y sub-knobs. This usually won’t cause any issues however, if the user has animation they retimed on one of the XY knobs while the other knob has no animation and is a value other than 0, the added retime expression will cause knob with a non-zero value and no animation to be the output of the expression multiplied by that value. This is no good! The user would expect their constant, non-animated value to remain consistently not animated. Here’s an example, let’s say we have a transform node with the x field on the translate value set to 50 and the y field has some keyframed animation. If we apply the expression (Kronos1.timingFrame2) to both knobs, the x field will output the value of 50*Kronos1.timingFrame2, which would be an unexpected output. That x value would then become animated based on the Kronos even though it had no animation to start with. To address this issue, we need to call setExpression() on the sub-knob and not the parent knob. That’s why this function loops through the sub-knobs as well.
Setting the retime expression
Finally, we use a function called getRetimeExpression() which returns the correct expression for the knob based on the node that’s doing the retiming. As stated in the prior section, different retime nodes use different expressions to apply their retiming. This function simply checks the knob class and some values of knobs within the retime node, and returns an expression to do the retiming.
Creating the retime expression
where the code falls short.
The biggest blind spot of this code is that it will only work if the knob that’s getting retimed is void of any expression other than “curve”. If the knob to be retimed has some other user created expression, this script will override that expression. I took some time to try and fix this, but I never found a solid solution and came to the conclusion that it would be a rare occurrence that this would happen anyways. A “work around” would just be to retime the node that’s driving the expression in the first place. If someone runs into a situation where this design choice becomes a hindrance, I’d love to hear about it and work out a better solution.
A potential second blind spot is that if the user is using a Kronos or OFlow to do the retiming, they might want to change the timing method after running this script. If the user changes the timing method, for example, from “frame” to “output speed” after this script has applied the expressions, the expressions won’t update to accommodate that change. A potential solution to this problem might be to update the getRetimeExpression() function to output a TCL conditional that checks the parent retime node for which timing method is being used, and update the timing expression to match.
problems I encountered.
While working on this project I ran into quite a few roadblocks that forced me to rethink my approach. Overall, the majority of the issues I encountered were completely self inflicted, many of the technical and syntactical aspects of the project were simple enough to figure out with an internet search or through doing my own testing. I started to step on my toes when I went diving into dictionaries to store all the node/knob/sub-knob relationships. I had a dictionary nested in a dictionary which was nested in yet another dictionary. The for-loops required to parse this complex list was a headache to wrap my head around. This dictionary issue stemmed from me trying to execute too many tasks in one step. I should have tried the simplest method first instead of over-engineering a solution in my head.
I got first draft of the script working for a single node, I then wanted to extrapolate the script to work on a group of selected nodes. Instead of taking my solution that worked for one node and applying to a list of all the nodes, I went about gathering a huge nested list of all nodes, knobs, and sub-knobs and then tried to parse through this complicated list. In my finished script, I scrapped the dictionaries entirely and applied my single node solution to a list of only the required nodes. This made my code so much easier to read and understand and debug later down the line.
know it for next time.
On the next Python project that I take on, I’ll need to keep in mind that a node’s knobs are not simply a one dimensional object; knobs are made of sub-knobs and I need to structure my code to work for the sub-knobs first. Furthermore, I’m glad I learned how to leverage node.animation() and node.animations() to quickly get the keyframe information I required. Understanding how these functions worked marked a dramatic turning point in how I was approaching this problem.
I also need to keep an eye out for “smelly code” which are sections of code that could be written more elegantly or provide more flexibility. I came across a series of videos on YouTube by Derek Banas on refactoring code that was eye opening to me. At the time of watching this series, my code was a jumbled stinking mess that was so complex, I could barely understand it. After watching a few of these videos, I realised how much I could improve the functionality and readability of my script. If you’re getting off the ground with scripting I’d highly recommend taking a look at this video series.
I see this script being put to use on a shot-by-shot basis, rather than being a staple program loaded on board a complex pipeline. It might only get used a handful of times on a given project yet, it has the ability to save that one poor artist (me) a bit of time when dealing with shots containing temporal manipulations. While this script might not be a groundbreaking change to visual effects pipelines, it is very easy to implement and can be added to any compositor's toolbox in a flash.
Thanks so much for taking time to read through this summary. I urge any experienced programmers to delve into my script and point out any potential weak points and let me know how I could improve upon this to make the script bulletproof. I’m always looking to improve upon my programming skills and your input would be most appreciated.
This script requires minimal setup, at its simplest you can copy/paste the script into Nuke's script editor and make sure you call the function by typing, on a new line, retimeKeyframes(). Then, select the nodes you want retimed, then highlight the script and press ctrl+enter to execute the script.
If you choose to pre-load the script from .nuke, I'd recommend setting up a Python folder, loading the folder from init.py and adding the script to a toolbar in the .nuke. The GitHub link provided is set up as such.
You can find download links to this script on GitHub: