I hope the xmas turkey is fully digested by now. I modified the turntable animation script a bit to animate yaw, pitch and roll camera rotations. It just serves as another example to get people hooked on Lua scripting There's a bug in the combo box behaviour but this should be fixed in the next release.
Here's the code:
- Code: Select all
--
-- Yaw, pitch & roll 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.ERROR_DIALOG,
title = "Animation Error",
text = text,
}
if halt then error("ERROR: "..text) end
end
-- lets create a bunch of labels and sliders
local typeLbl = createLabel("Animation Type")
local degLbl = createLabel("Degrees")
local offsetLbl = createLabel("Start Angle")
local durationLbl = createLabel("Duration")
local frameRateLbl = createLabel("Framerate")
local frameLbl = createLabel("Frames")
local samplesLbl = createLabel("Samples/px")
local degSlider = createSlider(360, -360 , 360, 1, false)
local offsetSlider = createSlider(0 , -180 , 180, 1, false)
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)
-- combo box to select yaw, pitch or roll
local typeCombo = octane.gui.create
{
type = octane.gui.componentType.COMBO_BOX,
items = { "yaw", "pitch", "roll" },
selectedItem = "yaw",
}
-- 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 =
{
typeLbl , typeCombo ,
degLbl , degSlider ,
offsetLbl , offsetSlider ,
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
-- 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 = 20,
width = 400,
height = 20,
enable = false,
}
-- 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 },
}
-- 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,
}
-- 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,
}
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 window = octane.gui.create
{
type = octane.gui.componentType.WINDOW,
text = "Yaw, Pitch & Roll 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.nodegraph.getRootGraph():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.nodegraph.getRootGraph():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, 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 values = {}
for i=0,nbFrames-1 do
-- calculate the angle of rotation for this frame
local angle = math.rad( (i / nbFrames) * rotAngle + offsetAngle )
print("angle: ", angle)
-- for yaw we rotate the viewing dir around the up vector and calculate
-- a fresh target position
if animType == "yaw" then
local newViewDir = octane.vec.rotate(origViewDir, origCamUp, angle)
local newCamTarget = octane.vec.add(origCamPosition, newViewDir)
table.insert(values, newCamTarget)
-- for pitch we rotate the viewing dir around the right vector and calculate
-- a fresh target position
elseif animType == "pitch" then
local newViewDir = octane.vec.rotate(origViewDir, origCamRight, angle)
local newCamTarget = octane.vec.add(origCamPosition, newViewDir)
table.insert(values, newCamTarget)
-- for rolling we rotate the up vector around the viewing dir
else
local newCamUp = octane.vec.rotate(origCamUp, octane.vec.normalized(origViewDir), angle)
table.insert(values, newCamUp)
end
end
-- animate the camera position
if animType == "roll" then
camNode:getConnectedNode(octane.P_UP):setAnimator(octane.A_VALUE, { 0 }, values, 1 / nbFrames)
else
camNode:getConnectedNode(octane.P_TARGET):setAnimator(octane.A_VALUE, { 0 }, values, 1 / nbFrames)
end
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()
-- check if rendering was cancelled
if (IS_CANCELLED) then
octane.render.stop()
return
end
end
local function startRender(sceneGraph, rtNode, camNode, path)
-- clear the cancel flag
IS_CANCELLED = false
-- TODO: add a motion blur slider
octane.render.setShutterTime(0)
-- disable part of the ui except for the cancel button
typeCombo :updateProperties{ enable = false }
degSlider :updateProperties{ enable = false }
offsetSlider :updateProperties{ enable = false }
samplesSlider :updateProperties{ enable = false }
durationSlider :updateProperties{ enable = false }
frameRateSlider :updateProperties{ enable = false }
frameSlider :updateProperties{ enable = false }
fileChooseButton:updateProperties{ enable = false }
cancelButton :updateProperties{ enable = true }
-- get the presets from the GUI
local animType = typeCombo:getProperties().selectedItem
local rotAngle = degSlider:getProperties().value
local offsetAngle = offsetSlider:getProperties().value
local nbFrames = frameSlider:getProperties().value
local nbSamples = samplesSlider:getProperties().value
local outPath = fileEditor:getProperties().text
-- set up the animator for the camera
setCamAnimator(camNode, rotAngle, offsetAngle, animType, nbFrames)
-- start rendering out each frame
local currentTime = 0
for frame=1,nbFrames do
-- set the time in the scene
sceneGraph:updateTime(currentTime)
-- update the progress bar
progressBar:updateProperties{ text = string.format("rendering frame %d", frame) }
-- 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" }
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 / nbFrames)
-- update the progress bar
progressBar:updateProperties{ progress = frame / nbFrames }
end
-- enable part of the ui except for the cancel button
typeCombo :updateProperties{ enable = true }
degSlider :updateProperties{ enable = true }
offsetSlider :updateProperties{ enable = true }
samplesSlider :updateProperties{ enable = true }
durationSlider :updateProperties{ enable = true }
frameRateSlider :updateProperties{ enable = true }
frameSlider :updateProperties{ enable = true }
fileChooseButton:updateProperties{ enable = true }
cancelButton :updateProperties{ enable = false }
-- update the progress bar
progressBar:updateProperties{ progress = 0, text = "finished" }
end
local function cancelRender()
IS_CANCELLED = true
octane.render.stop()
end
------------------------------------------------------------------------------
-- Main Flow
-- global variable that holds the output path
OUT_PATH = nil
-- Get the render target and camera in global variables
SCENE_GRAPH, RT_NODE, CAM_NODE = getSceneCopy()
-- render and cancel button are disable
renderButton:updateProperties{ enable = false }
cancelButton:updateProperties{ enable = false }
-- 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:getProperties().value * frameRateSlider:getProperties().value)
frameSlider:updateProperties{ value = frames }
elseif component == frameRateSlider then
-- if the frame rate changes, update the #frames
local frames = math.ceil(durationSlider:getProperties().value * frameRateSlider:getProperties().value)
frameSlider:updateProperties{ value = frames }
elseif component == frameSlider then
-- if the #frames changes, update the duration of the animation
local duration = frameSlider:getProperties().value / frameRateSlider:getProperties().value
durationSlider:updateProperties{ value = duration }
elseif 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
renderButton:updateProperties{ enable = true }
fileEditor:updateProperties{ text = ret.result }
OUT_PATH = ret.result
else
renderButton:updateProperties{ enable = false }
fileEditor:updateProperties{ text = "" }
OUT_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 == cancelButton then
cancelRender()
elseif component == window 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:updateProperties { callback = guiCallback }
frameRateSlider:updateProperties { callback = guiCallback }
frameSlider:updateProperties { callback = guiCallback }
fileChooseButton:updateProperties { callback = guiCallback }
renderButton:updateProperties { callback = guiCallback }
cancelButton:updateProperties { callback = guiCallback }
window:updateProperties { callback = guiCallback }
-- the script will block here until the window closes
window:showWindow()
If you run the script, the window should look like this:
[youtube]http://www.youtube.com/watch?v=RXdwfk1YoUk&feature=youtu.be[/youtube]
cheers,
Thomas