-- Camera animation script -- ------------------------------------------------------------------------------ -- 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 and optionally halts the script local function showError(text, halt) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, title = "Animation Error", text = text, } if halt then error("ERROR: "..text) end 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 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) -- manual layouting is tedious so let's add all our stuff in a group. local formatComboBox = octane.gui.create { type = octane.gui.componentType.COMBO_BOX , -- type of component name = "file_format", items = {"avi","mkv","mp4"}, selectedIx = 1, } -- 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 = 4, -- 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 , }, 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 cameraFrameSlider = createSlider(0,1,3,1,false) local cameramFrameLabel = createLabel("Preview Frame") local cameraChooseButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Camera File", width = 80, height = 20, } -- create an editor that will show the chosen file path local cameraEditor = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", x = 0, width = 400, height = 20, enable = 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 = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Output...", width = 80, height = 20, } -- create an editor that will show the chosen file path local fileEditor = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", x = 0, width = 400, height = 20, enable = false, } 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 = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "", width = 80, height = 20, } -- 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 = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Render", width = 80, height = 20, } local stopButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Stop", width = 80, height = 20, } local exitButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Exit", width = 80, height = 20, } local ffmpegButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "ffMpeg", width = 80, height = 20, } 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", children = { layoutGrp }, width = layoutGrp:getProperties().width, height = layoutGrp:getProperties().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] -- find the index of the selected render target node local idx = -1 for i, item in ipairs(octane.project.getSceneGraph():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", true) 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.project.getSceneGraph():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", true) end -- check if a thin lens camera is connected to the render target local copyCam = copyRt:getConnectedNode(octane.P_CAMERA) if not copyCam or copyCam :getProperties().type ~= octane.NT_CAM_THINLENS then showError("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 local function setCamAnimator(camNode, rotAngle, offsetAngle, animType, 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 local 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 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 frameprogressBar:updateProperties{ text = string.format("%d%%", 100*pr),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 local 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 local 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 local function startRender(sceneGraph, rtNode, camNode, path) -- clear the cancel flag 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 nbFrames = FRAMES 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 print(nbStart) setCamAnimator(camNode, rotAngle, offsetAngle, animType, nbStart,nbEnd)--nbFrames) -- start rendering out each frame local currentTime = 0 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-nbStart) * (1 / (nbEnd-nbStart)) -- 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" } if(ffmpegCheck:getProperties().checked==true and not IS_CANCELLED) then ffmpeg() end end local 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 -- Get the render target and camera in global variables SCENE_GRAPH, RT_NODE, CAM_NODE = getSceneCopy() -- render and cancel button are disable enableGui() -- callback handling the GUI elements local function readLines(sPath) local file = io.open(sPath, "r") if file then FRAMES=0 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 ) FRAMES=FRAMES+1 end file.close() return tLines end return nil end local function setPreviewcamera (frame) octane.render.callbackStop() collectgarbage() local origCamTarget = CAM_NODE:getPinValue(octane.P_TARGET) local origCamPosition = CAM_NODE:getPinValue(octane.P_POSITION) -- calculate the animated values for each frame local t_value= {CAMERA[frame][1],CAMERA[frame][2],CAMERA[frame][3]} local p_value= {CAMERA[frame][4],CAMERA[frame][5],CAMERA[frame][6]} -- animate the camera position CAM_NODE:setPinValue(octane.P_TARGET,t_value) CAM_NODE:setPinValue(octane.P_POSITION,p_value) octane.render.start { renderTargetNode = RT_NODE, maxSamples = 100, callback = renderCallback, } collectgarbage() end local function ffmpeg() if OUT_PATH== nil then return 0 end FORMAT=formatComboBox:getProperties().items[formatComboBox:getProperties().selectedIx] FPS=frameRateSlider:getProperties().value local octane_dir='"c:\\Program Files\\OTOY\\OctaneRender\\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 local 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, } -- 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} else cameraEditor:updateProperties{ text = "" } enableGui() IN_PATH = nil end elseif component == renderButton then -- Get the render target and camera in global variables -- SCENE_GRAPH, RT_NODE, CAM_NODE = getSceneCopy() -- 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) end end -- 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 } -- the script will block here until the window closes window:showWindow()