Of course we don't have to start from scratch: the UI is similar to the turntable animation script here: http://render.otoy.com/forum/viewtopic.php?f=73&t=37313
I will highlight mostly the rendering parts.
The first step is inspecting the scene and figuring out what to render. we get the first render target in the scene, and also get the total length of the animation via the root node graph. We also set up some global state needed in the render callback.
- Code: Select all
local root = octane.nodegraph.getRootGraph()
local alltargets = root:findNodes(octane.NT_RENDERTARGET)
if #alltargets == 0 then
error("Please set up your render target in the main graph.", 0)
end
local rtnode = alltargets[1]
local interval = root:getAnimationTimeSpan()
-- cancel flag and other globals
isCanceled = false
startT = 0
endT = 0
dT = 0
currentT = 0
The render callback checks if the rendering was canceled, and it updates the progress bar:
- Code: Select all
function renderCallback(result)
if isCanceled then
octane.render.stop()
end
local t = currentT + dT * result.samples / result.maxSamples
t = (t - startT) / (endT - startT)
progressBar:updateProperties{progress = t}
end
The render function is called when we click the render button, and contains the main loop rendering all the frames. First we get some settings from our GUI components. Note that for this to work, we need to store these components in global variables (i.e. don't declare them as
local
):- Code: Select all
startT = startSlider:getProperties().value
endT = endSlider:getProperties().value
dT = 1 / fpsSlider:getProperties().value
if endT < startT then return end
local file = fileEditor:getProperties().text
Then comes some pattern black magic, to figure out where to put the sequence number in the file name. We figure out if the file name already contains a sequence number, and we split it up in the part before and after the sequence number.
- Code: Select all
-- 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:updateProperties{text = prefix..sequenceMatch..suffix}
end
Then comes the render loop. We increment the current time stamp until we get to the end time set up via the sliders. Due to rounding errors, and because the end time is the last animated time stamp, we need to make sure this time stamp is also rendered, even if some small rounding errors occur.
For each frame we update the time stamp of the root graph. This automatically updates all animated attributes in the scene. Then we can call
octane.render.start
, which will block until the render is finished.After rendering we check if the rendering is canceled, and if not we save the image if a file name was given. To give some feedback on what we are saving, we update the file text field.
Finally we increment the current time.
- Code: Select all
currentT = startT
local seq = 0
-- careful with rounding errors
while currentT < endT + .001 * dT do
-- time and sequence number
seq = seq + 1
root:updateTime(currentT)
-- render
local result = octane.render.start
{
renderTargetNode = rtnode,
callback = renderCallback,
maxSamples = samplesSlider:getProperties().value
}
if isCanceled then
-- canceled
progressBar:updateProperties{progress = 0}
return
elseif file ~= "" then
-- see if a file was given, and replace the sequence number
local thisFile = prefix..seqPattern:format(seq)..suffix
fileEditor:updateProperties{text = thisFile}
octane.render.saveImage(thisFile, octane.render.imageType.PNG8)
end
-- go to next time stamp
currentT = currentT + dT
end
-- set the progress bar to 100%. 1.0 activates indefinite mode, so...
progressBar:updateProperties{progress = .99999}
Most of the GUI code is very similar to the turntable animation script, so I'm not repeating it here. There are some different sliders, and some sliders and the file name text box are stored in global variables, so we can access them from the callbacks.
We still have to hook up callbacks to our buttons. We will also stop the render if the window is closed.
- Code: Select all
function guiCallback(component, event)
if event == octane.gui.eventType.BUTTON_CLICKED then
if component == fileChooseButton then
-- OK, this is not implemented yet...
elseif component == renderButton then
isCanceled = false
renderButton:updateProperties{enable = false}
cancelButton:updateProperties{enable = true}
render()
renderButton:updateProperties{enable = true}
cancelButton:updateProperties{enable = false}
elseif component == cancelButton then
isCanceled = true
end
elseif event == octane.gui.eventType.WINDOW_CLOSE then
isCanceled = true
end
end
We can do some further improvements to this script. We still have to hook up the output... button and the shutter time slider. For now you can set the shutter time in the time line below the rendered image before you start the script.
A small downside of this script is that it modifies the scene. If you want to render multiple scenes in a batch, this will be a problem, because Octane displays a prompt to save changes before opening a new scene. To avoid this we can copy the root node graph to a temporary root graph created in the script, and render from this one.
But for now, here is the complete script.
- Code: Select all
-- gather some info about the stuff to render
local root = octane.nodegraph.getRootGraph()
local alltargets = root:findNodes(octane.NT_RENDERTARGET)
if #alltargets == 0 then
error("Please set up your render target in the main graph.", 0)
end
local rtnode = alltargets[1]
local interval = root:getAnimationTimeSpan()
-- cancel flag and other globals
isCanceled = false
startT = 0
endT = 0
dT = 0
currentT = 0
-- the actual rendering is quite easy.
-- Render callback
function renderCallback(result)
if isCanceled then
octane.render.stop()
end
local t = currentT + dT * result.samples / result.maxSamples
t = (t - startT) / (endT - startT)
progressBar:updateProperties{progress = t}
end
-- Render main loop
function render()
startT = startSlider:getProperties().value
endT = endSlider:getProperties().value
dT = 1 / fpsSlider:getProperties().value
if endT < startT then return end
local file = fileEditor:getProperties().text
-- 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:updateProperties{text = prefix..sequenceMatch..suffix}
end
currentT = startT
local seq = 0
-- careful with rounding errors
while currentT < endT + .001 * dT do
-- time and sequence number
seq = seq + 1
root:updateTime(currentT)
-- render
local result = octane.render.start
{
renderTargetNode = rtnode,
callback = renderCallback,
maxSamples = samplesSlider:getProperties().value
}
if isCanceled then
-- canceled
progressBar:updateProperties{progress = 0}
return
elseif file ~= "" then
-- see if a file was given, and replace the sequence number
local thisFile = prefix..seqPattern:format(seq)..suffix
fileEditor:updateProperties{text = thisFile}
octane.render.saveImage(thisFile, octane.render.imageType.PNG8)
end
-- go to next time stamp
currentT = currentT + dT
end
-- set the progress bar to 100%. 1.0 activates indefinite mode, so...
progressBar:updateProperties{progress = .99999}
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 startLbl = createLabel("Start time")
local endLbl = createLabel("End time")
local fpsLbl = createLabel("Framerate")
local shutterLbl = createLabel("Shutter time")
local samplesLbl = createLabel("Samples/px")
startSlider = createSlider(interval[1], interval[1], interval[2], .001)
endSlider = createSlider(interval[2], interval[1], interval[2], .001)
fpsSlider = createSlider(25 , 10, 120 , 1)
shutterSlider = createSlider(0, 0, 1, 1/25)
samplesSlider = createSlider(400, 1 , 16000, 1)
samplesSlider:updateProperties{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 = 7, -- 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
}
-- 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
fileEditor = octane.gui.create
{
type = octane.gui.componentType.TEXT_EDITOR,
text = "",
x = 20,
width = 400,
height = 20,
}
-- 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 = 2,
children =
{
fileChooseButton, fileEditor,
},
padding = { 2 },
inset = { 5 },
}
-- eye candy, a progress bar
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,
}
-- 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 renderButton = octane.gui.create
{
type = octane.gui.componentType.BUTTON,
text = "Render",
width = 80,
height = 20,
}
local cancelButton = octane.gui.create
{
type = octane.gui.componentType.BUTTON,
text = "Cancel",
width = 80,
height = 20,
enable = false,
}
local buttonGrp = octane.gui.create
{
type = octane.gui.componentType.GROUP,
text = "",
rows = 1,
cols = 2,
children = { renderButton, cancelButton },
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:getProperties().width, -- same dimensions as the layout group
height = layoutGrp:getProperties().height,
}
-- gui callback
function guiCallback(component, event)
if event == octane.gui.eventType.BUTTON_CLICKED then
if component == fileChooseButton then
elseif component == renderButton then
isCanceled = false
renderButton:updateProperties{enable = false}
cancelButton:updateProperties{enable = true}
render()
renderButton:updateProperties{enable = true}
cancelButton:updateProperties{enable = false}
elseif component == cancelButton then
isCanceled = true
end
elseif event == octane.gui.eventType.WINDOW_CLOSE then
isCanceled = true
end
end
-- hookup the callback with all the GUI elements
fileChooseButton:updateProperties { callback = guiCallback }
renderButton:updateProperties { callback = guiCallback }
cancelButton:updateProperties { callback = guiCallback }
animationWindow:updateProperties { callback = guiCallback }
animationWindow:showWindow()
Note: This script is now installed with Octane, it is available in the menu as Scripts » Render Imported Animation. This version only works with earlier release candidates.
--
Roeland