-- helper to pop-up an error dialog and optionally halts the script local function showError(text) octane.gui.showDialog { type = octane.gui.dialogType.ERROR_DIALOG, title = "Animation Error", text = text, } end -- Returns copies of the original scene graph, the camera node and the rendertarget. -- This prevents us from modifying the original scene. local function getSceneCopy() -- get the selected render target local selectedRt = octane.project.getSelection()[1] -- find the index of the selected render target node local idx = -1 for i, item in ipairs(octane.nodegraph.getRootGraph():getOwnedItems()) do if item == selectedRt then idx = i break end end -- see if we could find the node if idx == -1 then showError("no render target selected") return end -- create a full copy of the project so that we don't modify the original project local copyScene = octane.nodegraph.createRootGraph("Project Copy") copies = copyScene:copyFrom(octane.nodegraph.getRootGraph():getOwnedItems()) copyRt = copies[idx] -- check if the copied node is a render target with a thinlens camera connected to it if not copyRt or copyRt:getProperties().type ~= octane.NT_RENDERTARGET then showError("no render target selected") return end return copyScene, copyRt end -- copy the scene, so we don't modify the original scene. copyScene, rtNode = getSceneCopy() if rtNode == nil then return end local interval = copyScene:getAnimationTimeSpan() -- cancel flag and other globals isCanceled = false startT = 0 endT = 0 dT = 0 currentT = 0 -- the actual rendering is quite easy. -- Render callback function renderCallback(result) if isCanceled then octane.render.stop() end local t = currentT + dT * result.samples / result.maxSamples t = (t - startT) / (endT - startT) progressBar:updateProperties{progress = t} end -- Render main loop function render() startT = startSlider:getProperties().value endT = endSlider:getProperties().value dT = 1 / fpsSlider:getProperties().value local shutter = shutterSlider:getProperties().value if endT < startT then return end local file = fileEditor:getProperties().text -- check if we have a file name given local prefix, sequenceMatch, suffix -- If we have a file name, check where to substitute the sequence number: -- pattern tutorial here: http://lua-users.org/wiki/PatternsTutorial if file ~= "" then -- strip png extension file = file:gsub("%.png$", "") -- split file into prefix, sequence number and suffix -- make sure the sequence number is in the final file name part prefix, sequenceMatch, suffix = file:match("(.-)(%d+)([^\\/]*)$") -- if the file name doesn't contain a sequence number, the match fails -- so just assume it is all prefix. if sequenceMatch == nil then prefix = file sequenceMatch = "0000" suffix = "" end -- pattern for string.format. seqPattern = "%0"..sequenceMatch:len().."d" -- add png extension suffix = suffix..".png" -- display resulting file name fileEditor:updateProperties{text = prefix..sequenceMatch..suffix} end octane.render.setShutterTime(shutter) currentT = startT local seq = 0 -- careful with rounding errors while currentT < endT + .001 * dT do -- time and sequence number seq = seq + 1 copyScene:updateTime(currentT) -- render local result = octane.render.start { renderTargetNode = rtNode, callback = renderCallback, maxSamples = samplesSlider:getProperties().value } if isCanceled then -- canceled progressBar:updateProperties{progress = 0} return elseif file ~= "" then -- see if a file was given, and replace the sequence number local thisFile = prefix..seqPattern:format(seq)..suffix fileEditor:updateProperties{text = thisFile} octane.render.saveImage(thisFile, octane.render.imageType.PNG8) end -- go to next time stamp and update progress bar currentT = currentT + dT local t = (currentT - startT) / (endT - startT) progressBar:updateProperties{progress = t} end end -- creates a text label and returns it local function createLabel(text) return octane.gui.create { type = octane.gui.componentType.LABEL, -- type of component text = text, -- text that appears on the label width = 100, -- width of the label in pixels height = 24, -- height of the label in pixels } end -- creates a slider and returns it local function createSlider(value, min, max, step) return octane.gui.create { type = octane.gui.componentType.SLIDER, -- type of the component width = 400, -- width of the slider in pixels height = 20, -- height of the slider in pixels value = value, -- value of the slider minValue = min, -- minimum value of the slider maxValue = max, -- maximum value of the slider step = step, -- interval between 2 discrete slider values } end -- lets create a bunch of labels and sliders local startLbl = createLabel("Start time") local endLbl = createLabel("End time") local fpsLbl = createLabel("Framerate") local shutterLbl = createLabel("Shutter time") local samplesLbl = createLabel("Samples/px") startSlider = createSlider(interval[1], interval[1], interval[2], .001) endSlider = createSlider(interval[2], interval[1], interval[2], .001) fpsSlider = createSlider(24 , 1, 120 , .1) shutterSlider = createSlider(1/24, 0, 1 , .0001) samplesSlider = createSlider(400 , 1, 16000, 1) samplesSlider:updateProperties{logarithmic = true} shutterSlider:updateProperties{logarithmic = true} -- manual layouting is tedious so let's add all our stuff in a group. local settingsGrp = octane.gui.create { type = octane.gui.componentType.GROUP, -- type of component text = "Settings", -- title for the group rows = 7, -- number of rows in the grid cols = 2, -- number of colums in the grid -- the children is a list of child component that go in each cell. The cells -- are filled left to right, top to bottom. I just formatted the list to show -- where each component goes in the grid. children = { startLbl , startSlider , endLbl , endSlider , fpsLbl , fpsSlider , shutterLbl , shutterSlider , samplesLbl , samplesSlider , }, padding = { 2 }, -- internal padding in each cell inset = { 5 }, -- inset of the group component itself } -- create a button to show a file chooser local fileChooseButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Output...", width = 80, height = 20, } -- create an editor that will show the chosen file path fileEditor = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", x = 20, width = 400, height = 20, } -- for layouting the button and the editor we use a group local fileGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "Output", rows = 1, cols = 2, children = { fileChooseButton, fileEditor, }, padding = { 2 }, inset = { 5 }, } -- eye candy, a progress bar progressBar = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, text = "render progress", width = fileGrp:getProperties().width * 0.8, -- as wide as the group above height = 20, } -- for layouting the progress bar local progressGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 1, children = { progressBar }, padding = { 10 }, centre = true, -- centre the progress bar in it's cell border = false, } -- render & cancel buttons local renderButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Render", width = 80, height = 20, } local cancelButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Stop", width = 80, height = 20, enable = false, } local buttonGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 2, children = { renderButton, cancelButton }, padding = { 5 }, border = false, } -- group that layouts the other groups local layoutGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 4, cols = 1, children = { settingsGrp, fileGrp, progressGrp, buttonGrp }, centre = true, padding = { 2 }, border = false, debug = false, -- true to show the outlines of the group, handy } -- window that holds all components local animationWindow = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Render Animation", children = { layoutGrp }, width = layoutGrp:getProperties().width, -- same dimensions as the layout group height = layoutGrp:getProperties().height, } -- gui callback function guiCallback(component, event) if event == octane.gui.eventType.BUTTON_CLICKED then if component == fileChooseButton then -- choose an output file local ret = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Choose the output file", wildcards = "*.png", save = true, } -- if a file is chosen if ret.result ~= "" then fileEditor:updateProperties{ text = ret.result } end elseif component == renderButton then isCanceled = false renderButton:updateProperties{enable = false} cancelButton:updateProperties{enable = true} render() renderButton:updateProperties{enable = true} cancelButton:updateProperties{enable = false} elseif component == cancelButton then isCanceled = true end elseif event == octane.gui.eventType.WINDOW_CLOSE then isCanceled = true end end -- hookup the callback with all the GUI elements fileChooseButton:updateProperties { callback = guiCallback } renderButton:updateProperties { callback = guiCallback } cancelButton:updateProperties { callback = guiCallback } animationWindow:updateProperties { callback = guiCallback } animationWindow:showWindow()