The LUT implementation steps that Foundry advises for both 1D and 3D LUTs can use some work. By default, the 1D LUTs affect every channel (even alphas). For 3D LUTs it depends on the node used, but problems can vary from only affecting the default rgb
to touching every layer- even data layers.
For those not totally familiar with the concept of LUTs, a good overview is here, while the wikipedia page for 3D LUTs is actually pretty decent.
All code presented here is covered under a standard MIT license, and owned by Rhythm & Hues (posted with permission). A gist of this code is also published.
1D LUTs
The workflow the foundry advises for a 1D LUT is as follows:
To register one of the LUTs in the Project Settings as a Viewer Process, use, for example, the following function in your init.py:
1 2 3 4 5 6 7 8 |
|
This registers a built-in gizmo called
ViewerProcess_1DLUT
as a Viewer Process and sets it to use the Cineon LUT. The registered Viewer Process appears in the Viewer Process dropdown menu as Cineon.
Protecting The Alpha Channel
The built-in gizmo unfortunately lacks the one option we need, limiting the application of the LUT to rgb
channels only. However, the gizmo only includes a single node, ViewerLUT
, that does include that option. Let’s create a basic group that includes the ViewerLUT
node, but gives us more freedom.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
The key is line 10, we need to toggle the rgb_only
bool checkbox on the ViewerLUT
node so that it doesn’t affect the alpha channels anymore.
Despite recommending the usage of the ViewerProcess_1DLUT
, the Foundry does point out that parameter of the ViewerLUT
on the very next page.
Now our 1D LUT is no longer affecting alpha channels, but unfortunately several layers representing data channels. Motion vectors, zdepth, etc, are still being hit with our LUT, as those channels are being shuffled into rgb
for display purposes.
Protecting Data Channels
We need to add a Remove
node to strip these layers before the ViewerLUT
node, and then a Copy
node afterwards to add them back in. If those layers aren’t present in the incoming stream, Nuke doesn’t complain.
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
When changing many knob values at once, it’s often helpful to define a dictionary whose keys are the knob name, and values are the desired knob value. Then we can iterate through the dictionary, grabbing the knob object, and setting it to the retrieved value from the dictionary.
Here’s the copy node we need that adds the removed layers back in from the original input image stream:
28 29 30 31 32 33 34 35 |
|
With the copy, we set it to all
channels and remove the automatically filled in values in the from0
and on knobs. We’ll change the input to the Output
node below to be linked to the copy
node, and then we’ll add an expanded docstring.
Final 1D LUT Function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
It’s important to return the group
object, because that returned object will be used as the Viewer Process itself!
Adding A LUT With lutGroup1D()
Now that we have our function, we can use it in place of the ViewerProcess_1DLUT
node:
1 2 3 4 5 6 7 |
|
3D LUTs
We have 3 goals for 3D LUTs:
- Make sure every color layer is being affected by the LUT transform.
- Protect data layers from the transform.
- Ensure the transform happens in the correct colorspace.
We don’t need to worry about 3D LUTs affecting the alpha channel, because while a 1D LUT affects every channel the same, a 3D LUT has instructions for each color channel specifically- meaning it won’t have any instructions for alpha channels and will leave them alone.
Foundry gives some example nodes to use for 3D LUTs, such as the Vectorfield
node, but doesn’t mention the best node to use, the OpenColorIO node, OCIOFileTransform
.
Why Use OpenColorIO?
OpenColorIO is an open source project sponsored by Sony Imageworks. It has a lot of features, but right now we’re primarily interested in it because it allows us to select which layers to apply the LUT to- including all of them.
In comparison, the Vectorfield
node only affects the default rgb
layer, which is a pretty large handicap. The Vectorfield
node does have an option for colorspace conversion of input images, which is a plus. We’ll have to do our own colorspace conversion before applying the LUT with OCIOFileTransform
, as almost all LUTs expect the inputted channels to be in a Log colorspace.
Building A Basic Viewer Process With OCIOFileTransform
We’ll use the basic framework of our lutGroup1D()
function to create a lutGroup3D()
, including protection of data layers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
We’ve only had to change a few lines here. Instead of a string representing an already registered LUT name, the OCIOFileTransform
requires a full filepath, which we need to provide in the lutFile
arg.
In addition, we’ve set channels
to all
, which (unlike the Vectorfield
node) will ensure that every layer’s rgb
will get affected. We’re already protecting the data channels by removing them, just like in lutGroup1D()
.
Setting Input Colorspace
Most LUTs require an input colorspace that’s different from Linear space that Nuke works in. Vectorfield
has this conversion as a built in option, but OCIOFileTransform
does not, so we’ll have to add it before getting the correct result.
Traditionally the expected input colorspace is Cineon Log, but on an Arri Alexa show it would be LogC. We’ll specify which in an arg of the function, and use use a ViewerLUT
node to convert from to that.
Why use ViewerLUT
as opposed to a Colorspace
? Because ViewerLUT
lets us specify any of the 1D LUTs currently within the Nuke script, not just the built in ones. This means your pipeline can specify a different version of Cineon Log or an older revision of Alexa LogC.
We’ll make this input colorspace optional, so if the 3D LUT is built for linear values you can omit the arg.
Final 3D LUT Function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
Adding A LUT With lutGroup3D()
Now that we have our function, we can use it in place of the Vectorfield
node:
1 2 3 4 5 6 7 8 |
|
Final Thoughts
That should be it. You’ve still got some odds and ends left over- if you want to add a 3D LUT for every *.cube file found in a folder, you need some way of deriving the LUT’s desired input colorspace.
Also, a slicker means of doing this would involve creating a base LUT object class, with methods for doing things like deriving input colorspace, building the lut group, and registering it. A class level variable could then keep track of all the created LUTs for science.