---------------------------------------------------------------------------------------------------- -- 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.49 shipping with Octane -- @description Batch rendering for cmd line -- @version 0.2 (modified to work with Octane 2023.1) -- 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, -- warning message to display when finished if deep image couldn't be saved deepImageWarning = 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, -- use premultiplied alpha when saving exr or tiff premultipliedAlpha = true, -- openExr compression Type exrCompressionType = octane.exrCompressionType.ZIP, -- openExr compression factor used for DWA exrCompressionLevel = 45, -- TIFF compression type tiffCompressionType = octane.tiffCompressionType.LZW, -- JPEG quality [1..100] jpegQuality = 75, -- 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 = octaneRenderUtils.IMAGE_SAVE_FORMAT_NAMES local imageSaveFormatIds = octaneRenderUtils.IMAGE_SAVE_FORMATS local function imageSaveFormatIdToIx(id) for i, v in ipairs(imageSaveFormatIds) do if v == id then return i end end end local function exrCompressionTypeIdToIx(id) for i, v in ipairs(octaneRenderUtils.COMPRESSION_TYPES_EXR) do if v == id then return i end end end local function tiffCompressionTypeIdToIx(id) for i, v in ipairs(octaneRenderUtils.COMPRESSION_TYPES_TIFF) do if v == id then return i end end end local function getImageExportSettings(renderTarget) return octaneRenderUtils.composeImageExportSettings(renderTarget.imageSaveFormat, gSettings) end local function buildColorSpaceInfo(renderTarget) return octaneRenderUtils.buildColorSpaceInfo( renderTarget.imageSaveFormat, renderTarget.colorSpace, renderTarget.ocioColorSpaceName, renderTarget.ocioLookName, renderTarget.forceToneMapping) end -- The batch rendering function. This will render each frame for each selected render target, and -- return how long rendering took in total (in seconds) or zero if rendering was canceled. 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 completedTime = 0 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 octaneRenderUtils.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 -- update the progress print(string.format("%.1f%% of the sequence", progress * 100)) -- 1) save out all the render passes if octaneRenderUtils.hasRenderPasses(renderTarget.node) and gSettings.saveAllPasses then -- a) multi-layer EXR if octaneRenderUtils.isExrImageSaveFormat(renderTarget.imageSaveFormat) and gSettings.saveMultiLayerExr then -- 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 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, "Canceled") return 0 end -- increase the progress counter progress = progress + progressStep completedTime = completedTime + octane.render.getRenderResultStatistics().renderTime -- save out the multi layer EXR if gSettings.outputDirectory and path and not skipFrame then if not octane.render.saveRenderPassesMultiExr3( path, nil, renderTarget.imageSaveFormat == octane.imageSaveFormat.EXR_16, buildColorSpaceInfo(renderTarget), gSettings.premultipliedAlpha, getImageExportSettings(renderTarget), 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 -- check if at least 1 file doesn't exist 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, "Canceled") return 0 end -- update the progress counter progress = progress + progressStep completedTime = completedTime + octane.render.getRenderResultStatistics().renderTime -- save out the passes as discrete files if gSettings.outputDirectory and path and not skipFrame then local premultipliedAlphaType if octaneRenderUtils.supportsPremultipliedAlpha(renderTarget.imageSaveFormat) and gSettings.premultipliedAlpha then premultipliedAlphaType = octane.premultipliedAlphaType.LINEARIZED else premultipliedAlphaType = octane.premultipliedAlphaType.NONE end local ok = octane.render.saveRenderPasses3(path, renderPassExportObjs, renderTarget.imageSaveFormat, buildColorSpaceInfo(renderTarget), premultipliedAlphaType, getImageExportSettings(renderTarget), 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 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, "Canceled") return 0 end -- update the progress counter progress = progress + progressStep completedTime = completedTime + octane.render.getRenderResultStatistics().renderTime -- save out the image if gSettings.outputDirectory and path and not skipFrame then local premultipliedAlphaType if octaneRenderUtils.supportsPremultipliedAlpha(renderTarget.imageSaveFormat) and gSettings.premultipliedAlpha then premultipliedAlphaType = octane.premultipliedAlphaType.LINEARIZED else premultipliedAlphaType = octane.premultipliedAlphaType.NONE end local ok = octane.render.saveRenderPass3(renderPassId, path, renderTarget.imageSaveFormat, buildColorSpaceInfo(renderTarget), premultipliedAlphaType, getImageExportSettings(renderTarget), false) if not ok then error("failed to save image") end end end end end end return completedTime 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() print("Completed")