---------------------------------------------------------------------------------------------------- -- 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, 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/ -a 1 -a 100 -a 1024 -a EXR16 -a 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 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 : PNG8, PNG16, EXR16, EXR16TONEMAPPED, EXR, EXRTONEMAPPED -- -- -- @author Pascal ANDRE (Calus) based on batch rendering script 0.42 shipping with Octane -- @description Batch rendering for cmd line -- @version 0.2 -- 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, -- generate Composite Project for photoshop generateCompositePrj = false, -- how many sub frames to render subFrameCount = nil, -- saves denoiser output as main passes if enabled saveDeBeautyAsMain = true, -- image save type imageType = "PNG16" } 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 imageTypeNames = { "PNG (8-bit)", "PNG (16-bit)", "EXR (16-bit) Untonemapped", "EXR (16-bit) Tonemapped", "EXR (32-bit) Untonemapped", "EXR (32-bit) Tonemapped" } local imageTypeIds = { octane.imageSaveType.PNG8, octane.imageSaveType.PNG16, octane.imageSaveType.EXR16, octane.imageSaveType.EXR16TONEMAPPED, octane.imageSaveType.EXR, octane.imageSaveType.EXRTONEMAPPED, } local function imageTypeIdToIx(id) for i, v in ipairs(imageTypeIds) do if v == id then return i end end 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 enabledRenderTargets = {} for _, renderTarget in ipairs(gSettings.renderTargets) do if renderTarget.render then enabledRenderTargets[#enabledRenderTargets + 1] = renderTarget end end -- saftey check before we start rendering. for renderTargetIx, renderTarget in ipairs(enabledRenderTargets) do if octaneRenderUtils.hasRenderPasses(renderTarget.node) and gSettings.saveAllPasses then local isExrFileType = (renderTarget.fileType ~= octane.imageSaveType.PNG8 and renderTarget.fileType ~= octane.imageSaveType.PNG16) if isExrFileType == false or gSettings.saveMultiLayerExr == false then if not string.match(gSettings.template, "%%p") then octane.gui.showError( "Save all render passes option is enabled for a render target, but %p (render pass name)".. " is missing in the filename template. The file names for all the passes will be same ".. " and will be overwriting each other", "Warning") end end end 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 local isExrFileType = (renderTarget.fileType ~= octane.imageSaveType.PNG8 and renderTarget.fileType ~= octane.imageSaveType.PNG16) if isExrFileType 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.fileType, "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 local isToneMap = (renderTarget.fileType == octane.imageSaveType.EXRTONEMAPPED or renderTarget.fileType == octane.imageSaveType.EXR16TONEMAPPED) local saveExrIn16Bit = (renderTarget.fileType == octane.imageSaveType.EXR16 or renderTarget.fileType == octane.imageSaveType.EXR16TONEMAPPED) -- save out the multi layer EXR if gSettings.outputDirectory and path and not skipFrame then if not octane.render.saveRenderPassesMultiExr( path, renderPassExportObjs, gSettings.exrCompressionType, saveExrIn16Bit, nil, false, gSettings.generateCompositePrj, isToneMap) 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.fileType) 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 --composite fileName will be either nil or same as rendered image file name local compositeFileName = nil if gSettings.generateCompositePrj then compositeFileName = octaneRenderUtils.createFilename(gSettings.template, renderTargetIx, frameIx + startFileNum, subFrameIx, renderTarget.node.name, renderTarget.fileType, "all") compositeFileName = octane.file.join(path, compositeFileName) end -- save out the passes as discrete files if gSettings.outputDirectory and path and not skipFrame then local ok = octane.render.saveRenderPasses(path, renderPassExportObjs, renderTarget.fileType, false, compositeFileName, nil, gSettings.exrCompressionType) 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.fileType, 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.saveRenderPass(renderPassId, path, renderTarget.fileType, false, 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.imageSaveType[v] then gSettings.imageType = 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, fileType = octane.imageSaveType[gSettings.imageType]}) end end end end end if #gSettings.renderTargets == 0 then for _, node in ipairs(renderTargetNodes) do table.insert(gSettings.renderTargets, {node = node, render = true, fileType = octane.imageSaveType[gSettings.imageType]}) end end -- run batch render gSettings.batchRender()