Advertisement

Write a Render Manager for Nuke using Python

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

Learn how to write a custom render manager for Nuke using Python, allowing you to render one or more Nuke projects without needing to open the software.

1. Introduction

The purpose of this tutorial is to explain how to write a software that allows you to manage the rendering process in Nuke. You might have several Nuke comps that need to be rendered, so by using such a program you can render all of them at once without opening Nuke itself, that means the system is not loading Nuke's graphic interface so it can reserve more memory for the rendering process. Here you can see an example of the program you are going to build:

Graphic User Interface.
The program rendering three projects.

The program has a clear user interface that allows you to organize and queue as many renders as you need.

Requirements

In this tutorial I assume you have a basic understanding of Python and some dos commands. This software is meant to be run on the Windows operating system. The tools you will need are the following:

Python 2.x installed ( https://www.python.org ) Do not use the 3.x version because Nuke doesn't support it.

wxPython library ( http://www.wxpython.org ) This allows you to create a user interface. You might also use Tkinter, Qt, but this is not covered in this tutorial.

Software structure

We will call this software NukeRenderManager. The program is made of three files:

  • NukeRenderingManager.py

  • exeNuke.bat

  • Rendering.py

NukeRenderingManager.py: it contains everything about the graphic user interface and all the information regarding the location of the Nuke projects and all frame ranges.

exeNuke.bat: it is in charge of launching Nuke in terminal mode by passing through all the information coming from the NukeRenderingManager.py file. This file is called for each render, so if three Nuke comps need to be rendered, this file will be run three times.

Rendering.py: it gets all the information from exeNuke.bat and perform the rendering. This file is executed for each Nuke project.

2. Writing the NukeRenderingManager.py

Description

The NukeRenderingManager.py manages the user interface and organize the list of the projects to render.

The User Interface

To build our user interface we utilize the wxPython library. As I said before, you can use a different library but for the purpose of this tutorial, I will explain wxPython. To install it you just need to download the installer, launch it and everything is ready (you can find the link above). After the library is installed you need to start Python 2.x IDLE and this gives you the Python shell. From the File menu choose New File, now you have an empty editor. If you want you can use any other editor you might feel comfortable with. 

Empty Python Editor.

 Save the file as NukeRenderingManager.py and put it in any folder you want.

The first thing to do is importing the modules we need. The first is os that allows us to use the operating system functions, the second is the wx that is going to be useful to build a graphic user interface:

import os
import wx

We are going to create a window that contains all we need, so we achieve this goal by creating a custom class that is derived from wx.Frame:

Class mainWindow(wx.Frame):

Then we implement the constructor by calling the wx.Frame.__init__:

def __init__(self):
        
#constructor
wx.Frame.__init__(self,None,title="Nuke Rendering Manager",size=(600,300),style=wx.SYSTEM_MENU|wx.CAPTION|wx.CLOSE_BOX)

Then we create a status bar:

self.CreateStatusBar()

 We add a text control to show which Nuke projects are going to be processed:

# prepare the Nuke scripts list on screen
self.NukeScriptsList=wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.NukeScriptsList.SetEditable(False)
self.NukeScriptsList.SetBackgroundColour((120,120,120))
self.NukeScriptsList.SetForegroundColour((50,255,50))
self.NukeScriptsList.SetValue('Nuke scripts:\n')

The wx.TextCtrl provide us an area where we can write the list, we need it as multiline so we declare wx.TE_MULTILINE. We don’t need it to be editable so we use SetEditable(False), then we define some colours and finally we show a text.

Then we create a render button:

# it creates the render button
self.RenderButton=wx.Button(self,label="Render",pos=(8,200))

 A very important thing is the sizer. The sizer allows us to define a layout, we will be using the BoxSizer that places elements horizontally and vertically, we choose a vertical placement for the text control and the button:

self.layout=wx.BoxSizer(wx.VERTICAL)
self.layout.Add(self.NukeScriptsList,1,wx.EXPAND)
self.layout.Add(self.RenderButton,0,wx.EXPAND)
self.SetSizer(self.layout)

The second parameter in the Add method is a numer that describes how mush space each element occupies, 0 means that the minimum size will be used, 1 means that space available will be occupied, in our case we want the button to minimized and the text control to have the remaining space.

We prepare some variables:

self.NukeScripts=[]
self.dirName=""
self.fileName=""

Then we prepare the menu. We start by creating a menubar as wx.MenuBar(), we create e menu called filemenu as wx.Menu(), we add the Add Nuke Scripts and Exit items and append them to the filmenu. And finally we append filemenu to menuBar:

# it creates menu items
menuBar=wx.MenuBar()

filemenu=wx.Menu()
addNukeScript=filemenu.Append(wx.ID_ANY,"Add Nuke script","Add Nuke script")
ClearList=filemenu.Append(wx.ID_ANY,"Clear list","Clear list")
exitEvt=filemenu.Append(wx.ID_EXIT,"Exit","Exit")

menuBar.Append(filemenu,"File")
self.SetMenuBar(menuBar)

wx.ID_ANY wx.ID_EXIT are used to provide an ID to the elements, in the first case we get an ID for the item, but in the second case we have a ID_EXIT that creates a special ID for the exit action.

The next step is to let these elements perform some operation, for that we use the wx.Bind function that allows us to bind the element to a specific function:

self.Bind(wx.EVT_MENU,self.onAdd,addNukeScript)

The first argument says that we are dealing with a menu event, the second calls the function that we want to link to this element and the third is the element itself. In this case is the addNukeScritp item in the menu. We still have to implement the self.onAdd function, we will do that later:

self.Bind(wx.EVT_MENU,self.onClearList,ClearList)

 The ClearList action is bound to the onClearList method:

self.Bind(wx.EVT_BUTTON,self.onRender,self.RenderButton)

Here we bind the self.RenderButton to the self.onRender function that we have to implement:

self.Bind(wx.EVT_MENU, self.onExit,exitEvt)

 Finally we assign the self.onExit function to the exitEvt element.

To complete the constructor we show the mainWindow:

# it shows the main window
self.Show(True)

So far we have our constructor:

import os
import wx

class mainWindow(wx.Frame):

    def __init__(self):
        
        #constructor
        wx.Frame.__init__(self,None,title="Nuke Rendering Manager",size=(600,300),style=wx.SYSTEM_MENU|wx.CAPTION|wx.CLOSE_BOX)

        # it creates a status bar
        self.CreateStatusBar()

        # prepare the Nuke scripts list on screen
        self.NukeScriptsList=wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.NukeScriptsList.SetEditable(False)
        self.NukeScriptsList.SetBackgroundColour((120,120,120))
        self.NukeScriptsList.SetForegroundColour((50,255,50))
        self.NukeScriptsList.SetValue('Nuke scripts:\n')

        # it creates the render button
        self.RenderButton=wx.Button(self,label="Render",pos=(8,8))

        # layout
        self.layout=wx.BoxSizer(wx.VERTICAL)
        self.layout.Add(self.NukeScriptsList,1,wx.EXPAND)
        self.layout.Add(self.RenderButton,0,wx.EXPAND)
        self.SetSizer(self.layout)
        
        #variables
        self.NukeScripts=[]
        self.dirName=""
        self.fileName=""

        # it creates menu items
        menuBar=wx.MenuBar()

        filemenu=wx.Menu()
        addNukeScript=filemenu.Append(wx.ID_ANY,"Add Nuke script","Add Nuke script")
        ClearList=filemenu.Append(wx.ID_ANY,"Clear list","Clear list")
        exitEvt=filemenu.Append(wx.ID_EXIT,"Exit","Exit")

        menuBar.Append(filemenu,"File")
        self.SetMenuBar(menuBar)

        # it binds elements to events
        self.Bind(wx.EVT_MENU,self.onAdd,addNukeScript)
        self.Bind(wx.EVT_MENU,self.onClearList,ClearList)
        self.Bind(wx.EVT_BUTTON,self.onRender,self.RenderButton)
        self.Bind(wx.EVT_MENU, self.onExit,exitEvt)

        # it shows the main window
        self.Show(True)
Snapshot of the editor.

Let’s have a look at the functions. The first thing I want to explain is onAdd that is executed whenever the menu event addNukeScript is called. The goal of this function is to add the Nuke scripts information on a list:

# it adds Nuke scripts on the list
    def onAdd(self,event):
        wildcard="Nuke scripts *.nk|*.nk"
        dlg=wx.FileDialog(self,message="Add Nuke script",wildcard=wildcard,style=wx.OPEN)
        if dlg.ShowModal()==wx.ID_OK:
            self.dirName=dlg.GetDirectory()
            self.fileName=dlg.GetFilename()
            self.NukeScripts.append(self.dirName+self.fileName)
            self.updateList()
        dlg.Destroy()

Because this function is called as an event occurs, as we define it we have to include an extra parameter that in this case we called event. We define a wildcard as a string, that is useful to guide users to what extension they must look for:

wildcard="Nuke scripts *.nk|*.nk"

A file open dialog is created and as the user clicks OK, we memorize the directory and the file name to our variables and we call updateList to update the screen:

if dlg.ShowModal()==wx.ID_OK:
            self.dirName=dlg.GetDirectory()
            self.fileName=dlg.GetFilename()
            self.NukeScripts.append(self.dirName+self.fileName)
            self.updateList()

The updateList method clears the screen, loops through the NukeScripts list and writes again on the screen:

#it updates the Nuke scripts list on screen
    def updateList(self):
        self.NukeScriptsList.Clear()
        for i in self.NukeScripts:
            self.NukeScriptsList.AppendText(i+"\n") 

The onClearList function clears the screen and the NukeScripts list:

def onClearList(self,event):
        self.NukeScriptsList.Clear()
        self.NukeScripts=[]

We have onRender() , that will be implemented in the next section, and the onExit function that closes the application:

# it starts the rendering process
    def onRender(self,event):
        print "Rendering..."

    # it closes the program
    def onExit(self,event):
        self.Close(True)

This is the mainWindow class definition, now we need to make an instance of it in order to see and use it, but first we have to create an wx.App object:

app=wx.App(False)

Then we create our mainWindow instance:

mainWindow=mainWindow()

Finally we need to call the MainLoop function to start the application:

app.MainLoop()

So at this point we have the NukeRenderingManager.py code except the onRender method that we are going to implement in the next section.

To make our program more robust I added a couple of lines to make some checks. As we load a Nuke script, it would be good if we check if the file extension is .nk, even if the wildcard filters our choice. We use the os.path.splitext function, then if the extension is .nk we proceed as normal:

#we check if we have a Nuke script
            self.extension=os.path.splitext(self.fileName)
            
            if self.extension[1]==".nk":    
                self.NukeScripts.append(self.dirName+self.fileName)
                self.updateList()

The os.path.splitext gives back a list with the name and the file extension at the [0] and [1] position. Since we are loading external files, it might be possible that some of them may be corrupted, so to increase the quality of our application we will handle the exceptions:

# it adds Nuke scripts on the list
    def onAdd(self,event):
        wildcard="Nuke scripts *.nk|*.nk"
        dlg=wx.FileDialog(self,message="Add Nuke script",wildcard=wildcard,style=wx.OPEN)

        try:
            if dlg.ShowModal()==wx.ID_OK:
                self.dirName=dlg.GetDirectory()
                self.fileName=dlg.GetFilename()
            
                #we check if we have a Nuke script
                self.extension=os.path.splitext(self.fileName)
            
                if self.extension[1]==".nk":    
                    self.NukeScripts.append(self.dirName+”\\”+self.fileName)
                    self.updateList()
        except:
            print "unable to read this file"
                
        dlg.Destroy()

 As you have noticed I have used self.NukeScripts.append(self.dirName+”\\”+self.fileName), I had to add “\\” because I found out that if any nuke script is located in c:\ it returns c:\, you have to add the \ manually.

Before the end of this section I want to mention that in order to make the entire system work, we should avoid placing the nuke scripts, the exeNuke.bat and the Rendering.py files in a folder that has a very long path. I have been testing the program and for some reason when this path is too long it doesn’t work, it might because the prompt is not able to handle such strings.

So our NukeRenderingManager.py is the following:

import os
import wx

class mainWindow(wx.Frame):

    def __init__(self):
        
        #constructor
        wx.Frame.__init__(self,None,title="Nuke Rendering Manager",size=(600,300),style=wx.SYSTEM_MENU|wx.CAPTION|wx.CLOSE_BOX)

        # it creates a status bar
        self.CreateStatusBar()

        # prepare the Nuke scripts list on screen
        self.NukeScriptsList=wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.NukeScriptsList.SetEditable(False)
        self.NukeScriptsList.SetBackgroundColour((120,120,120))
        self.NukeScriptsList.SetForegroundColour((50,255,50))
        self.NukeScriptsList.SetValue('Nuke scripts:\n')

        # it creates the render button
        self.RenderButton=wx.Button(self,label="Render",pos=(8,8))

        # layout
        self.layout=wx.BoxSizer(wx.VERTICAL)
        self.layout.Add(self.NukeScriptsList,1,wx.EXPAND)
        self.layout.Add(self.RenderButton,0,wx.EXPAND)
        self.SetSizer(self.layout)
        
        # variables
        self.NukeScripts=[]
        self.dirName=""
        self.fileName=""

        # it creates menu items
        menuBar=wx.MenuBar()

        filemenu=wx.Menu()
        addNukeScript=filemenu.Append(wx.ID_ANY,"Add Nuke script","Add Nuke script")
        ClearList=filemenu.Append(wx.ID_ANY,"Clear list","Clear list")
        exitEvt=filemenu.Append(wx.ID_EXIT,"Exit","Exit")

        menuBar.Append(filemenu,"File")
        self.SetMenuBar(menuBar)

        # it binds elements to events
        self.Bind(wx.EVT_MENU,self.onAdd,addNukeScript)
        self.Bind(wx.EVT_MENU,self.onClearList,ClearList)
        self.Bind(wx.EVT_BUTTON,self.onRender,self.RenderButton)
        self.Bind(wx.EVT_MENU, self.onExit,exitEvt)

        # it shows the main window
        self.Show(True)

    #it updates the Nuke scripts list on screen
    def updateList(self):
        self.NukeScriptsList.Clear()
        for i in self.NukeScripts:
            self.NukeScriptsList.AppendText(i+"\n")

    # it adds Nuke scripts on the list
    def onAdd(self,event):
        wildcard="Nuke scripts *.nk|*.nk"
        dlg=wx.FileDialog(self,message="Add Nuke script",wildcard=wildcard,style=wx.OPEN)

        try:
            if dlg.ShowModal()==wx.ID_OK:
                self.dirName=dlg.GetDirectory()
                self.fileName=dlg.GetFilename()
            
                #we check if we have a Nuke script
                self.extension=os.path.splitext(self.fileName)
            
                if self.extension[1]==".nk":
                    self.NukeScripts.append(self.dirName+"\\"+self.fileName)
                    self.updateList()
        except:
            print "unable to read this file"
                
        dlg.Destroy()

    def onClearList(self,event):
        self.NukeScriptsList.Clear()
        self.NukeScripts=[]

    # it starts the rendering process for each Nuke script
    def onRender(self,event):
            #to implement
	return

    # it closes the program
    def onExit(self,event):
        self.Close(True)
                        

app=wx.App(False)
mainWindow=mainWindow()
app.MainLoop()
Another snapshot of the editor.

3. Writing the exeNuke.bat file

Description

A bat file is recognized by the Windows operating system as a collection of commands. You can write any kind of command you want, you can also launch programs, and that is a feature we are going to use. If you are familiar with prompt instructions you will find this process easy.

 First of all you need to open an empty text file (I recommend Notepad), and save it as exeNuke.bat. As I mentioned in the previous section, we should avoid placing these files in a location that has a very long path, that because the prompt is not able to handle it, so place all the three files we are writing on your drive, with just a few subfolders, something like c:\NukeRenderingManager or c:\myProjects\NukeRenderingManager. 

That rule applies to the Nuke scripts as well, they might be located in a different place, but make sure that the path isn't too long.

Implementation

I want to briefly explain how Nuke works. We usually work in Nuke through its graphic user interface, but for some specific tasks we might want to run it in terminal mode. That means we only write commands to perform any usual operation, it looks like a Windows prompt:

The way we send instructions to Nuke in terminal mode is by writing some Python code. Let’s suppose you want to create a Blur node, you can type nuke.createNode(‘Blur’) and so on. What we are going to do is let the bat file open Nuke in terminal mode and start the render of a project, doing everything by sending commands and without any graphic user interface.

The first instruction are:

C:\
C:

 This is to make sure we can start typing the Nuke path in order to launch it:

cd Programmi\Nuke6.2v6
Nuke6.2 –t

Of course these lines might be different, write the location of your machine. The –t means terminal mode. If you double click on your exeNuke.bat file you should see Nuke in terminal mode. If you want to quit, just type quit() and hit Enter. In order to perform the rendering we also need to execute the Rendering.py file, so we can update our code:

cd\
c:
cd Programmi\Nuke6.2v6
Nuke6.2 -t c:\NukeRenderingManager\Rendering.py

By adding the location of the Rendering.py file, we ask to open Nuke in terminal mode and execute the Rendering.py that contains all the code to perform the rendering, and as I said before terminal mode requires the Python language, so we use the Rendering.py code. But we still need one piece of information, the Rendering.py file needs to know where the Nuke scripts are located. 

Remember that the exeNuke.bat and Rendering.py will be called for each Nuke script, so If we have to render three projects they will be launched three times. But each time they are called the Rendering.py needs to know where the scritp is located, to achieve this task we need to get this information from the above NukeRenderingManager.py.

Snapshot of the batch file editor .

Complete NukeRenderingManagerFile.py

The only method we need to implement is onRender(). What we do is loop through NukeScripts and call the bat file each time:

# it starts the rendering process for each Nuke script
    def onRender(self,event):
        for i in self.NukeScripts:
            os.system("C:/exeNuke.bat"+ " "+i)

We use the os.system function to execute the file. As you have noticed we also pass i as the argument after a space. We basically send the NukeScript path to the batch file. The fact that we can easily send this information to the batch file gives us a great flexibility.

Complete the exeNuke.bat file

The way a batch file gets arguments is by using the symbol % followed by a number, because we passed one information we will write %1. Here the complete code:

cd\
c:
cd Programmi\Nuke6.2v6
Nuke6.2 -t c:\Rendering.py %1

 We launch Nuke and we call the Rendering.py by giving it the path of the script as an argument.

Before I conclude this section I want to recap the process described until now. NukeRenderingManager.py provides us the graphic user interface and organizes the list of the Nuke scripts to be rendered. For each of the scripts exeNuke.bat and Rendering.py will be called. The first is in charge of running Nuke in terminal mode, grabbing the path of the script to be processed and passing it to the Rendering.py that will perform the render itself. Now we need to implement Rendering.py.

4. Writing the Rendering.py file

Implementation

The first thing we need to do is grab the path of the script we passed into the batch file. To accomplish this, we simply use the following statement sys.argv[1]. Then we transform this information in string:

prj=str(sys.argv[1])

The instruction to open a Nuke project is the following:

nuke.scriptOpen(prj)

Now we have the script ready to use. What we need to do now is look for the write node we want and render. In my example, the write node I need is called Write1, but you can use any name you want. Here is the complete code:

prj=str(sys.argv[1])

nuke.scriptOpen(prj)

for i in nuke.allNodes():
    if i.Class()=="Write":
        if i['name'].getValue()=="Write1":
            first_frame=nuke.Root().knob('first_frame').value()
            last_frame=nuke.Root().knob('last_frame').value()
            nuke.execute(i,first_frame,last_frame)

What we do is loop through all the nodes in the script, we check if the node is a write one, we control that the name is Write1, we get the first and the last frame of the project and we use the nuke.execute function to execute the render.

Snapshot of the rendering.py file.

Conclusion

To launch the program just double click on the NukeRenderingManager.py. Enjoy!

Advertisement