---------------------------------------------------------------------------------------------------- -- Daylight animation rendering. -- -- @author Octane Dev Team -- @description Renders a daylight animation -- @version 0.2 -- @script-id OctaneRender Daylight Animation ------------------------------------------------------------------------------- -- GUI code -- 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, log) 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 logarithmic = log -- set the slider logarithmic } end -- helper to pop-up an error dialog local function showError(text) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, icon = octane.gui.dialogIcon.WARNING, title = "Daylight Animation Error", text = text, } end -- lets create a bunch of labels and sliders local startLbl = createLabel("Start hour") local endLbl = createLabel("End hour") local durationLbl = createLabel("Duration") local frameRateLbl = createLabel("Framerate") local frameLbl = createLabel("Frames") local samplesLbl = createLabel("Samples/px") local startSlider = createSlider(0, 0, 24, 1, false) local endSlider = createSlider(24, 0, 24, 1, false) local targetSlider = createSlider(10 , 0.001, 10000, 0.001, true) local samplesSlider = createSlider(400, 1 , 16000, 1, true) -- these sliders are couples (25 frames @ 25 fps is 1 second of animation) local durationSlider = createSlider(1 , 1 , 3600 , 0.001, true) local frameRateSlider = createSlider(25, 10, 120 , 1 , false) local frameSlider = createSlider(25, 10, 432000, 1 , 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 = 6, -- 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 , durationLbl , durationSlider , frameRateLbl , frameRateSlider , frameLbl , frameSlider , 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, } local row1 = octane.gui.create { type = octane.gui.componentType.GROUP, border = false, rows = 1, cols = 2, children = { fileChooseButton, fileEditor }, padding = { 2 }, inset = { 0 }, } startnrChk = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Start file numbering at:", checked = false, width = 150, height = 20 } startnrNum = octane.gui.create { type = octane.gui.componentType.NUMERIC_BOX, minValue = 0, maxValue = 10000, value = 0, step = 1, height = 20 } local row2 = octane.gui.create { type = octane.gui.componentType.GROUP, border = false, rows = 1, cols = 2, children = { startnrChk, startnrNum }, padding = { 2 }, inset = { 0 }, } skipExistingChk = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Skip existing image files", checked = true, width = 250, height = 20 } fileGrpChildren = { row1, row2, skipExistingChk} 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 = #fileGrpChildren, cols = 1, children = fileGrpChildren, padding = { 2 }, inset = { 5 }, } -- progress bar -- eye candy, a progress bar local progressBar = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, text = "render progress", 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 daylightWindow = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Daylight Animation", children = { layoutGrp }, width = layoutGrp.width, height = layoutGrp.height, } ------------------------------------------------------------------------------ -- Animation Rendering Code (helpers to get us going) -- Returns copies of: -- * the original scene graph -- * the sun direction node connected to the daylight environment -- * the render target node -- These copies prevent 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") 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 a daylight environment is connected to the render target local copyEnv = copyRt:getInputNode(octane.P_ENVIRONMENT) if not copyEnv or copyEnv.type ~= octane.NT_ENV_DAYLIGHT then showError("no daylight environment connected to the render target") return nil end -- check if a sun direction is connected to the daylight environment local copySunDir = copyEnv:getInputNode(octane.P_SUN_DIR) if not copySunDir or copySunDir.type ~= octane.NT_SUN_DIRECTION then showError("no sun direction connected to the daylight environment") return nil end return copyScene, copyRt, copySunDir end -- Animates the hour node connected to the daylight environment local function setHourAnimator(sunDirNode, startHour, endHour, nbFrames, frameRate) -- calculate the new hour for each frame local hours = {} for i=0,nbFrames-1 do -- calculate the hour for each frame local hour = startHour + (endHour - startHour) * (i / nbFrames) -- store the new hour table.insert(hours, hour) end -- animate the hour sunDirNode:getConnectedNode(octane.P_HOUR):setAnimator(octane.A_VALUE, { 0 }, hours, 1 / frameRate) end -- helper functions to get the filename function parseFileInputs() local file = fileEditor and fileEditor.text or "" -- 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 return {prefix, seqPattern, suffix} end return nil end function getImageFile(fileparts, seq, currentFrame) local fileFrameNo if startnrChk.checked then fileFrameNo = seq - 1 + startnrNum.value else fileFrameNo = currentFrame end return fileparts[1]..fileparts[2]:format(fileFrameNo)..fileparts[3] end -- cancel flag IDLE, RENDERING = 1, 2 status = IDLE -- Render callback function renderCallback() -- check if rendering was cancelled if status == IDLE then octane.render.callbackStop() return end end -- Render main loop local function startRender(sceneGraph, rtNode, sunDirNode, path) status = RENDERING -- motion blur doesn't make sense here so we disable it octane.render.setShutterTime(0) -- get the presets from the GUI local startHour = startSlider.value local endHour = endSlider.value local nbFrames = frameSlider.value local nbSamples = samplesSlider.value local frameRate = frameRateSlider.value -- set up the animator for the hour setHourAnimator(sunDirNode, startHour, endHour, nbFrames, frameRate) -- file name local fileParts = parseFileInputs() -- 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, nbSamples, false) end -- start rendering out each frame local currentTime = 0 local nbRendered = 0 local nbSkipped = 0 local nbFailed = 0 for frame=1,nbFrames do -- set the time in the scene sceneGraph:updateTime(currentTime) -- update the progress bar local text = string.format("rendering frame %d", frame) if nbFailed ~= 0 then text = text.." ("..nbFailed.." failed)" end progressBar.text = text -- current file name local thisFile if fileParts then -- see if a file was given, and replace the sequence number thisFile = getImageFile(fileParts, frame, frame) fileEditor.text = thisFile end -- render frame if the file doesn't exist yet (or no file is given), or always if -- we want to overwrite if not thisFile or not skipExistingChk.checked or not octane.file.exists(thisFile) then -- fire up the render engine, yihaah! local finished = pcall(octane.render.start, { renderTargetNode = rtNode, callback = renderCallback, }) if not finished then nbFailed = nbFailed + 1 end -- break out if we're cancelled and set it in the progress bar if status == IDLE then progressBar:updateProperties{ progress = 0, text = "cancelled" } break elseif fileParts then -- save the current frame octane.render.saveImage(thisFile, octane.render.imageType.PNG8) end nbRendered = nbRendered + 1 else nbSkipped = nbSkipped + 1 end -- update the time for the next frame currentTime = frame / frameRate -- update the progress bar progressBar.progress = frame / nbFrames end -- update the progress bar if status ~= IDLE then if nbRendered == 0 and nbSkipped > 0 then progressBar:updateProperties{ text = "all image files already exist", progress = 0 } else local text = "finished" if nbFailed ~= 0 then text = text.." ("..nbFailed.." failed)" end progressBar.text = text end end status = IDLE end local function cancelRender() status = IDLE octane.render.callbackStop() end ------------------------------------------------------------------------------ -- Main Flow -- Get the render target and sun dir in global variables SCENE_GRAPH, RT_NODE, SUN_DIR_NODE = getSceneCopy() -- if getSceneCopy failed, halt the script if SCENE_GRAPH == nil then return end inputwidgets = {degSlider, offsetSlider, targetSlider, samplesSlider, durationSlider, frameRateSlider, frameSlider, shutterSpeedSlider, fileChooseButton, fileEditor, startnrChk, startnrNum, skipExistingChk} -- all the components on the last row are file input widgets and may be -- nil. In Lua this just gives a smaller array. -- callback handling the GUI elements local function guiCallback(component, event) if component == durationSlider then -- if the duration of the animation changes, update the #frames local frames = math.ceil(durationSlider.value * frameRateSlider.value) frameSlider.value = frames elseif component == frameRateSlider then -- if the frame rate changes, update the #frames local frames = math.ceil(durationSlider.value * frameRateSlider.value) frameSlider.value = frames elseif component == frameSlider then -- if the #frames changes, update the duration of the animation local duration = frameSlider.value / frameRateSlider.value durationSlider.value = duration elseif 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 startRender(SCENE_GRAPH, RT_NODE, SUN_DIR_NODE, file) renderButton.text = RENDERTXT for _, c in pairs(inputwidgets) do c.enable = true end elseif status == RENDERING then cancelRender() end elseif component == exitButton then daylightWindow:closeWindow() elseif component == daylightWindow then -- when the window closes, cancel rendering if event == octane.gui.eventType.WINDOW_CLOSE then cancelRender() end end end -- hookup the callback with all the GUI elements durationSlider .callback = guiCallback frameRateSlider .callback = guiCallback frameSlider .callback = guiCallback if fileChooseButton then fileChooseButton.callback = guiCallback end renderButton .callback = guiCallback exitButton .callback = guiCallback daylightWindow .callback = guiCallback -- the script will block here until the window closes daylightWindow:showWindow()