The Lazy Compositor – Switch It Up!

I’m a lazy compositor and I don’t like keyframes.

If a bunch of keyframes and I were at a bus stop waiting for the same bus, when it arrives, I’d let them go on and wait for the next one.

Unfortunately, we ended on the same bus while working on the The Mandalorian. On that show, there were lot of blasters being fired, and they had to be animated frame by frame to match a target look.

We achieved this by using a master controller to tweak each parameter of the blaster template.

Setting keys on this thing was unpleasant, especially when it was required to change parameters each frame. You know, set this parameter to 1 at this frame, and set its neighboring frames to 0 to prevent interpolation.

Multiply this with 10 parameters and you quickly find yourself with a mess of keyframes that are difficult to change and extremely error prone as the keys are so interwoven.

I don’t know about you, but I also die a little inside when I’m doing this.

All this manual work makes me a sad compositor.

And because I am a lazy compositor, probably the laziest, I searched the internet for a better way.

And I found nothing.

However, my determination to be as lazy as possible was overwhelming.

So, how solve this problem without keyframes?

Expressions, of course!

I duplicated the master controller for each frame that needed adjustments and switched between them with this expression:

input1.input.label == frame

Like this:

BEEP, BOOP, COMPUTER COMPUTE!

This expression first asks the switch nodes for their second inputs, which are the “MASTER_CTRL” nodes. Because computers are dumb like me and start counting at 0, so that’d be “input1” instead of “input0” ( Instead of using “input0”, we can just use “input” and Nuke is smart enough to figure out what we mean, but it doesn’t hurt to be specific! ).

Next, we’ll be placing a dot node in between each switch node and the “MASTER_CTRL” node, so let’s make sure our expression takes that extra node into consideration by adding “.input” to look one node further upstream. So now the expression looks like this:

input1.input

We now have arrived at the “MASTER_CTRL” nodes, and from here, we simply use a equivalence Binary Operator to test if the “MASTER_CTRL“‘s label is equal to the current frame.

input1.input.label == frame

If the label is equal to the frame, the switch will be set to 1.

If the label is not equal to the frame, the switch will be set to 0.

So now, for each frame I’d only have to copy and paste the switch and “MASTER_CTRL” nodes and hook up their inputs and change the label to the desired frame.

Which turned out to be way too much work for me …

I’m too lazy for this!

I needed a better way!

Then I thought about another technique I was using to turn on lens flares on specific frames.

It goes something like this:

On a switch node, we create a text input knob and named it “frames“.

We’ll later enter frame numbers into this knob to drive our switch.

In the “which” knob, we write a Boolean expression with in Python:

str(nuke.frame()) in re.split(‘[^0-9]+’,nuke.thisNode()[‘frames’].getValue())

Don’t forget to press the Python button when entering the expression!

NOTE: This expression uses Python’s Regular Expression library, which might not be imported by default in your flavor of Nuke, so make sure to import it if you want to try this! Import it by typing in the script editor “import re” and hitting CTRL + ENTER.

Alright, let’s see what this Python snippet is doing.

First, it asks Nuke what frame we’re on with “nuke.frame()“. The result Nuke returns is an integer, which we’ll want to convert to text ( or string in computer talk ) to later compare with the values in “frames” text input knob. To do that, we call the Python’s convert to string function.

str(nuke.frame())

Next, we use regular expressions or ReGex, which is a way to search for specific patterns in text, to find all valid frame numbers in the “frames” knob.

To better illustrate what’s going on, let’s go ahead and put some frames in now.

Enter some frames to test our Boolean expression.

Let’s break down the ReGex half of our initial expression:

re.split(‘[^0-9]+’,nuke.thisNode()[‘frames’].getValue())

First, we need to read the values in the “frames” knob on our switch node with:

nuke.thisNode()[‘frames’].getValue()

nuke.thisNode()” tells Nuke that we want to operate on the current node itself, and “[‘frames’]” tells Nuke that we want to access the “frames” knob we created, and finally, what do we want to do with the “frames” knob? Well, we want to get its value of course, thus the “getValue()” command is called.

Cool, we now have the value of the “frames” knob, which is just a long string:

“1, 10, 20, 30, 40, 50”

What we want to do next is to compare each numerical value in the string with the current frame. We can’t do it with the string, we’d need to process it further to take out all the empty spaces, commas, and store the digits in a list!

To achieve this, we run the ReGex split command on our string:

re.split(‘[^0-9]+’ , nuke.thisNode()[‘frames’].getValue())

The re.split() function takes two parameters here, a search pattern (‘[^0-9]+’) and the string to perform the search on, and will split the string at any instance where it matched with the search pattern. In the end it will give us back a list with all the split out values.

The search pattern I used was:

‘[^0-9]+’

It looks cryptic yes, but it helps us to find any non-digit characters, and allows for repeating characters, which means we can use any separator character when typing out our frames.

The result a neat list:

[’10’, ’20’, ’30’, ’40’, ’50’]

Finally, we test to see if the current frame we got earlier is in this list:

str(nuke.frame()) in re.split(‘[^0-9]+’,nuke.thisNode()[‘frames’].getValue())

We can see the expression working now:

Cool!
Works with any separator character!

Ok, now that we saw what a little bit of Python can do, let’s see how I was able to come up with the laziest solution to my original problem.

This article is so long and boring that you probably forgot what the original problem was, so to remind you:

I AM TOO LAZY TO SET UP A SWITCH EACH TIME I WANT TO TOGGLE TO A DIFFERENT FRAME, SO I WANT ONE SWITCH TO DO ALL THE WORK!

Like this:

The Switch knows which input to switch to based on the frame!

The design I came up was to have a switch that reads the label from its inputs and automatically switches to the appropriate input when the frame is the same as the label.

And here’s the code that did just that:

[nuke.thisNode().input(i)[‘label’].getValue() for i in range(nuke.thisNode().inputs())].index(str(nuke.frame()))

This is a bit more complicated and cryptic because we can’t achieve this result with a simple Boolean expression like the last example.

But I firmly believe the longer the expression the more lazy we get to be. And that’s a good thing!

Let’s unpack, starting with the first part:

[nuke.thisNode().input(i)[‘label’].getValue() for i in range(nuke.thisNode().inputs())]

This is called a list comprehension in Python is really just a for loop written in one line. It might help to read it like this:

[ doThis for eachItem in thisList ]

List comprehensions give us back a list of values.

With the knowledge we gathered from the previous examples, we can make a decent guess that this list comprehension is trying to create a list with all the labels for all the upstream nodes for the current node.

And the result in our example:

[”, ‘1’, ’10’, ’20’, ’30’, ’40’, ’50’]

You can see that the first value in the list is an empty string, and that’s because we’re using a NoOp as our first input. We can use this as a default input for the switch when there are no valid frames.

Let’s examine the second part of the code:

.index(str(nuke.frame()))

We can see that the current frame is being converted into a string, and then passed into the index() function which will find the first position where the value is the same as the frame number. This will return the correct result regardless of the order of inputs, which is what we want, because it’d be anti-lazy to have to reorder all upstream nodes whenever we insert or delete an existing input.

Testing our switch now shows that the which is updating to the correct input whenever the current frame is matched with the values in the list.

Remember, computers start counting at 0! That’s why it sets input to 1 even though ‘1’ is the second value in the list.

Correct input is guaranteed even with scrambled inputs, which maximizes our opportunity to be lazy!

Maximize laziness at all costs!

Adding new inputs is very easy, update the label to the desired frame, and the switch will do the rest!

Easy!!

That’s it for this article, hopefully you’ve gotten some new ideas for procedural compositing, and ways of being as lazy possible!

Thank you for reading!

Nuke script of the examples if you’re interested:

set cut_paste_input [stack 0]
version 12.1 v2
BackdropNode {
 inputs 0
 name BackdropNode1
 tile_color 0x38aa50ff
 label "EXAMPLE 1\\nTCL BOOLEAN EXPRESSION"
 note_font_size 42
 selected true
 xpos -1902
 ypos 11
 bdwidth 1002
 bdheight 695
}
BackdropNode {
 inputs 0
 name BackdropNode2
 tile_color 0x388e8e00
 label "EXAMPLE 2\\nPYTHON BOOLEAN EXPRESSION\\nMIGHT NEED TO IMPORT REGEX FIRST!"
 note_font_size 42
 selected true
 xpos -782
 ypos 10
 bdwidth 859
 bdheight 679
}
BackdropNode {
 inputs 0
 name BackdropNode3
 tile_color 0xaa5c5cff
 label "EXAMPLE 3\\nPYTHON LIST COMPREHENSION"
 note_font_size 42
 selected true
 xpos 150
 ypos 10
 bdwidth 1356
 bdheight 676
}
push $cut_paste_input
Reformat {
 format "256 256 0 0 256 256 1 square_256"
 name Reformat1
 label asdf
 selected true
 xpos -1740
 ypos 185
 hide_input true
}
Dot {
 name Dot19
 selected true
 xpos -1706
 ypos 259
}
set N566b9400 [stack 0]
Dot {
 name Dot5
 selected true
 xpos -1553
 ypos 259
}
set N566b9000 [stack 0]
Dot {
 name Dot1
 selected true
 xpos -1409
 ypos 259
}
set N566b8c00 [stack 0]
Dot {
 name Dot2
 selected true
 xpos -1278
 ypos 259
}
set N566b8800 [stack 0]
Dot {
 name Dot3
 selected true
 xpos -1110
 ypos 259
}
Text2 {
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{51 48}
   }
 old_expression_markers {{0 1}
   }
 box {25.5 73.5 238.5 174.5}
 transforms {{0 2}
   }
 cursor_position 13
 center {1024 778}
 cursor_initialised true
 initial_cursor_position {{25.5 174.5}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 1024 778 0 0 1 1 0 0 0 0}
   }
 name MASTER_CTRL3
 label 30
 selected true
 xpos -1144
 ypos 290
}
Dot {
 name Dot4
 selected true
 xpos -1110
 ypos 566
}
push $N566b8800
Text2 {
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{51 48}
   }
 old_expression_markers {{0 1}
   }
 box {25.5 73.5 238.5 174.5}
 transforms {{0 2}
   }
 cursor_position 13
 center {1024 778}
 cursor_initialised true
 initial_cursor_position {{25.5 174.5}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 1024 778 0 0 1 1 0 0 0 0}
   }
 name MASTER_CTRL2
 label 30
 selected true
 xpos -1312
 ypos 290
}
Dot {
 name Dot7
 selected true
 xpos -1278
 ypos 465
}
push $N566b8c00
Text2 {
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{50 48}
   }
 old_expression_markers {{0 1}
   }
 box {25.5 73.5 238.5 174.5}
 transforms {{0 2}
   }
 cursor_position 13
 center {1024 778}
 cursor_initialised true
 initial_cursor_position {{25.5 174.5}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 1024 778 0 0 1 1 0 0 0 0}
   }
 name MASTER_CTRL1
 label 20
 selected true
 xpos -1443
 ypos 289
}
Dot {
 name Dot6
 selected true
 xpos -1409
 ypos 408
}
push $N566b9000
Text2 {
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{49 48}
   }
 old_expression_markers {{0 1}
   }
 box {25.5 73.5 238.5 174.5}
 transforms {{0 2}
   }
 cursor_position 13
 center {1024 778}
 cursor_initialised true
 initial_cursor_position {{25.5 174.5}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 1024 778 0 0 1 1 0 0 0 0}
   }
 name MASTER_CTRL
 label 10
 selected true
 xpos -1587
 ypos 290
}
Dot {
 name Dot18
 selected true
 xpos -1553
 ypos 352
}
push $N566b9400
Switch {
 inputs 2
 which {{"input1.input.label == frame"}}
 name Switch10
 selected true
 xpos -1740
 ypos 349
}
Switch {
 inputs 2
 which {{"input1.input.label == frame"}}
 name Switch2
 selected true
 xpos -1740
 ypos 405
}
Switch {
 inputs 2
 which {{"input1.input.label == frame"}}
 name Switch3
 selected true
 xpos -1740
 ypos 462
}
Switch {
 inputs 2
 which {{"input1.input.label == frame"}}
 name Switch4
 selected true
 xpos -1740
 ypos 563
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{56 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text10
 label 80
 selected true
 xpos 1313
 ypos 247
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{55 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text9
 label 70
 selected true
 xpos 1193
 ypos 245
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{54 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text8
 label 60
 selected true
 xpos 1075
 ypos 241
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{52 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text6
 label 40
 selected true
 xpos 841
 ypos 238
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{49 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text3
 label 10
 selected true
 xpos 511
 ypos 238
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{49}
   }
 old_expression_markers {{0 0}
   }
 box {0 411 54 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text2
 label 1
 selected true
 xpos 401
 ypos 238
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{53 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text7
 label 50
 selected true
 xpos 951
 ypos 238
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{50 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text4
 label 20
 selected true
 xpos 621
 ypos 238
 hide_input true
}
Text2 {
 inputs 0
 font_size_toolbar 100
 font_width_toolbar 100
 font_height_toolbar 100
 message "\[value label]"
 old_message {{51 48}
   }
 old_expression_markers {{0 1}
   }
 box {0 411 107 512}
 transforms {{0 2}
   }
 cursor_position 13
 center {256 256}
 cursor_initialised true
 initial_cursor_position {{0 512}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 256 256 0 0 1 1 0 0 0 0}
   }
 name Text5
 label 30
 selected true
 xpos 731
 ypos 238
 hide_input true
}
NoOp {
 inputs 0
 name NoOp1
 selected true
 xpos 404
 ypos 507
 hide_input true
}
Switch {
 inputs 10
 which {{"\[python \\\[nuke.thisNode().input(i)\\\['label'\\].getValue()\\ for\\ i\\ in\\ range(nuke.thisNode().inputs())\\].index(str(nuke.frame()))]"}}
 name Switch1
 selected true
 xpos 673
 ypos 507
}
Reformat {
 inputs 0
 format "512 512 0 0 512 512 1 square_512"
 name Reformat2
 selected true
 xpos -454
 ypos 248
 hide_input true
}
Text2 {
 font_size_toolbar 120
 font_width_toolbar 100
 font_height_toolbar 100
 message FLARE
 old_message {{70 76 65 82 69}
   }
 box {95 183.5 461 304.5}
 transforms {{0 2}
   }
 font_size_values {{0 120 1 120 2 120 3 120 4 120 0 110 1 110 2 110 3 110 4 110}
   }
 cursor_position 3
 font_size 120
 center {1024 778}
 cursor_initialised true
 initial_cursor_position {{95 304.5}
   }
 group_animations {{0} imported: 0 selected: items: "root transform/"}
 animation_layers {{1 11 1024 778 0 0 1 1 0 0 0 0}
   }
 name Text1
 selected true
 xpos -454
 ypos 283
}
Crop {
 box {0 0 512 512}
 name Crop1
 selected true
 xpos -454
 ypos 322
}
push 0
Switch {
 inputs 2
 which {{"\[python str(nuke.frame())\\ in\\ re.split('\\\[^0-9\\]+',nuke.thisNode()\\\['frames'\\].getValue())]"}}
 name Switch6
 selected true
 xpos -454
 ypos 539
 addUserKnob {20 User}
 addUserKnob {1 frames}
 frames "10, 20, 30, 40, 50, 60"
}
Viewer {
 frame 10
 frame_range 1-90
 name Viewer1
 selected true
 xpos 672
 ypos 612
}
NoOp {
 inputs 0
 name IMPORT_REGEX_HERE
 selected true
 xpos -683
 ypos 245
 hide_input true
 addUserKnob {20 User}
 addUserKnob {22 importRe l "Import ReGex" T "import re" +STARTLINE}
}