---------------------------------------------------------------------------------------------------- -- 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 -- (e.g. _01.png, _02.png, ...) -- -- @author Mark Basset, Octane dev team and others -- @description Batch rendering for Octane. -- @version 0.3 -- @script-id OctaneRender batch rendering -- Global table with our settings. All global variables should be here to keep an overview. -- TODO: a property table here would make everything more compact local gSettings = { -- absolute path of the current project projectPath = octane.project.getCurrentProject(), -- the copied scene graph sceneGraph = nil, -- list of render target nodes in the current project together with their enable state -- and export file format renderTargets = {}, -- absolute path to the output directory of the rendered images outputDirectory = nil, -- true to override the max samples/px overrideMaxSamples = false, -- max samples/px maxSamples = 1000, -- filename template for the output files template = "%n_%f_%p.%e", -- true if the rendering was cancelled cancelled = false, -- handle for the progress update function (takes value and text) progress = nil, -- handle for the batch render function batchRender = nil, -- handle for the window window = nil, -- set to true to show the debug outlines of the groups showGrpOutlines = false, -- framerate fps = nil, -- delta value between 2 animation frames (1 / fps) dT = nil, -- frame number where we start rendering startFrame = nil, -- frame number where we finish rendering (inclusive) endFrame = nil, -- start time of the animation (s) startTime = nil, -- end time of the animation (s) endTime = nil, -- shutter time shutterTime = nil, -- frame number from which we start number files (0 means no offset) fileNumber = 0, -- skip existing files skipExisting = false, -- save all enabled render passes saveAllPasses = true, -- save the render passes as a layered exr saveMultiLayerExr = false, } local function loadFromDisk() for k, v in pairs(octane.storage.project) do if k then gSettings[k] = v end end end local function storeOnDisk() local storage = octane.storage.project storage.fps = gSettings.fps storage.startTime = gSettings.startTime storage.endTime = gSettings.endTime storage.shutterTime = gSettings.shutterTime storage.overrideMaxSamples = gSettings.overrideMaxSamples storage.maxSamples = gSettings.maxSamples storage.fileNumber = gSettings.fileNumber storage.fileTemplate = gSettings.fileTemplate storage.outputDirectory = gSettings.outputDirectory storage.template = gSettings.template storage.skipExisting = gSettings.skipExisting storage.saveAllPasses = gSettings.saveAllPasses storage.saveMultiLayerExr = gSettings.saveMultiLayerExr end -- Recursively enables or disables all components on a window. local 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 -- sorts a table alpha numerically -- (snippet from http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua) local function alphanumsort(nodes) local function padnum(d) local dec, n = string.match(d, "(%.?)0*(.+)") return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) end local function compare(n0, n1) local a, b = n0.name, n1.name return a:gsub("%.?%d+",padnum)..("%3d"):format(#b) < b:gsub("%.?%d+",padnum)..("%3d"):format(#a) end table.sort(nodes, compare) end local function cancelRendering() gSettings.cancelled = true octane.render.callbackStop() end local function isAnimatedScene() local interval = gSettings.sceneGraph:getAnimationTimeSpan() return interval[1] < interval[2] end local function displayTimeAsSeconds() return octane.project.getPreferences():getAttribute(octane.A_TIME_DISPLAY) == 1 end local function displayShutterAsSeconds() return octane.project.getPreferences():getAttribute(octane.A_SHUTTER_TIME_DISPLAY) == 1 end local function calcSceneFrameCount() local interval = gSettings.sceneGraph:getAnimationTimeSpan() return math.max(0, 1 + math.floor((interval[2] - interval[1] + .0001 * gSettings.dT) * gSettings.fps)) end local function timeToFrame(time) local interval = gSettings.sceneGraph:getAnimationTimeSpan() return math.floor((time - interval[1] + .0001 * (gSettings.dT)) * gSettings.fps) end local function frameToTime(frame) local interval = gSettings.sceneGraph:getAnimationTimeSpan() return interval[1] + frame * gSettings.dT end local function calcProgressUnit() local activeTargets = 0 for _, renderTarget in ipairs(gSettings.renderTargets) do if renderTarget.render then activeTargets = activeTargets + 1 end end return 1 / (activeTargets * (gSettings.endFrame - gSettings.startFrame + 1)) end local function createFilename(ix, frame, name, imageType, pass) -- 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", } local s = gSettings.template -- %i -> index of the render target s = string.gsub(s, "%%i", string.format("%d", ix)) -- %s -> frame number s = string.gsub(s, "%%f", string.format("%d", gSettings.fileNumber + frame)) -- %n -> name of the node s = string.gsub(s, "%%n", name) -- %e -> extension s = string.gsub(s, "%%e", fileExtensions[imageType]) -- %t -> timestamp (h_m_s) s = string.gsub(s, "%%t", os.date("%H_%M_%S")) -- %p -> render pass name s = string.gsub(s, "%%p", pass) return s end local function createRenderPassExportObjs(renderTargetNode) -- get the render passes node local rpNode = renderTargetNode:getInputNode(octane.P_RENDER_PASSES) if not rpNode then return nil end -- HACK: mix the info passes with the beauty passes rpNode:setPinValue(octane.P_RENDER_PASS_INFO_AFTER_BEAUTY, false) -- create the export objects local objs = {} for _, id in ipairs(octane.render.getAllRenderPassIds()) do local info = octane.render.getRenderPassInfo(id) if info.pinId ~= octane.P_UNKNOWN then if rpNode:getPinValue(info.pinId) then local exportObj = { ["exportName"] = nil, ["origName"] = info.name, ["renderPassId"] = info.renderPassId, } table.insert(objs, exportObj) end else local exportObj = { ["exportName"] = nil, ["origName"] = info.name, ["renderPassId"] = info.renderPassId, } table.insert(objs, exportObj) end end return objs end ---------------------------------------------------------------------------------------------------- -- Main window creation -- layout constants local BUTTON_WIDTH = 100 local BUTTON_HEIGHT = 20 local WIDE_LBL_WIDTH = 200 local NARROW_LBL_WIDTH = 150 local LBL_HEIGHT = 20 local GRP_PAD = 2 -- Creates a tabular overview of all the render targets in the current project. local function initRenderTargetOverview() local tableChildren = {} -- collect all the render targets from the current project for ix, renderTarget in ipairs(gSettings.renderTargets) do local description = string.format("%5d: %s", ix, renderTarget.node.name) local lbl = octane.gui.createLabel { text = description, width = WIDE_LBL_WIDTH, height = LBL_HEIGHT } local renderBox = octane.gui.createCheckBox { text = "render", enable = true, width = 80, checked = renderTarget.render, callback = function(box) -- toggle the for the render target enable state in the settings renderTarget.render = box.checked end } local fileTypeCombo = octane.gui.createComboBox { type = octane.gui.componentType.COMBO_BOX, items = { "PNG (8-bit)", "PNG (16-bit)", "EXR", }, selectedIx = 1, width = 120, callback = function(combo) -- update the file type for this render target in the settings local fileTypes = { octane.render.imageType.PNG8, octane.render.imageType.PNG16, octane.render.imageType.EXR, } renderTarget.fileType = fileTypes[combo.selectedIx] end } -- add components in the list table.insert(tableChildren, lbl) table.insert(tableChildren, renderBox) table.insert(tableChildren, fileTypeCombo) end local renderAllButton = octane.gui.createButton { width = BUTTON_WIDTH, height = BUTTON_HEIGHT, text = "Render all", callback = function() for _,renderTarget in ipairs(gSettings.renderTargets) do renderTarget.render = true end for _,child in ipairs(tableChildren) do if child.type == octane.gui.componentType.CHECK_BOX then child.checked = true end end end } local renderNoneButton = octane.gui.createButton { width = BUTTON_WIDTH, height = BUTTON_HEIGHT, text = "Render none", callback = function() for _,renderTarget in ipairs(gSettings.renderTargets) do renderTarget.render = false end -- update the check boxes for _,child in ipairs(tableChildren) do if child.type == octane.gui.componentType.CHECK_BOX then child.checked = false end end end } -- add the disable button between 2 dummy labels local dummyLbl = octane.gui.createLabel { text = "", width = 1, height = 1, } table.insert(tableChildren, dummyLbl) table.insert(tableChildren, renderAllButton) table.insert(tableChildren, renderNoneButton) -- create a group to pack it all up local renderTargetsGrp = octane.gui.create { type = octane.gui.componentType.GROUP, children = tableChildren, border = true, cols = 3, rows = #tableChildren / 3, border = false, padding = { GRP_PAD }, debug = gSettings.showGrpOutlines, } -- wrap the group in a panel stack to make it scrollable return octane.gui.create { type = octane.gui.componentType.PANEL_STACK, height = 200, children = { renderTargetsGrp }, open = { true }, captions = { "Render targets" }, } end -- Initializes a GUI group with all the config settings local function initSettingGroup(width) -- frames per second local fpsLbl = octane.gui.createLabel { text = "Framerate", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, enable = isAnimatedScene(), } local sliderWidth = width - NARROW_LBL_WIDTH - 10 local fpsSlider = octane.gui.createSlider { value = gSettings.fps, minValue = 1, maxValue = 120, step = 0.1, width = sliderWidth, enable = isAnimatedScene(), } -- start and end time/frame sliders local startTimeLbl = octane.gui.createLabel { text = string.format("Start %s", displayTimeAsSeconds() and "time" or "frame"), width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, enable = isAnimatedScene(), } local startSlider = octane.gui.createSlider { width = sliderWidth, enable = isAnimatedScene(), } local endTimeLbl = octane.gui.createLabel { text = string.format("End %s", displayTimeAsSeconds() and "time" or "frame"), width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, enable = isAnimatedScene(), } local endSlider = octane.gui.createSlider { width = sliderWidth, enable = isAnimatedScene(), } if displayTimeAsSeconds() then local interval = gSettings.sceneGraph:getAnimationTimeSpan() startSlider.value = gSettings.startTime startSlider.minValue = interval[1] startSlider.maxValue = interval[2] startSlider.step = gSettings.dT endSlider.value = gSettings.endTime endSlider.minValue = interval[1] endSlider.maxValue = interval[2] endSlider.step = gSettings.dT else startSlider.value = gSettings.startFrame startSlider.minValue = 0 startSlider.maxValue = calcSceneFrameCount() - 1 startSlider.step = 1 endSlider.value = gSettings.endFrame endSlider.minValue = 0 endSlider.maxValue = calcSceneFrameCount() - 1 endSlider.step = 1 end -- shutter time local shutterTimeLbl = octane.gui.createLabel { text = string.format("Shutter time (%s)", displayShutterAsSeconds() and "s" or "%"), width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, enable = isAnimatedScene(), } local shutterTimeSlider = octane.gui.createSlider { type = octane.gui.componentType.SLIDER, logarithmic = true, width = sliderWidth, enable = isAnimatedScene(), } if displayShutterAsSeconds() then shutterTimeSlider.value = gSettings.shutterTime shutterTimeSlider.minValue = 0 shutterTimeSlider.maxValue = 1 shutterTimeSlider.step = 0.0001 else shutterTimeSlider.value = gSettings.shutterTime * 100 * gSettings.fps shutterTimeSlider.minValue = 0 shutterTimeSlider.maxValue = 1 shutterTimeSlider.step = 0.1 end -- file numbering local fileNbrLbl = octane.gui.createLabel { text = "File numbering", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, enable = isAnimatedScene(), } local fileNbrBox = octane.gui.createNumericBox { x = 10, minValue = 0, maxValue = 10000, value = gSettings.fileNumber, step = 1, enable = isAnimatedScene() and gSettings.useFileNumber, } local fileNbrCheck = octane.gui.createCheckBox { text = "Start file numbering at:", checked = gSettings.useFileNumber, enable = isAnimatedScene() and fileNbrBox.checked, callback = function(me) if (me.checked) then fileNbrBox.enable = true gSettings.fileNumber = fileNbrBox.value else fileNbrBox.enable = false gSettings.fileNbrBox = 0 end end } local fileNbrGrp = octane.gui.createGroup { border = false, children = { { fileNbrCheck, fileNbrBox } }, padding = { 0 }, inset = { 0 }, enable = isAnimatedScene(), } -- time controls are tightly coupled by this callback local function timeSliderCallback(slider) gSettings.fps = fpsSlider.value gSettings.dT = 1 / fpsSlider.value -- fps changed -> update time interval sliders range if slider == fpsSlider then if displayTimeAsSeconds() then startSlider.step = gSettings.dT endSlider.step = gSettings.dT else local frameCount = calcSceneFrameCount() startSlider.maxValue = frameCount - 1 startSlider.value = math.min(startSlider.value, startSlider.maxValue) endSlider.maxValue = frameCount - 1 endSlider.value = math.min(endSlider.value, endSlider.maxValue) end end if displayTimeAsSeconds() then gSettings.startTime = startSlider.value gSettings.endTime = endSlider.value gSettings.startFrame = timeToFrame(gSettings.startTime) gSettings.endFrame = timeToFrame(gSettings.endTime) else gSettings.startFrame = startSlider.value gSettings.endFrame = endSlider.value gSettings.startTime = frameToTime(gSettings.startFrame, gSettings.fps) gSettings.endTime = frameToTime(gSettings.endFrame , gSettings.fps) end -- make sure start and end don't cross over if (slider == startSlider) then gSettings.startFrame = math.min(gSettings.startFrame, gSettings.endFrame) startSlider.value = math.min(startSlider.value, endSlider.value) else gSettings.endFrame = math.max(gSettings.startFrame, gSettings.endFrame) endSlider.value = math.max(startSlider.value, endSlider.value) end end startSlider.callback = timeSliderCallback endSlider.callback = timeSliderCallback fpsSlider.callback = timeSliderCallback -- override label local overrideLbl = octane.gui.createLabel { text = "Override s/px", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, } local maxSamplexBox = octane.gui.createNumericBox { x = 10, value = gSettings.maxSamples, minValue = 1, maxValue = 256000, step = 1, logarithmic = true, enable = gSettings.overrideMaxSamples, callback = function(me) gSettings.maxSamples = me.value end } local overrideChk = octane.gui.createCheckBox { text = "Samples/px:", checked = false, tooltip = "Override max samples in the kernel node", checked = gSettings.overrideMaxSamples, callback = function(box) if box.checked then maxSamplexBox.enable = box.checked gSettings.overrideMaxSamples = true gSettings.maxSamples = maxSamplexBox.value else maxSamplexBox.enable = false gSettings.overrideMaxSamples = false end end } local overrideGrp = octane.gui.createGroup { border = false, children = { { overrideChk, maxSamplexBox } }, padding = { 0 }, inset = { 0 }, } -- template label local templateLbl = octane.gui.createLabel { text = "Filename template", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, } -- template text editor local templateEditor = octane.gui.createTextEditor { text = gSettings.template, width = width - templateLbl.width - 10, height = 20, enable = true, tooltip = "Template parameters: %i render target index %n node name %e extension %t timestamp %f frame %p render pass name", callback = function(editor) gSettings.template = editor.text end } -- create a button to browse for an output folder local outputButton = octane.gui.createButton { text = "Output folder...", width = BUTTON_WIDTH, height = BUTTON_HEIGHT, tooltip = "Specify the folder to receive the rendered output files." } -- create an editor that will show the chosen file path local txtEditorDirectory = octane.gui.createTextEditor { text = gSettings.outputDirectory or "", width = templateEditor.width, height = 20, enable = true, callback = function(me) gSettings.outputDirectory = me.text end } -- skip existing files local skipExistingLbl = octane.gui.createLabel { text = "Skip existing files", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, } local skipExistingChk = octane.gui.createCheckBox { text = "", checked = gSettings.skipExisting, callback = function(me) gSettings.skipExisting = me.checked end } -- render passes local renderPassLbl = octane.gui.createLabel { text = "Render passes", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, } local renderPassesChk = octane.gui.createCheckBox { text = "Save all enabled passes", checked = gSettings.saveAllPasses, callback = function(me) gSettings.saveAllPasses = me.checked end } local dummyLbl = octane.gui.createLabel { text = "", width = NARROW_LBL_WIDTH, height = LBL_HEIGHT, } local multiLayerExrChk = octane.gui.createCheckBox { text = "Save layered EXR (only when saved as EXR)", checked = gSettings.saveMultiLayerExr, width = txtEditorDirectory.width, callback = function(me) gSettings.saveMultiLayerExr = me.checked end } -- callback function for the output button outputButton.callback = function() -- 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(gSettings.projectPath), browseDirectory = true, save = false, } if not ret.result or ret.result == "" then gSettings.outputDirectory = nil else gSettings.outputDirectory = ret.result txtEditorDirectory.text = gSettings.outputDirectory end end -- group holding it all together return octane.gui.createGroup { children = { { fpsLbl , fpsSlider }, { startTimeLbl , startSlider }, { endTimeLbl , endSlider }, { shutterTimeLbl , shutterTimeSlider }, { fileNbrLbl , fileNbrGrp }, { overrideLbl , overrideGrp }, { templateLbl , templateEditor }, { outputButton , txtEditorDirectory }, { skipExistingLbl, skipExistingChk }, { renderPassLbl , renderPassesChk }, { dummyLbl , multiLayerExrChk }, }, text = "Settings", border = true, inset = { GRP_PAD }, padding = { GRP_PAD }, debug = gSettings.showGrpOutlines, } end -- Inits the progress bar. local function initProgressBar(width) local progressBar = octane.gui.createProgressBar { text = "", width = width-10, height = LBL_HEIGHT } -- the global progress function needs to update the progress bar gSettings.progress = function(progress, text) progressBar.progress = progress progressBar.text = text end return progressBar end -- Inits a GUI group for the control buttons. local function initControlsGroup() local cancelButton = octane.gui.createButton { text = "Cancel", width = BUTTON_WIDTH, height = BUTTON_HEIGHT, tooltip = "Cancel batch processing.", enable = false, callback = function() cancelRendering() end } local startButton = octane.gui.createButton { text = "Start", width = BUTTON_WIDTH, height = BUTTON_HEIGHT, tooltip = "Start batch processing.", callback = function() -- we will hang in this callback until rendering is finished, the only way to stay -- responsive is via the render callback -- clear cancel flag gSettings.cancelled = false -- disable all but the cancel button setEnabled(gSettings.window, false) cancelButton.enable = true -- do the work (blocks) gSettings.batchRender() -- enable all but the cancel button setEnabled(gSettings.window, true) cancelButton.enable = false if not gSettings.cancelled then gSettings.progress(101, "Finished") end end } return octane.gui.create { type = octane.gui.componentType.GROUP, children = { startButton, cancelButton }, rows = 1, cols = 2, border = false, padding = { 2 }, debug = gSettings.showGrpOutlines, } end -- Initializes the main window. local function initMainWindow() -- initialize the main pieces of the user interface local renderTargetOverview = initRenderTargetOverview() local userSettings = initSettingGroup(renderTargetOverview.width) local progressBar = initProgressBar(userSettings.width) local controlsGroup = initControlsGroup() -- these guys go on the window top-to-bottom local children = { renderTargetOverview, userSettings, progressBar, controlsGroup } -- this group holds everything together local layoutGrp = octane.gui.create { type = octane.gui.componentType.GROUP, children = children, cols = 1, rows = #children, border = false, padding = { 2 }, centre = true, debug = gSettings.showGrpOutlines, } -- window that holds all components return octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Batch rendering", children = { layoutGrp }, width = layoutGrp.width, height = layoutGrp.height, callback = function() -- cancel rendering on close cancelRendering() end } end ---------------------------------------------------------------------------------------------------- -- Batch rendering -- The batch rendering function. This will render each frame for each selected render target. gSettings.batchRender = function() -- keeps track of the render progress (in range [0,1]) local progress = 0 local progressStep = calcProgressUnit() -- render each animation frame for frame = gSettings.startFrame,gSettings.endFrame do -- update the time in the scene gSettings.sceneGraph:updateTime(frameToTime(frame)) -- render all the render targets that are enabled for ix, renderTarget in ipairs(gSettings.renderTargets) do if renderTarget.render then -- override max samples if configured if gSettings.overrideMaxSamples then renderTarget.node:getInputNode(octane.P_KERNEL):setPinValue(octane.P_MAX_SAMPLES, gSettings.maxSamples) end -- create the render pass export objects (returns nil when there aren't any passes) local renderPassExportObjs = createRenderPassExportObjs(renderTarget.node) -- 1) save out all the render passes if renderPassExportObjs and gSettings.saveAllPasses then -- a) multi-layer EXR if renderTarget.fileType == octane.render.imageType.EXR and gSettings.saveMultiLayerExr then for _, exportObj in ipairs(renderPassExportObjs) do exportObj.exportName = exportObj.origName end -- create an output path for the image local filename = createFilename(ix, frame, renderTarget.node.name, renderTarget.fileType, "all") local path if gSettings.outputDirectory then path = octane.file.join(gSettings.outputDirectory, filename) else path = string.format("%s (dry-run)", filename) end -- update the progress gSettings.progress(progress, string.format("frame %d/%d - %s -> %s (layerd EXR)", frame, calcSceneFrameCount(), renderTarget.node.name, path)) progress = progress + progressStep local skipFrame = gSettings.skipExisting and gSettings.outputDirectory and octane.file.exists(path) -- do the rendering of the image if not skipFrame then octane.render.start { renderTargetNode = renderTarget.node, callback = function() if gSettings.cancelled then octane.render.callbackStop() end end } end -- cancelled -> bail out and update progress bar if gSettings.cancelled then gSettings.progress(0, "Cancelled") return end -- save out the multi layer EXR if gSettings.outputDirectory and path and not skipFrame then octane.render.saveRenderPassesMultiExr(path, renderPassExportObjs) end -- b) each render pass as a discrete file else -- create file names for each file for _, exportObj in ipairs(renderPassExportObjs) do exportObj.exportName = createFilename(ix, frame, renderTarget.node.name, renderTarget.fileType, exportObj.origName) end local path if gSettings.outputDirectory then path = gSettings.outputDirectory else path = "(dry-run)" end -- update the progress gSettings.progress(progress, string.format("frame %d/%d - %s -> %s (discrete files)", frame, calcSceneFrameCount(), renderTarget.node.name, path)) progress = progress + progressStep -- check if at least 1 file doesn't exits local skipFrame = gSettings.skipFrame if skipFrame then for _, exportObj in ipairs(renderPassExportObjs) do local fullPath = octane.file.join(path, exportObj.exportName) if not octane.file.exists(fullPath) then skipFrame = false break end end end -- do the rendering of the image if not skipFrame then octane.render.start { renderTargetNode = renderTarget.node, callback = function() if gSettings.cancelled then octane.render.callbackStop() end end } end -- cancelled -> bail out and update progress bar if gSettings.cancelled then gSettings.progress(0, "Cancelled") return end -- save out the passes as discrete files if gSettings.outputDirectory and path and not skipFrame then octane.render.saveRenderPasses(path, renderPassExportObjs, renderTarget.fileType) end end -- 2) only save out the beauty pass else -- create an output path for the image local filename = createFilename(ix, frame, renderTarget.node.name, renderTarget.fileType, "") local path if gSettings.outputDirectory then path = octane.file.join(gSettings.outputDirectory, filename) else path = string.format("%s (dry-run)", filename) end -- update the progress gSettings.progress(progress, string.format("frame %d/%d - %s -> %s", frame, calcSceneFrameCount(), renderTarget.node.name, path)) progress = progress + progressStep local skipFrame = gSettings.skipExisting and gSettings.outputDirectory and octane.file.exists(path) -- do the rendering of the image if not skipFrame then octane.render.start { renderTargetNode = renderTarget.node, callback = function() if gSettings.cancelled then octane.render.callbackStop() end end } end -- cancelled -> baild out and update progress bar if gSettings.cancelled then gSettings.progress(0, "Cancelled") return end -- save out the image if gSettings.outputDirectory and path and not skipFrame then octane.render.saveImage(path, renderTarget.fileType) end end end end end end --------------------------------------------------------------------------------------------------- -- Main script -- create a copy of the original project gSettings.sceneGraph = octane.nodegraph.createRootGraph("Project Copy") gSettings.sceneGraph:copyFromGraph(octane.project.getSceneGraph()) loadFromDisk() -- find out the time settings for this scene (if not loaded from disk) local interval = gSettings.sceneGraph:getAnimationTimeSpan() gSettings.shutterTime = gSettings.shutterTime or octane.render.getShutterTime() gSettings.fps = gSettings.fps or octane.project.getProjectSettings():getAttribute(octane.A_FRAMES_PER_SECOND) gSettings.dT = 1 / gSettings.fps gSettings.startTime = gSettings.startTime or interval[1] gSettings.endTime = gSettings.endTime or interval[2] gSettings.startFrame = timeToFrame(gSettings.startTime) gSettings.endFrame = timeToFrame(gSettings.endTime) gSettings.fileNumber = 1 -- fetch all the render target nodes local renderTargetNodes = gSettings.sceneGraph:findNodes(octane.NT_RENDERTARGET, true) -- if no render targets are found -> error out if #renderTargetNodes == 0 then error("No render targets in this project.") end -- sort all the render target nodes alphanumerically alphanumsort(renderTargetNodes) -- initialize the state for the render targets for _, node in ipairs(renderTargetNodes) do local state = { ["node"] = node, ["render"] = true, ["fileType"] = octane.render.imageType.PNG8, } table.insert(gSettings.renderTargets, state) end -- the script blocks here until the window is closes gSettings.window = initMainWindow() gSettings.window:showWindow() storeOnDisk()