-- Renders all the available render targets of the loaded project. -- Asks the user for an output directory and saves all the images into -- that directory (_01.png, _02.png, ...) -- -- @author Mark, Thomas -- @version 0.2 -- @shortcut ctrl + b local version = "v1.0 - for OctaneRender 1.5" -- global table with our settings local settings = { ["projectPath"] = octane.project.getCurrentProject(), ["renderTargets"] = octane.project.getSceneGraph():findNodes(octane.NT_RENDERTARGET, true), ["outputDirectory"] = nil, ["enabled"] = {}, ["fileType"] = {} } -- If no render targets are found, error out if #settings.renderTargets == 0 then error("No render targets were found in this project.", projectPath) end -------------------------------------------------------------------------------------------------- -- Gui helpers -- Creates a text label and returns it function createLabel(text, width, height) return octane.gui.create { type = octane.gui.componentType.LABEL, -- type of the component text = text, -- text that appears on the label width = width, -- width of the label in pixels height = height, -- height of the label in pixels } end -- Creates a button and returns it function createButton(name, text, width, height, tooltip) return octane.gui.create { type = octane.gui.componentType.BUTTON, -- type of the component name = name, -- name of the component text = text, -- button text width = width, -- width in pixels height = height, -- height in pixels tooltip = tooltip, -- tooltip text } end -- Creates a slider and returns it function createSlider(name, value, min, max, step, width, height, logarithmic) return octane.gui.create { type = octane.gui.componentType.SLIDER, -- type of the component name = name, -- name of the component value = value, -- value of the slider minValue = min, -- minimum value of the slider maxValue = max, -- maximum value of the slider step = step, -- interval between 2 discrete slider values width = width, -- width of the slider in pixels height = height, -- height of the slider in pixels logarithmic = logarithmic, -- Make slider logarithmic } end -- Creates a group and retuns it function createGroup(name, children, border, rows, cols, width, height, text, padding, inset, center) return octane.gui.create { type = octane.gui.componentType.GROUP, -- type of the component name = name, -- name of component children = children, -- list of children components border = border, -- boolean flag to show border rows = rows, -- number of rows in group grid cols = cols, -- number of cols in group grid width = width, -- width in pixels height = height, -- height in pixels text = text, -- text to display at top of group padding = padding, -- internal padding in each cell inset = inset, -- inset of the group component centre = center, -- center the group } end -- recursively enables or disables all components on a window function setEnabled(component, enable) if component.type == octane.gui.componentType.WINDOW or component.type == octane.gui.componentType.GROUP then for _, childComponent in ipairs(component.children) do setEnabled(childComponent, enable) end else component.enable = enable end end -------------------------------------------------------------------------------------------------- -- Gui helpers -- layout constants local BUTTON_WIDTH = 100 local BUTTON_HEIGHT = 20 local WIDE_LBL_WIDTH = 200 local NARROW_LBL_WIDTH = BUTTON_WIDTH local LBL_HEIGHT = 20 local GRP_PAD = 2 -- create a table with: -- * render target sequence number -- * render target name -- * render target enable box -- * render target output format box local tableChildren = {} for ix, renderTarget in ipairs(settings.renderTargets) do local description = string.format("%d: %s", ix, renderTarget.name) local lbl = createLabel(description, WIDE_LBL_WIDTH, LBL_HEIGHT) local enableBox = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "render", enable = true, width = 80, } local fileTypeCombo = octane.gui.create { type = octane.gui.componentType.COMBO_BOX, items = { "PNG 8-bpc" , "PNG 16-bpc" , "EXR untonemapped", "EXR tonemapped" , }, selectedIx = 1, width = 120, } table.insert(tableChildren , lbl) table.insert(tableChildren , enableBox) table.insert(tableChildren , fileTypeCombo) table.insert(settings.enabled , true) table.insert(settings.fileType, octane.render.imageType.PNG8) -- on click, toggle the enabled flag in the list enableBox.callback = function() settings.enabled[ix] = not settings.enabled[ix] end -- on combo change -> modify the image file type fileTypeCombo.callback = function() local fileTypes = { octane.render.imageType.PNG8, octane.render.imageType.PNG16, octane.render.imageType.EXR, octane.render.imageType.EXRTONEMAPPED, } settings.fileType[ix] = fileTypes[fileTypeCombo.selectedIx] end end -- add a disable all button local disableAllButton = octane.gui.create { type = octane.gui.componentType.BUTTON, width = BUTTON_WIDTH, height = BUTTON_HEIGHT, text = "Disable All", callback = function(me) local enable = nil if me.text == "Disable All" then me.text = "Enable All" enable = false else me.text = "Disable All" enable = true end for ix=1,#settings.renderTargets do settings.enabled[ix] = enable end for _,child in ipairs(tableChildren) do if child.type == octane.gui.componentType.CHECK_BOX then child.checked = enable end end end } table.insert(tableChildren, createLabel("", 1, 1)) table.insert(tableChildren, disableAllButton) table.insert(tableChildren, createLabel("", 1, 1)) createButton("output", "Output Folder...", BUTTON_WIDTH, BUTTON_HEIGHT, "Specify the folder to receive the rendered output files.") local renderTargetsGrp = octane.gui.create { type = octane.gui.componentType.GROUP, children = tableChildren, border = true, cols = 3, rows = #tableChildren / 3, text = "Render Targets", padding = { GRP_PAD }, } -- maximum samples slider local lblMaxSamples = createLabel("samples/px", NARROW_LBL_WIDTH, LBL_HEIGHT) local boxMaxSamples = octane.gui.create { type = octane.gui.componentType.NUMERIC_BOX, width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, maxValue = 256000, minValue = 1, step = 1, value = 100, } -- output location local btnOutput = createButton("output", "Output Folder...", BUTTON_WIDTH, BUTTON_HEIGHT, "Specify the folder to receive the rendered output files.") -- create an editor that will show the chosen file path local txtEditorDirectory = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "" , x = 0 , -- calculate the width to make the 2 stacked groups the same size -- no magic formula just trial and error ;) width = renderTargetsGrp.width - btnOutput.width - 18, height = 20 , enable = false , } -- buttons (apply not used in this case) local btnOK = createButton("ok", "Apply", BUTTON_WIDTH, BUTTON_HEIGHT, "Start batch processing.") local btnCancel = createButton("cancel", "Cancel", BUTTON_WIDTH, BUTTON_HEIGHT, "Cancel.") grpExecute = createGroup("grpExecute", { btnOK, btnCancel}, false, 1, 2, nil, nil, "Execute", { 5 }, nil, false) local settingsGrp = octane.gui.create { type = octane.gui.componentType.GROUP, children = { lblMaxSamples, boxMaxSamples, btnOutput , txtEditorDirectory, }, text = "Settings", border = true, cols = 2, rows = 2, padding = { GRP_PAD }, } local progressBar = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, width = settingsGrp.width - 8, height = LBL_HEIGHT, } -- this group holds everything together local layoutGrp = octane.gui.create { type = octane.gui.componentType.GROUP, rows = 4, cols = 1, children = { renderTargetsGrp, settingsGrp, progressBar, grpExecute, }, border = false, padding = { 2 }, centre = true, } -- window that holds all components local wndMain = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Batch Rendering", children = { layoutGrp }, width = layoutGrp.width, height = layoutGrp.height, } -- Callback function for the GUI Controls local function guiCallback(component, event) if component == btnOutput then -- ask the user for an output directory for the results local ret = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Select output directory for the render results", path = octane.file.getParentDirectory(settings.projectPath), browseDirectory = true, save = false, } if not ret.result or ret.result == "" then settings.outputDirectory = nil else settings.outputDirectory = ret.result txtEditorDirectory.text = settings.outputDirectory end elseif component == btnOK then -- Clear the cancel flag IS_CANCELLED = false setEnabled(wndMain, false) btnCancel.enable = true batchRender() setEnabled(wndMain, true) btnCancel.enable = false progressBar.text = "finished" elseif component == btnCancel then cancelRender() wndMain:closeWindow() elseif component == wndMain then cancelRender() end end -- Hookup GUI controls to their Callback function boxMaxSamples.callback = guiCallback btnOutput.callback = guiCallback btnOK.callback = guiCallback btnCancel.callback = guiCallback function cancelRender() IS_CANCELLED = true octane.render.callbackStop() end ------------------------------------------------------------------------------------------------------------------------------------ -- MAIN ROUTINE ------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------ function batchRender() progressBar.progress = 0 -- calculate the progress bar step local numEnabled = 0 for ix, enabled in ipairs(settings.enabled) do if enabled then numEnabled = numEnabled + 1 end end local progressBarStep = 1 / numEnabled -- render all the render targets that are enabled for ix, renderTarget in ipairs(settings.renderTargets) do -- break out if we're cancelled and set it in the progress bar if IS_CANCELLED then progressBar.text = "cancelled" break end -- only render if the rt is enabled if settings.enabled[ix] then -- create an output path for the image local path = nil if settings.outputDirectory then -- common extension for our image output types local fileExtensions = { [octane.render.imageType.PNG8] = "png", [octane.render.imageType.PNG16] = "png", [octane.render.imageType.EXR] = "exr", [octane.render.imageType.EXRTONEMAPPED] = "exr", } path = string.format("%s/%s_%dspp_%03d.%s", settings.outputDirectory, renderTarget.name, boxMaxSamples.value, ix, fileExtensions[settings.fileType[ix]]) end -- update the progress progressBar.text = string.format("rendering %s (%s)", renderTarget.name, path or "dry-run") -- render the image octane.render.start { renderTargetNode = renderTarget, maxSamples = boxMaxSamples.value, } -- save out the image if path then octane.render.saveImage(path, settings.fileType[ix]) end -- update the progress bar progressBar.progress = progressBar.progress + progressBarStep end end end setEnabled(wndMain, true) btnCancel.enable = false -- This must be placed such that all required functions are read prior to execution. -- The script will hold here until the window closes wndMain:showWindow()