here is the lua script for the batch rendering from octane.
----------------------------------------------------------------------------------------------------
-- 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. <rendertargetname>_01.png, <rendertargetname>_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/algorithm ... ans-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) == octane.timeDisplay.SECONDS
end
local function displayShutterAsSeconds()
return octane.project.getPreferences():getAttribute(octane.A_SHUTTER_TIME_DISPLAY) == octane.shutterTimeDisplay.SECONDS
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
-- don't prefix the name of the beauty pass, the beauty pass should be
-- in the RGBA channels
if exportObj.renderPassId == octane.renderPassId.BEAUTY then
exportObj.exportName = ""
else
exportObj.exportName = exportObj.origName
end
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()