---------------------------------------------------------------------------------------------------- -- This is a modified version of the Octane batch rendering script to make it work from command line. -- -- Accepted arguments (optional): output path, render targets. image format, color space, start frame, end frame, max samples. -- -- Usage example with command line : -- octane.exe C:\cmdTest.orbx --no-gui --script C:\cmdBatchRender.lua -A C:/images/ 1 100 1024 EXR_16 LINEAR_SRGB renderTarget -- -- You can declare arguments in any order, -- except number argument will have an order : start frame, end frame, max samples.(as I have no way to guess which number argument is for what) -- and specific image format/color space need to be declared before a render target. -- -- If no arguments in the command line, then all render targets are rendered, -- using default settings from the script and the complete timeline, -- and an "images" folder will be created next to the orbx file. -- -- keywords for image format: PNG_8, PNG_16, EXR_16, EXR_32 -- keywords for color space: SRGB, LINEAR_SRGB, ACES2065_1, ACESCG, OCIO -- -- @author Pascal ANDRE (Calus) based on batch rendering script 0.43 shipping with Octane -- @description Batch rendering for cmd line -- @version 0.2 (modified) -- Common code for the render scripts. require("octane_render_utils_lua") -- Global table with all default settings. 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 = octane.file.getParentDirectory(octane.project.getCurrentProject()) .. "/images", -- true to override the max samples/px overrideMaxSamples = false, -- max samples/px maxSamples = 512, -- filename template for the output files template = "%n_%p_%f.%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, -- true if we use custom file numbering useFileNumbering = false, -- frame number from which we start number files fileNumber = 0, -- skip existing files skipExisting = false, -- save all enabled render passes saveAllPasses = true, -- save the render passes as a layered exr saveMultiLayerExr = true, -- save additional deep image output saveDeepImage = false, -- openExr compression Type exrCompressionType = octane.exrCompressionType.ZIP, -- how many sub frames to render subFrameCount = nil, -- saves denoiser output as main passes if enabled saveDeBeautyAsMain = true, -- image save format imageSaveFormat = "PNG_8", -- color space colorSpace = "SRGB", -- OCIO color space name ocioColorSpaceName = "", -- OCIO look name ocioLookName = "", -- force tone mapping forceToneMapping = false, } local function cancelRendering() gSettings.cancelled = true octane.render.callbackStop() 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))) / gSettings.subFrameCount end local imageSaveFormatNames = { "PNG (8-bit)", "PNG (16-bit)", "EXR (16-bit)", "EXR (32-bit)", } local imageSaveFormatIds = { octane.imageSaveFormat.PNG_8, octane.imageSaveFormat.PNG_16, octane.imageSaveFormat.EXR_16, octane.imageSaveFormat.EXR_32, } local function imageSaveFormatIdToIx(id) for i, v in ipairs(imageSaveFormatIds) do if v == id then return i end end end local function isExrImageSaveFormat(type) return (type == octane.imageSaveFormat.EXR_16 or type == octane.imageSaveFormat.EXR_32) end -- Returns a color space info table suitable for rendering based on the currently selected options. local function buildColorSpaceInfo(renderTarget) -- get the color space and resolve the meaning of "other" local colorSpace = renderTarget.colorSpace if colorSpace == octane.namedColorSpace.OTHER then -- this means use the default color space for the chosen file type if isExrImageSaveFormat(renderTarget.imageSaveFormat) then colorSpace = octane.namedColorSpace.LINEAR_SRGB else colorSpace = octane.namedColorSpace.SRGB end end -- if an OCIO color space is selected, build an OCIO color space info if colorSpace == octane.namedColorSpace.OCIO then local curveType if isExrImageSaveFormat(renderTarget.imageSaveFormat) then -- EXR files should be interpreted by any software as scene-linear curveType = octane.colorSpaceCurveType.LINEAR else -- PNG files should be interpreted by any software as sRGB curveType = octane.colorSpaceCurveType.UNIFORM end return { type = octane.outputColorSpaceType.OCIO_COLOR_SPACE, ocioColorSpaceName = renderTarget.ocioColorSpaceName, ocioLookName = renderTarget.ocioLookName, forceToneMapping = renderTarget.forceToneMapping, ocioColorSpaceCurveType = curveType, } end -- build a known color space info return { type = octane.outputColorSpaceType.KNOWN_COLOR_SPACE, colorSpace = colorSpace, forceToneMapping = renderTarget.forceToneMapping, } end -- The batch rendering function. This will render each frame for each selected render target. gSettings.batchRender = function() -- Interactive render region should not be active when running batch render script. local renderRegion = { active = false } octane.render.setRenderRegion(renderRegion) local progress = 0 local progressStep = calcProgressUnit() local lastFrameIx = octaneRenderUtils.calculateLastFrame(gSettings.sceneGraph, gSettings.fps) -- create the output directory if it does not exist yet if gSettings.outputDirectory and octane.file.isAbsolute(gSettings.outputDirectory) and not octane.file.exists(gSettings.outputDirectory) then octane.file.createDirectory(gSettings.outputDirectory) end -- all render targets that need rendering local haveMultipleAovFiles = false local enabledRenderTargets = {} for _, renderTarget in ipairs(gSettings.renderTargets) do if renderTarget.render then enabledRenderTargets[#enabledRenderTargets + 1] = renderTarget if not haveMultipleAovFiles and gSettings.saveAllPasses and (not isExrImageSaveFormat(renderTarget.imageSaveFormat) or not gSettings.saveMultiLayerExr) and octaneRenderUtils.hasRenderPasses(renderTarget.node) then haveMultipleAovFiles = true end end end -- safety check before we start rendering. local errorMsg = octaneRenderUtils.verifyFilenameTemplate( gSettings.template, #enabledRenderTargets > 1, gSettings.startFrame ~= gSettings.endFrame, gSettings.subFrameCount > 1, haveMultipleAovFiles) if errorMsg ~= "" then octane.gui.showError(errorMsg, "Warning") end -- get start file number local startFileNum = octaneRenderUtils.ternaryOperator(gSettings.useFileNumbering, gSettings.fileNumber, 0) -- for every frame: for frameIx = gSettings.startFrame, gSettings.endFrame do -- update the time in the scene gSettings.sceneGraph:updateTime(octaneRenderUtils.frameToTime(frameIx, gSettings.fps)) -- for every render target: for renderTargetIx, renderTarget in ipairs(enabledRenderTargets) do -- override samples/px if gSettings.overrideMaxSamples then octaneRenderUtils.setMaxSamples(renderTarget.node, gSettings.maxSamples) end -- for every sub-frameIx: for subFrameIx = 1, gSettings.subFrameCount do if gSettings.subFrameCount > 1 then octaneRenderUtils.setSubFrameInterval(renderTarget.node, subFrameIx, gSettings.subFrameCount) end -- 1) save out all the render passes if octaneRenderUtils.hasRenderPasses(renderTarget.node) and gSettings.saveAllPasses then -- a) multi-layer EXR if isExrImageSaveFormat(renderTarget.imageSaveFormat) and gSettings.saveMultiLayerExr then local renderPassExportObjs = octaneRenderUtils.createMultiLayerExrExports( renderTarget.node) -- create an output path for the image local filename = octaneRenderUtils.createFilename( gSettings.template, renderTargetIx, frameIx + startFileNum, subFrameIx, renderTarget.node.name, renderTarget.imageSaveFormat, "all") if gSettings.outputDirectory then path = octane.file.join(gSettings.outputDirectory, filename) else path = string.format("%s [dry-run]", filename) end -- update the progress local status if gSettings.subFrameCount > 1 then status = string.format("frameIx %d.%d/%d - %s -> %s (layered EXR)", frameIx, subFrameIx, lastFrameIx, renderTarget.node.name, path) else status = string.format("frameIx %d/%d - %s -> %s (layered EXR)", frameIx, lastFrameIx, renderTarget.node.name, path) end -- and increase the progress counter progress = progress + progressStep print(string.format("%.1f", (progress*100)) , " % of the sequence") 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 if not octane.render.saveRenderPassesMultiExr2( path, renderPassExportObjs, renderTarget.imageSaveFormat == octane.imageSaveFormat.EXR_16, buildColorSpaceInfo(renderTarget), gSettings.exrCompressionType, nil, false) then error("failed to save render passes in multi-layer EXR"); end end -- b) each render pass as a discrete file else local renderPassExportObjs = octaneRenderUtils.createDiscreteRenderPassExports( renderTarget.node, gSettings.outputDirectory or "", gSettings.template, renderTargetIx, frameIx + startFileNum, subFrameIx, renderTarget.imageSaveFormat) local path if gSettings.outputDirectory then path = gSettings.outputDirectory else path = "[dry-run]" end -- update the progress local status if gSettings.subFrameCount > 1 then status = string.format("frame %d.%d/%d - %s -> %s (discrete files)", frameIx, subFrameIx, lastFrameIx, renderTarget.node.name, path) else status = string.format("frame %d/%d - %s -> %s (discrete files)", frameIx, lastFrameIx, renderTarget.node.name, path) end -- update the progress counter progress = progress + progressStep print(string.format("%.1f", (progress*100)) , " % of the sequence") -- check if at least 1 file doesn't exits local skipFrame = gSettings.skipExisting 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 local ok = octane.render.saveRenderPasses2(path, renderPassExportObjs, renderTarget.imageSaveFormat, buildColorSpaceInfo(renderTarget), gSettings.exrCompressionType, false, nil) if not ok then error("failed to save render passes to discrete files") end end end -- 2) only save out the beauty pass else -- figure out whether we need to save the denoiser output or the main pass. local renderPassId = octane.renderPassId.BEAUTY if octaneRenderUtils.isDenoiserEnabled(renderTarget.node) and gSettings.saveDeBeautyAsMain then renderPassId = octane.renderPassId.BEAUTY_DENOISER_OUTPUT end -- create an output path for the image local filename = octaneRenderUtils.createFilename( gSettings.template, renderTargetIx, frameIx + startFileNum, subFrameIx, renderTarget.node.name, renderTarget.imageSaveFormat, octaneRenderUtils.getRenderPassName(renderPassId)) 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 local status if gSettings.subFrameCount > 1 then status = string.format("frame %d.%d/%d - %s -> %s", frameIx, subFrameIx, lastFrameIx, renderTarget.node.name, path) else status = string.format("frame %d/%d - %s -> %s", frameIx, lastFrameIx, renderTarget.node.name, path) end -- update the progress counter progress = progress + progressStep print(string.format("%.1f", (progress*100)) , " % of the sequence") 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 image if gSettings.outputDirectory and path and not skipFrame then local ok = octane.render.saveRenderPass2(renderPassId, path, renderTarget.imageSaveFormat, buildColorSpaceInfo(renderTarget), gSettings.exrCompressionType, false) if not ok then error("failed to save image") end 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()) -- find out the time settings for this scene (if not loaded from disk) local interval = gSettings.sceneGraph:getAnimationTimeSpan() 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] -- When start and end time are loaded from disk, make sure they make sense for the current animation. gSettings.startTime = math.max(gSettings.startTime, interval[1]) gSettings.endTime = math.min(gSettings.endTime, interval[2]) gSettings.startFrame, gSettings.endFrame = octaneRenderUtils.timespanToFrames(gSettings.startTime, gSettings.endTime, gSettings.fps) gSettings.subFrameCount = gSettings.subFrameCount or 1 gSettings.fileNumber = gSettings.fileNumber or 0 -- 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 render target nodes by name octaneRenderUtils.alphanumsort(renderTargetNodes, function(node) return node.name end) -- cmdline arguments if #arg>0 then local numcount = 0 for i,v in ipairs(arg) do if tonumber(v) then numcount = numcount + 1 if numcount == 1 then gSettings.startFrame = v elseif numcount == 2 then gSettings.endFrame = v elseif numcount == 3 then gSettings.maxSamples = v overrideMaxSamples = true end elseif octane.imageSaveFormat[v] then gSettings.imageSaveFormat = v elseif octane.namedColorSpace[v] then gSettings.colorSpace = v elseif octane.file.isAbsolute(v) then gSettings.outputDirectory = v else for _, node in ipairs(renderTargetNodes) do if node.name == v then table.insert(gSettings.renderTargets,{node = node, render = true, imageSaveFormat = octane.imageSaveFormat[gSettings.imageSaveFormat], colorSpace = octane.namedColorSpace[gSettings.colorSpace], ocioColorSpaceName = gSettings.ocioColorSpaceName, ocioLookName = gSettings.ocioLookName, forceToneMapping = gSettings.forceToneMapping}) end end end end end if #gSettings.renderTargets == 0 then for _, node in ipairs(renderTargetNodes) do table.insert(gSettings.renderTargets, {node = node, render = true, imageSaveFormat = octane.imageSaveFormat[gSettings.imageSaveFormat], colorSpace = octane.namedColorSpace[gSettings.colorSpace], ocioColorSpaceName = gSettings.ocioColorSpaceName, ocioLookName = gSettings.ocioLookName, forceToneMapping = gSettings.forceToneMapping}) end end -- run batch render gSettings.batchRender()