-- Camera animation script -- ------------------------------------------------------------------------------ -- GUI code -- creates a text label and returns it 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 check box and returns it function createCheckBox(t_name,t_text) return octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = t_text, width = 80, height = 20, name= t_name, } end -- creates a text editor and returns it function createEditor(t_name,t_text) return octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = t_text, x = 0, width = 400, height = 20, enable = false, name= t_name, } end -- creates a slider and returns it 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 -- creates a combo_box drop_down and returns it function createDrop_down(name, items, width, height, selected) return octane.gui.create { type = octane.gui.componentType.COMBO_BOX, -- type of the component name = name, -- name of component items = items, -- array of item to show in combo-box width = width, -- width in pixels height = height, -- height in pixels editable = false, -- default to non-editable selectedIx = selected, -- default item selection } end -- creates a button and returns it function createButton(name, text, width, height, tooltip) return octane.gui.create { type = octane.gui.componentType.BUTTON, -- type of the component name = name, -- name of the component text = text, -- button text width = width, -- width in pixels height = height, -- height in pixels tooltip = tooltip, -- tooltip text } end -- creates a group and retuns it function createGroup(name, children, border, rows, cols, width, height, text, padding, inset, center) return octane.gui.create { type = octane.gui.componentType.GROUP, -- type of the component name = name, -- name of component children = children, -- list of children components border = border, -- boolean flag to show border rows = rows, -- number of rows in group grid cols = cols, -- number of cols in group grid width = width, -- width in pixels height = height, -- height in pixels text = text, -- text to display at top of group padding = padding, -- internal padding in each cell inset = inset, -- inset of the group component centre = center, -- center the group } end -- lets create a bunch of labels and sliders local frameRateLbl = createLabel("Framerate") local samplesLbl = createLabel("Samples/px") local formatLbl = createLabel("Video Format") local mblurLbl = createLabel("Shutter Time") local renderTargetLbl = createLabel("Render Target") local formatComboBox = createDrop_down("file_format",{"avi","mkv","mp4"},400,20,1) local frameRateSlider = createSlider(25, 10, 120 , 1, false) local samplesSlider = createSlider(100, 1 , 16000, 1, true) local mblurSlider = createSlider(0.001, 0 , 2, 0.001, true) local renderTargetDropDown = createDrop_down("rendertarget", nil, 400, 20, nil) -- 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 = { formatLbl , formatComboBox , frameRateLbl , frameRateSlider , samplesLbl , samplesSlider , mblurLbl , mblurSlider , renderTargetLbl , renderTargetDropDown , }, padding = { 2 }, -- internal padding in each cell inset = { 5 }, -- inset of the group component itself } -- file input -- create a button to show a file camera chooser local cameraChooseButton = createButton("camerafile", "Camera File", 80, 20, "Choose camera path points file") local cameraEditor = createEditor("cameraeditor","") local cameramFrameLabel = createLabel("Preview Frame") local cameraFrameSlider = createSlider(0,1,3,1,false) -- for layouting the button and the editor we use a group local cameraGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "Camera Frame Points", rows = 2, cols = 2, children = { cameraChooseButton, cameraEditor,cameramFrameLabel,cameraFrameSlider, }, padding = { 2 }, inset = { 5 }, } -- create a button to show a file chooser local fileChooseButton = createButton("fileChooseButton", "Output...", 80, 20, "Select output Directory and file name") -- create an editor that will show the chosen file path local fileEditor = createEditor("fileEditor","") local startFrameLbl = createLabel("Start Frame") local endFrameLbl = createLabel("End Frame") local joinLbl = createLabel("Join at video") local startFrameSlider = createSlider(1,1,3,1,false) local endFrameSlider = createSlider(1,1,3,1,false) local ffmpegCheck = createCheckBox("ffmpeg","") -- for lay-outing the button and the editor we use a group local fileGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "Output", rows = 4, cols = 2, children = { fileChooseButton, fileEditor, startFrameLbl,startFrameSlider, endFrameLbl,endFrameSlider, joinLbl,ffmpegCheck, }, 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:getProperties().width * 0.8, -- as wide as the group above height = 20, } local frameprogressBar = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, text = "frame 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 = 2, cols = 1, children = { progressBar, frameprogressBar}, padding = { 10 }, centre = true, -- centre the progress bar in it's cell border = false, } -- render & cancel buttons local renderButton = createButton("Render", "Render", 80, 20, "Start Render") local stopButton = createButton("Stop", "Stop", 80, 20, "Stop Render") local exitButton = createButton("Exit", "Exit", 80, 20, "Exit Script") local ffmpegButton = createButton("ffMpeg", "ffMpeg", 80, 20, "Start ffMpeg encoding") local buttonGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 4, children = { renderButton, stopButton, exitButton,ffmpegButton }, padding = { 5 }, border = false, } -- group that layouts the other groups local layoutGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 5, cols = 1, children = { settingsGrp, cameraGrp, 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 window = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Camera Path Animation v1.00", children = { layoutGrp }, width = layoutGrp:getProperties().width, height = layoutGrp:getProperties().height, } -- helper to pop-up an error dialog and optionally halts the script function showError(text, halt) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, title = "Animation Error", text = text, } if halt then window:closeWindow() end end function reverseTable(table) local size = #table local newTable = {} for i,v in ipairs ( table ) do newTable[size + 1 - i] = v end return newTable end function lengthTable(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end -- Get an index from a array function get_key( t, value ) for k,v in pairs(t) do if v==value then return k end end return nil end function getAllrendertargets() local renderTargets = octane.project.getSceneGraph():findNodes(octane.NT_RENDERTARGET, true) local renTargList = {} local renTargNames = {} for i, item in ipairs(renderTargets) do renTargList[#renTargList + 1] = item renTargNames[#renTargNames + 1] = item.name end return reverseTable(renTargList), reverseTable(renTargNames) end -- Find the render target nodes renderTargets, renderTargNames = getAllrendertargets() local currSelection = octane.project.getSelection()[1] if currSelection == nil then CURRENTTARGET = renderTargets[1] elseif currSelection.type == octane.NT_RENDERTARGET then CURRENTTARGET = currSelection else CURRENTTARGET = renderTargets[1] end ------------------------------------------------------------------------------ -- 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. -- Returns copies of the original scene graph, the camera node and the rendertarget. -- This prevents us from modifying the original scene. function getSceneCopy() local copyScene = octane.nodegraph.createRootGraph("Project Copy") local copyRt = copyScene:copyFromGraph(octane.project.getSceneGraph(), { CURRENTTARGET })[1] -- check if the copied node is a render target if not copyRt or copyRt.type ~= octane.NT_RENDERTARGET then showError("TurntableG Error!","no render target selected", true) 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 showError("TurntableG Error!","no thinlens camera connected to the render target", true) end return copyScene, copyRt, copyCam end -- Sets up the yaw, pitch or roll animation. We do this by animating the camera's -- target position function setCamAnimator(camNode, sframe,eframe) --nbFrames) -- get the original camera settings local origCamTarget = camNode:getPinValue(octane.P_TARGET) local origCamPosition = camNode:getPinValue(octane.P_POSITION) local origCamUp = octane.vec.normalized(camNode:getPinValue(octane.P_UP)) local origViewDir = octane.vec.sub(origCamTarget, origCamPosition) local origCamRight = octane.vec.cross(octane.vec.normalized(origViewDir), origCamUp) -- calculate the animated values for each frame local t_values = {} local p_values = {} for i=sframe,eframe do t_value= {CAMERA[i][1],CAMERA[i][2],CAMERA[i][3]} p_value= {CAMERA[i][4],CAMERA[i][5],CAMERA[i][6]} table.insert(t_values,t_value) table.insert(p_values,p_value) end -- animate the camera position camNode:getConnectedNode(octane.P_TARGET):setAnimator(octane.A_VALUE, { 0 }, t_values, 1 / (eframe-sframe)) camNode:getConnectedNode(octane.P_POSITION):setAnimator(octane.A_VALUE, { 0 }, p_values, 1 / (eframe-sframe)) end -- creates a save path for the current frame function createSavePath(path, frame) local file = octane.file.getFileName(path) -- 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 local 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" -- return the path to the output file return octane.file.getParentDirectory(path).."/"..prefix..seqPattern:format(frame)..suffix end -- flag indicating cancellation IS_CANCELLED = false function enableGui() formatComboBox:updateProperties { enable = true } frameRateSlider:updateProperties { enable = true } samplesSlider:updateProperties { enable = true } mblurSlider:updateProperties { enable = true } cameraChooseButton:updateProperties { enable = true } if (cameraEditor:getProperties().text=="") then cameraFrameSlider:updateProperties { enable = false } startFrameSlider:updateProperties { enable = false } endFrameSlider:updateProperties { enable = false } else cameraFrameSlider:updateProperties { enable = true } startFrameSlider:updateProperties { enable = true } endFrameSlider:updateProperties { enable = true } end fileChooseButton:updateProperties { enable = true } if(cameraEditor:getProperties().text=="" or fileEditor:getProperties().text=="" ) then renderButton:updateProperties { enable = false } else renderButton:updateProperties { enable = true } end if(fileEditor:getProperties().text=="" ) then ffmpegButton:updateProperties { enable = false } else ffmpegButton:updateProperties { enable = true } end stopButton:updateProperties { enable = false } end function disableGui() formatComboBox:updateProperties { enable = false } frameRateSlider:updateProperties { enable = false } mblurSlider:updateProperties { enable = false } samplesSlider:updateProperties { enable = false } cameraChooseButton:updateProperties { enable = false } cameraFrameSlider:updateProperties { enable = false } fileChooseButton:updateProperties { enable = false } startFrameSlider:updateProperties { enable = false } endFrameSlider:updateProperties { enable = false } renderButton:updateProperties { enable = false } stopButton:updateProperties { enable = true } ffmpegButton:updateProperties { enable = true } end function renderCallback(result) -- check if rendering was cancelled collectgarbage() if (IS_CANCELLED) then octane.render.stop() return end local s=0 local ts=samplesSlider:getProperties().value local frt=0 for k,v in pairs(result) do if(k=="samples") then s=v end if(k=="renderTime") then frt=v end print(k,v) end local pr= s/ts if(FRAME_TIME==0) then ESTIMATED_TIME=frt/pr * (TOTAL_FRAMES-RENDER_FRAME) else ESTIMATED_TIME=(FRAME_TIME*0.5 + 0.5*frt/pv) * (TOTAL_FRAMES-RENDER_FRAME) end local efrt=frt/pr local efrt_h=math.floor(efrt/(60*60)) local efrt_m=math.floor((efrt-efrt_h*3600)/(60)) local efrt_s=efrt-efrt_h*3600-efrt_m*60 frameprogressBar:updateProperties{ text = string.format("%d%% - %02d:%02d:%02d", 100*pr , efrt_h,efrt_m,efrt_s),progress= pr} local rem_h=math.floor(ESTIMATED_TIME/(60*60)) local rem_m=math.floor((ESTIMATED_TIME-rem_h*3600)/(60)) local rem_s=ESTIMATED_TIME-rem_h*3600-rem_m*60 progressBar:updateProperties{ text = string.format("rendering frame %d/%d - %02d:%02d:%02d remains", RENDER_FRAME,TOTAL_FRAMES,rem_h,rem_m,rem_s) } end function startRender(sceneGraph, rtNode, camNode, path) -- clear the cancel flag cancelRender() IS_CANCELLED = false -- TODO: add a motion blur slider octane.render.setShutterTime(mblurSlider:getProperties().value) -- disable part of the ui except for the cancel button disableGui() -- get the pre-sets from the GUI local nbStart = startFrameSlider:getProperties().value local nbEnd = endFrameSlider:getProperties().value local nbSamples = samplesSlider:getProperties().value local inPath = fileEditor:getProperties().text local outPath = fileEditor:getProperties().text -- set up the animator for the camera --setCamAnimator(camNode,nbStart,nbEnd)--nbFrames) -- start rendering out each frame local currentTime = nbStart*1/FRAMES TOTAL_FRAMES=(nbEnd-nbStart) for frame=nbStart,nbEnd do -- set the time in the scene sceneGraph:updateTime(currentTime) RENDER_FRAME=frame-nbStart+1 -- update the progress bar -- fire up the render engine, yihaah! octane.render.start { renderTargetNode = rtNode, maxSamples = nbSamples, callback = renderCallback, } -- break out if we're cancelled and set it in the progress bar if IS_CANCELLED then progressBar:updateProperties{ progress = 0, text = "cancelled" } collectgarbage() break end -- save the current frame local out = createSavePath(path, frame) octane.render.saveImage(out, octane.render.imageType.PNG8) -- update the time for the next frame currentTime = (frame) * (1 / (FRAMES)) -- update the progress bar progressBar:updateProperties{ progress = RENDER_FRAME / TOTAL_FRAMES } collectgarbage() end -- enable part of the ui except for the cancel button enableGui() -- update the progress bar progressBar:updateProperties{ progress = 0, text = "finished" } frameprogressBar:updateProperties{ progress = 0, text = "" } if(ffmpegCheck:getProperties().checked==true and not IS_CANCELLED) then ffmpeg() end end function previewCallback(result) collectgarbage() end function setPreviewcamera (frame) currentTime = (frame) * (1 / (FRAMES)) SCENE_GRAPH:updateTime(currentTime) octane.render.start { renderTargetNode = RT_NODE, maxSamples = 100, callback = previewCallback, } collectgarbage() end function startPreview(sceneGraph, rtNode, camNode, path) cancelRender() octane.render.setShutterTime(0) local nbFrames = FRAMES -- set up the animator for the camera if(FRAMES~=0) then setCamAnimator(camNode,1,nbFrames) local currentTime = 0 sceneGraph:updateTime(currentTime) end -- fire up the render engine, yihaah! octane.render.start { renderTargetNode = rtNode, maxSamples = 100, callback = previewCallback, } collectgarbage() end function cancelRender() IS_CANCELLED = true octane.render.callbackStop() octane.render.clear() end ------------------------------------------------------------------------------ -- Main Flow -- global variables OUT_PATH = nil IN_PATH = nil FRAMES = 0 CAMERA = {} FPS = 25 FORMAT = " " FRAME_TIME = 0 ESTIMATED_TIME =0 RENDER_FRAME=0 TOTAL_FRAMES=0 --CURRENTTARGET = nil SCENE_GRAPH=nil RT_NODE=nil CAM_NODE=nil -- Get the render target and camera in global variables -- render and cancel button are disable enableGui() -- callback handling the GUI elements function readLines(sPath) local file = io.open(sPath, "r") if file then local tLines = {} local tline = {} for sline in file:lines() do n1,n2,n3,n4,n5,n6 = sline:match("([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)") tline={n1,n2,n3,n4,n5,n6} table.insert(tLines,tline ) end file.close() FRAMES=lengthTable(tLines) return tLines end return nil end function ffmpeg() if OUT_PATH== nil then return 0 end FORMAT=formatComboBox:getProperties().items[formatComboBox:getProperties().selectedIx] FPS=frameRateSlider:getProperties().value scriptDir = octane.file.getSpecialDirectories()["userScriptDirectory"] local octane_dir='"'..scriptDir..'\\ffmpeg.exe "' local ffmpeg_cmd=octane_dir .. " -i ".. OUT_PATH .."%04d.png -c:v libx264 -r ".. FPS .." -pix_fmt yuv420p " .. OUT_PATH .."." .. FORMAT print(ffmpeg_cmd) os.execute(ffmpeg_cmd) end function guiCallback(component, event) 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, path = IN_PATH, } -- if a file is chosen if ret.result ~= "" then fileEditor:updateProperties{ text = ret.result } enableGui() OUT_PATH = ret.result else fileEditor:updateProperties{ text = "" } enableGui() OUT_PATH = nil end elseif component == formatComboBox then elseif component == frameRateSlider then FPS=frameRateSlider:getProperties().value elseif component == ffmpegButton then ffmpeg() elseif component == startFrameSlider then elseif component == endFrameSlider then elseif component == cameraChooseButton then -- choose an input file local ret1 = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Choose the camera file", wildcards = "*.txt", save = false, } -- if a file is chosen if ret1.result ~= "" then cameraEditor:updateProperties{ text = ret1.result } enableGui() IN_PATH = ret1.result CAMERA =readLines(IN_PATH ) --Print a specific line print (FRAMES) cameraFrameSlider:updateProperties{maxValue=FRAMES} startFrameSlider:updateProperties{maxValue=FRAMES} endFrameSlider:updateProperties{maxValue=FRAMES} endFrameSlider:updateProperties{value=FRAMES} startPreview(SCENE_GRAPH, RT_NODE, CAM_NODE, OUT_PATH) else cameraEditor:updateProperties{ text = "" } enableGui() IN_PATH = nil end elseif component == renderButton then -- Start the actual rendering. startRender(SCENE_GRAPH, RT_NODE, CAM_NODE, OUT_PATH) elseif component == stopButton then cancelRender() elseif component == exitButton then cancelRender() window:closeWindow() elseif component == window then -- when the window closes, cancel rendering if event == octane.gui.eventType.WINDOW_CLOSE then cancelRender() end elseif component == cameraFrameSlider then collectgarbage() setPreviewcamera(cameraFrameSlider:getProperties().value) elseif component == renderTargetDropDown then local comboTarget = renderTargetDropDown.selectedIx -- A render target has been choosen in the drop down, now -- see if it matches the current selected target node. If they match -- then do nothing. If they do not match then set RT_NODE and CAM_NODE to -- new nodes. if renderTargets[comboTarget] ~= CURRENTTARGET then CURRENTTARGET = renderTargets[comboTarget] -- find the choosen render target node, and copy the scene SCENE_GRAPH, RT_NODE, CAM_NODE = getSceneCopy() end startPreview(SCENE_GRAPH, RT_NODE, CAM_NODE, OUT_PATH) end end -- Initialize the render targets combo box local renderTargKey = get_key(renderTargNames, CURRENTTARGET.name) renderTargetDropDown:updateProperties { items = renderTargNames, selectedIx = renderTargKey } SCENE_GRAPH, RT_NODE, CAM_NODE = getSceneCopy() -- hookup the callback with all the GUI elements startFrameSlider:updateProperties { callback = guiCallback } endFrameSlider:updateProperties { callback = guiCallback } formatComboBox:updateProperties { callback = guiCallback } frameRateSlider:updateProperties { callback = guiCallback } mblurSlider:updateProperties { callback = guiCallback } fileChooseButton:updateProperties { callback = guiCallback } cameraFrameSlider:updateProperties { callback = guiCallback } cameraChooseButton:updateProperties { callback = guiCallback } renderButton:updateProperties { callback = guiCallback } ffmpegButton:updateProperties { callback = guiCallback } stopButton:updateProperties { callback = guiCallback } exitButton:updateProperties { callback = guiCallback } window:updateProperties { callback = guiCallback } renderTargetDropDown:updateProperties { callback = guiCallback } -- the script will block here until the window closes window:showWindow()