---------------------------------------------------------------------------------------------------- -- Turntable animation script in Multi EXR -- -- @description Renders a turntable animation -- @author Octane Dev Team -- @version 0.5.1 -- @script-id OctaneRender turntable animation -- make a table and fill it with label/slider pairs, and setup their bounds and other properties -- as well. local params = {} local degLbl,degSlider = octane.gui.createParameter(params, "Degrees" , 360, -360 , 360 , 1 , false) local offsetLbl,offsetSlider = octane.gui.createParameter(params, "Start angle" , 0 , -180 , 180 , 1 , false) local targetLbl,targetSlider = octane.gui.createParameter(params, "Target distance", 10 , 0.001, 10000, 0.001, false) local durationLbl,durationSlider = octane.gui.createParameter(params, "Duration" , 1 , 1 , 3600 , 0.001, true) local samplesLbl,samplesSlider = octane.gui.createParameter(params, "Samples/px" , 400, 1 , 16000, 1 , true) local frameRateLbl,frameRateSlider = octane.gui.createParameter(params, "Framerate" , 25 , 10 , 120 , 1 , false) local frameLbl,frameSlider = octane.gui.createParameter(params, "Frames" , 25 , 10 , 432000,1 , true) local shutterSpeedLbl,shutterSpeedSlider = octane.gui.createParameter(params, "Shutter speed (%)", 100, 0, 1000, .1, false) -- create a group to layout the sliders together. This will put them in a double-column layout. local settingsGrp = octane.gui.createGroup(params) -- some globals used later fileGrpChildren = nil fileChooseButton = nil fileEditor = nil -- if we are able to save an image if octane.render.saveRenderPassesMultiExr then -- create a buttton to choose a file fileChooseButton = octane.gui.createButton("Output...") startnrChk = octane.gui.createCheckBox { text = "Start file numbering at:", checked = false, width = 150, height = 20 } skipExistingChk = octane.gui.createCheckBox { text = "Skip existing image files", checked = true, width = 250, height = 20 } startnrNum = octane.gui.createNumericBox { min = 0, max = 10000, value = 0, step = 1, height = 20 } fileEditor = octane.gui.createTextEditor { width = 400, height = 20 } -- create groups to do automatic layout of the button and editor local row1 = octane.gui.createGroup { border = false, padding = { 2 }, inset = { 0 }, children = { { fileChooseButton, fileEditor } } } local row2 = octane.gui.createGroup { border = false, padding = { 2 }, inset = { 0 }, children = { { startnrChk, startnrNum } } } -- make a table ready to give to a group layout. fileGrpChildren = { { row1 }, { row2 }, { skipExistingChk } } else -- if we can't save an image, make a label local label = octane.gui.createLabel { text = "Saving images is not available in the demo version", width = 500 } -- make the label the only item in the group fileGrpChildren = { label } end -- for layouting the button and the editor we use another group local fileGrp = octane.gui.createGroup { text = "Output", padding = { 2 }, inset = { 5 }, children = fileGrpChildren } -- eye candy, a progress bar local progressBar = octane.gui.createProgressBar{ text = "render progress", width = fileGrp.width * 0.8, height = 20 } -- for layouting the progress bar local progressGrp = octane.gui.createGroup { text = "", centre = true, padding = { 10 }, border = false, children = { { progressBar } } } -- render & cancel buttons local renderButton = octane.gui.createButton("Start render") local exitButton = octane.gui.createButton("Exit") -- layout the buttons together local buttonGrp = octane.gui.createGroup { children = { { renderButton, exitButton } }, padding = { 5 }, border = false } -- group that layouts the other groups local layoutGrp = octane.gui.createGroup { text = "", children = { { settingsGrp }, { fileGrp }, { progressGrp }, { buttonGrp } }, centre = true, padding = { 2 }, border = false, debug = false -- true to show the outlines of the group, handy } -- ... and finally, the window itself local turntableWindow = octane.gui.createWindow { text = "Turntable 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 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] if not selectedRt or selectedRt.type ~= octane.NT_RENDERTARGET then octane.gui.showError{ text = "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 the copied node is a render target with a thinlens camera connected to it if not copyRt or copyRt.type ~= octane.NT_RENDERTARGET then octane.gui.showError("No render target selected") return nil end -- check if a thin lens camera is connected to the render target local copyCam = copyRt:getInputNode(octane.P_CAMERA) if not copyCam or copyCam .type ~= octane.NT_CAM_THINLENS then octane.gui.showError("No thinlens camera connected to the render target") return nil end -- check if an animation settings node is connected to the render target local copyAnimSettings = copyRt:getInputNode(octane.P_ANIMATION) if not copyAnimSettings or copyAnimSettings.type ~= octane.NT_ANIMATION_SETTINGS then octane.gui.showError("No animation settings node connected to the render target") return nil end return copyScene, copyRt, copyCam, copyAnimSettings end local function setCamAnimator(camNode, rotAngle, offsetAngle, targetDistance, nbFrames, frameRate) -- get the original camera settings local origCamTarget = camNode:getPinValue(octane.P_TARGET) local origCamPosition = camNode:getPinValue(octane.P_POSITION) local origCamUp = camNode:getPinValue(octane.P_UP) local origViewDir = octane.vec.normalized(octane.vec.sub(origCamTarget, origCamPosition)) -- calculate the new camera position for each frame local positions = {} for i=0,nbFrames-1 do -- calculate the angle of rotation for this frame local angle = math.rad( (i / nbFrames) * rotAngle + offsetAngle ) -- rotate the viewing direction around the up vector local newViewDir = octane.vec.rotate(origViewDir, origCamUp, angle) -- scale the new view dir with the target distance newViewDir = octane.vec.scale(newViewDir, targetDistance) -- calculate the new camera position local newCamPosition = octane.vec.sub(origCamTarget, newViewDir) -- store the new camera position table.insert(positions, newCamPosition) end -- animate the camera position camNode:getInputNode(octane.P_POSITION):setAnimator(octane.A_VALUE, { 0 }, positions, 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 exr extension file = file:gsub("%.exr$", "") -- 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 exr extension suffix = suffix..".exr" -- 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 -- Returns the names of all enabled render passes. local function createRenderPassExportObjs(renderPassesNode) local objects = {} for _, renderPassId in ipairs(octane.render.getAllRenderPassIds()) do local info = octane.render.getRenderPassInfo(renderPassId) if info.pinId ~= octane.P_UNKNOWN then -- if the render pass is enabled -> add it to the export objects if renderPassesNode:getPinValue(info.pinId) then local exportObj = { ["exportName"] = info.name, ["origName"] = info.name, ["renderPassId"] = info.renderPassId, } table.insert(objects, exportObj) end else local exportObj = { ["exportName"] = info.name, ["origName"] = info.name, ["renderPassId"] = info.renderPassId, } table.insert(objects, exportObj) end end return objects 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, camNode, animNode) status = RENDERING -- shutter speed animNode:setPinValue(octane.P_SHUTTER_TIME, shutterSpeedSlider.value / 100.0) -- get the presets from the GUI local rotAngle = degSlider.value local offsetAngle = offsetSlider.value local targetDistance = targetSlider.value local nbFrames = frameSlider.value local frameRate = frameRateSlider.value -- set up the animator for the camera setCamAnimator(camNode, rotAngle, offsetAngle, targetDistance, 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, samplesSlider.value, false) end -- start rendering out each frame local currentTime = 0 local nbRendered = 0 local nbSkipped = 0 local nbFailed = 0 for frame=1,nbFrames do -- 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! -- set the time in the scene sceneGraph:updateTime(currentTime) 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 local exportObjects = createRenderPassExportObjs(rtNode:getInputNode(octane.P_RENDER_PASSES)) octane.render.saveRenderPassesMultiExr(thisFile, exportObjects)--, octane.imageSaveType.EXR) 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 local function initGui(camNode) -- set the initial target distance in the distance slider local target = camNode:getPinValue(octane.P_TARGET) local position = camNode:getPinValue(octane.P_POSITION) local viewDir = octane.vec.sub(target, position) local tgtLen = octane.vec.length(viewDir) targetSlider.value = tgtLen end ---------------------------------------------------------------------------------------------------- -- Main Flow -- Get the render target and camera in global variables SCENE_GRAPH, RT_NODE, CAM_NODE, ANIM_NODE = getSceneCopy() -- if getSceneCopy failed, halt the script if SCENE_GRAPH == nil then return end -- initialize the UI initGui(CAM_NODE) 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 = "*.exr", 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 = "Stop render" for _, c in pairs(inputwidgets) do c.enable = false end -- start render startRender(SCENE_GRAPH, RT_NODE, CAM_NODE, ANIM_NODE, file) renderButton.text = "Start render" for _, c in pairs(inputwidgets) do c.enable = true end elseif status == RENDERING then cancelRender() end elseif component == exitButton then turntableWindow:closeWindow() elseif component == turntableWindow 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 turntableWindow.callback = guiCallback -- the script will block here until the window closes turntableWindow:showWindow() -- reset the render engine octane.render.reset()