-- General animation rendering, this just takes the scene as is and renders -- it at regular time intervals -- -- @description Animation rendering -- @author Roeland Schoukens -- @version 0.2 -- -- helper to pop-up an error dialog local function showError(text) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, title = "Animation Error", text = text, } end -- Returns copies of the original scene graph 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] if not selectedRt or selectedRt.type ~= octane.NT_RENDERTARGET then showError("no render target selected", true) return nil end -- Create a full copy of the whole project so we don't modify the original project. local copyScene = octane.nodegraph.createRootGraph("Project Copy") local copyRt = copyScene:copyFromGraph(octane.project.getSceneGraph(), { selectedRt })[1] -- check if the copied node is a render target with a thinlens camera connected to it if not copyRt or copyRt.type ~= octane.NT_RENDERTARGET then showError("no render target selected", true) return nil 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() if interval[1] >= interval[2] then showError("This scene does not have any animation") return end -- cancel flag and other globals IDLE, RENDERING = 1, 2 status = IDLE startT = 0 endT = 0 dT = 0 currentT = 0 totalframes = 0 -- get from settings if we want frame numbers or time -- enum values TIME_AS_SECONDS = 1 TIME_AS_FRAME = 2 SHUTTER_AS_SECONDS = 1 SHUTTER_AS_PERCENT = 2 local prefs = octane.project.getPreferences() local prs = octane.project.getProjectSettings() local timeDisplay = prefs:getAttribute(octane.A_TIME_DISPLAY) local shutterDisplay = prefs:getAttribute(octane.A_SHUTTER_TIME_DISPLAY) local timeDisplay = prefs:getAttribute(octane.A_TIME_DISPLAY) local shutterDisplay = prefs:getAttribute(octane.A_SHUTTER_TIME_DISPLAY) local sceneFps = prs:getAttribute(octane.A_FRAMES_PER_SECOND) local sceneShutter = octane.render.getShutterTime() -- the actual rendering is quite easy. -- Render callback function renderCallback(result) if status == IDLE then octane.render.callbackStop() end local t = currentT + dT * result.samples / samplesSlider.value t = (t - startT) / (totalframes * dT) progressBar.progress = t end -- Render main loop function render() status = RENDERING local shutter = shutterSlider.value if shutterDisplay == SHUTTER_AS_PERCENT then shutter = shutter / 100 * dT end if endT < startT then return end local file = fileEditor and fileEditor.text or "" -- 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.text = prefix..sequenceMatch..suffix end octane.render.setShutterTime(shutter) -- set the max samples value of the kernel input node to make sure -- it's not less than the current setting local kernel = rtNode:getInputNode(octane.P_KERNEL) if kernel ~= nil then kernel:setPinValue(octane.P_MAX_SAMPLES, samplesSlider.value, false) end currentT = startT local seq = 0 frameNumber = startSlider.value if timeDisplay == TIME_AS_SECONDS then frameNumber = startSlider.value/dT end for seq = 1,totalframes do -- time and sequence number copyScene:updateTime(currentT) progressBar.text="Frame "..seq.."/"..totalframes local result = octane.render.start { renderTargetNode = rtNode, callback = renderCallback, maxSamples = samplesSlider.value } if status == IDLE then -- canceled progressBar.progress = 0 progressBar.text="Canceled" return elseif file ~= "" then -- see if a file was given, and replace the sequence number local thisFile = prefix..seqPattern:format(frameNumber)..suffix fileEditor.text = thisFile octane.render.saveImage(thisFile, octane.render.imageType.PNG8) end -- go to next time stamp and update progress bar currentT = currentT + dT frameNumber = frameNumber + 1 local t = (currentT - startT) / (totalframes * dT) progressBar.progress = t end progressBar.text="Done" status = IDLE end function cancel() status = IDLE 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 timeStr = (timeDisplay == TIME_AS_SECONDS) and "time" or "frame" local startLbl = createLabel("Start "..timeStr) local endLbl = createLabel("End "..timeStr) local fpsLbl = createLabel("Framerate") local shutterUnit = (shutterDisplay == SHUTTER_AS_SECONDS) and "sec" or "%" local shutterLbl = createLabel("Shutter time ("..shutterUnit..")") local samplesLbl = createLabel("Samples/px") dT = 1 / sceneFps if timeDisplay == TIME_AS_SECONDS then startSlider = createSlider(interval[1], interval[1], interval[2], dT) endSlider = createSlider(interval[2], interval[1], interval[2], dT) else -- for calculating the total frame count we have to consider rounding -- errors. The start and end sliders are inclusive bounds, so eg. with 24 -- FPS an interval of [3/24, 5/24] will render 3 frames. totalframes = math.max(0, 1 + math.floor((interval[2] - interval[1] + .0001*dT) / dT)) startSlider = createSlider(0 , 0, totalframes - 1, 1) endSlider = createSlider(totalframes - 1, 0, totalframes - 1, 1) end fpsSlider = createSlider(sceneFps, 1, 120 , .1) if shutterDisplay == SHUTTER_AS_SECONDS then shutterSlider = createSlider(sceneShutter, 0, 1 , .0001) else shutterSlider = createSlider(sceneShutter * 100 / dT, 0, 1000 , .1) end samplesSlider = createSlider(400 , 1, 16000, 1) samplesSlider.logarithmic = true shutterSlider.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 = 5, -- 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 } -- file output local fileGrpChildren = nil local fileChooseButton = nil fileEditor = nil if octane.render.saveImage then -- create a button to show a file chooser 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, } fileGrpChildren = { fileChooseButton, fileEditor } else local label = createLabel("Saving images is not available in the demo version") label.width = 500 fileGrpChildren = { label } end -- 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 = #fileGrpChildren, children = fileGrpChildren, padding = { 2 }, inset = { 5 }, } -- eye candy, a progress bar progressBar = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, width = fileGrp.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 RENDERTXT = "Start render" local STOPTXT = "Stop render" local EXITTXT = "Exit" local renderButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = RENDERTXT, width = 120, height = 20, } local exitButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = EXITTXT, width = 120, height = 20, } local buttonGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 2, children = { renderButton, exitButton }, 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.width, -- same dimensions as the layout group height = layoutGrp.height, } -- gui callback inputwidgets = {startSlider, endSlider, fpsSlider, shutterSlider, samplesSlider, fileChooseButton, fileEditor} -- these last two may be nil, making this a table with 5 elements. function guiCallback(component, event) if event == octane.gui.eventType.BUTTON_CLICK then if component == fileChooseButton then -- choose an output file local file = fileEditor.text if octane.file.isAbsolute(file) then file = octane.file.getParentDirectory(file) else file = nil end local ret = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Choose the output file", wildcards = "*.png", path = file, save = true, } -- if a file is chosen if ret.result ~= "" then fileEditor. text = ret.result end elseif component == renderButton then if status == IDLE then renderButton.text = STOPTXT for _, c in pairs(inputwidgets) do c.enable = false end -- start render render() renderButton.text = RENDERTXT for _, c in pairs(inputwidgets) do c.enable = true end elseif status == RENDERING then cancel() end elseif component == exitButton then animationWindow:closeWindow() end elseif event == octane.gui.eventType.WINDOW_CLOSE then cancel() end end function updateFramesCallback(component, event) dT = 1 / fpsSlider.value -- if the fps change, and the time sliders are using frame numbers, we have to update the bounds if component == fpsSlider and event == octane.gui.eventType.VALUE_CHANGE then if timeDisplay == TIME_AS_FRAME then sceneframes = math.max(0, 1 + math.floor((interval[2] - interval[1] + .0001*dT) / dT)) startSlider.maxValue = sceneframes - 1 startSlider.value = math.min(startSlider.value, sceneframes - 1) endSlider .maxValue = sceneframes - 1 endSlider.value = math.min(endSlider.value, sceneframes - 1) else startSlider.step = dT endSlider.step = dT end end startT = startSlider.value endT = endSlider.value if timeDisplay == TIME_AS_FRAME then startT = interval[1] + dT * startT endT = interval[1] + dT * endT end -- for calculating the total frame count we have to consider rounding -- errors. The start and end sliders are inclusive bounds, so eg. with 24 -- FPS an interval of [3/24, 5/24] will render 3 frames. totalframes = math.max(0, 1 + math.floor((endT - startT + .0001*dT) / dT)) progressBar.text="Frames: "..totalframes if event == octane.gui.eventType.VALUE_CHANGE then if component == startSlider then if endT < startT then endT = startT endSlider.value = startSlider.value end elseif component == endSlider then if endT < startT then startT = endT startSlider.value = endSlider.value end end end end -- hookup the callback with all the GUI elements if fileChooseButton then fileChooseButton.callback = guiCallback end renderButton.callback = guiCallback exitButton.callback = guiCallback animationWindow.callback = guiCallback animationWindow.callback = guiCallback startSlider.callback = updateFramesCallback endSlider.callback = updateFramesCallback fpsSlider.callback = updateFramesCallback updateFramesCallback() animationWindow:showWindow()