Some of you already noticed that the turntable and daylight animations disappeared. Let's fix that by rebuilding them in Lua. In this first tutorial we'll show how to build the user interface for the turntable animation. In the next tutorial we'll show how to do the actual rendering of these animations.
Gui components in the Lua API are managed via properties, just a fancy name for a table of key-value pairs. In the API browser you can find what properties look like. All properties in the API have the PROP_ prefix. When creating a gui component we just create properties and pass them into the octane.gui.create function. When creating a component, all properties are optional except for the type property. Octane needs to now what kind of component to create. When certain properties are omitted, Octane will choose some sane defaults.
The things we like to control in a turntable animation are:
- The rotation angle, the number of degrees we turn around the target position on a circle arc.
- The start angle, we might want to start somewhere halfway the circle.
- The target offset, the distance from the camera position to the target position.
- The duration in seconds of our animation.
- The framerate for our animation
- The number of frames we'd like to render, this is coupled to the duration and the frame rate.
- The quality, how many samples/px we like per frame of our animation.
- It's always handy to specify where the rendered frames are saved.
- A render button to start rendering the animation.
- A cancel button for when we change our mind.
- Just for eye candy let's add a progress bar so we know if we still have time for coffee.
Code: Select all
-- 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
Code: Select all
-- 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
Code: Select all
-- lets create a bunch of labels and sliders
local degLbl = createLabel("Degrees")
local offsetLbl = createLabel("Start Angle")
local targetLbl = createLabel("Target Offset")
local durationLbl = createLabel("Duration")
local frameRateLbl = createLabel("Framerate")
local frameLbl = createLabel("Frames")
local samplesLbl = createLabel("Samples/px")
local degSlider = createSlider(360, -360 , 360, 1)
local offsetSlider = createSlider(0 , -180 , 180, 1)
local targetSlider = createSlider(10 , 0.001, 100, 0.001)
local durationSlider = createSlider(10 , 1 , 3600 , 1)
local frameRateSlider = createSlider(25 , 10, 120 , 1)
local frameSlider = createSlider(250, 10, 432000, 1)
local samplesSlider = createSlider(400, 1 , 16000, 1)
Code: Select all
-- 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 =
{
degLbl , degSlider ,
offsetLbl , offsetSlider ,
targetLbl , targetSlider ,
durationLbl , durationSlider ,
frameRateLbl , frameRateSlider ,
frameLbl , frameSlider ,
samplesLbl , samplesSlider ,
},
padding = { 2 }, -- internal padding in each cell
inset = { 5 }, -- inset of the group component itself
}
from manually typing in some bogus. We pack these components in a group component:
Code: Select all
-- 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 },
}
so we just get the properties of the file group and get the width property and multiply it by 0.8. Afterwards we put the progress bar in a group. We could add the progress bar directly to the window (we create later) but it's easier to layout when it's embedded in a group:
Code: Select all
-- 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,
}
Code: Select all
-- 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,
}
Code: Select all
-- 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
}
Code: Select all
-- window that holds all components
local turntableWindow = octane.gui.create
{
type = octane.gui.componentType.WINDOW,
text = "Turntable Animation",
children = { layoutGrp },
width = layoutGrp:getProperties().width, -- same dimensions as the layout group
height = layoutGrp:getProperties().height,
}
Code: Select all
-- hookup the callback with all the GUI elements
degSlider:updateProperties { callback = guiCallback }
offsetSlider:updateProperties { callback = guiCallback }
targetSlider:updateProperties { callback = guiCallback }
durationSlider:updateProperties { callback = guiCallback }
frameRateSlider:updateProperties { callback = guiCallback }
frameSlider:updateProperties { callback = guiCallback }
samplesSlider:updateProperties { callback = guiCallback }
fileChooseButton:updateProperties { callback = guiCallback }
renderButton:updateProperties { callback = guiCallback }
cancelButton:updateProperties { callback = guiCallback }
turntableWindow:updateProperties { callback = guiCallback }
Code: Select all
-- the script will block here until the window closes
turntableWindow:showWindow()
Code: Select all
--
-- Turntable animation script
--
-- 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 degLbl = createLabel("Degrees")
local offsetLbl = createLabel("Start Angle")
local targetLbl = createLabel("Target Offset")
local durationLbl = createLabel("Duration")
local frameRateLbl = createLabel("Framerate")
local frameLbl = createLabel("Frames")
local samplesLbl = createLabel("Samples/px")
local degSlider = createSlider(360, -360 , 360, 1)
local offsetSlider = createSlider(0 , -180 , 180, 1)
local targetSlider = createSlider(10 , 0.001, 100, 0.001)
local durationSlider = createSlider(10 , 1 , 3600 , 1)
local frameRateSlider = createSlider(25 , 10, 120 , 1)
local frameSlider = createSlider(250, 10, 432000, 1)
local samplesSlider = createSlider(400, 1 , 16000, 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 = 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 =
{
degLbl , degSlider ,
offsetLbl , offsetSlider ,
targetLbl , targetSlider ,
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 turntableWindow = octane.gui.create
{
type = octane.gui.componentType.WINDOW,
text = "Turntable Animation",
children = { layoutGrp },
width = layoutGrp:getProperties().width,
height = layoutGrp:getProperties().height,
}
-- callback handling the GUI elements
local function guiCallback(component, event)
if component == degSlider then print("hello from degSlider") end
if component == offsetSlider then print("hello from offsetSlider") end
if component == targetSlider then print("hello from targetSlider") end
if component == durationSlider then print("hello from durationSlider") end
if component == frameRateSlider then print("hello from frameRateSlider") end
if component == frameSlider then print("hello from frameSlider") end
if component == samplesSlider then print("hello from samplesSlider") end
if component == fileChooseButton then print("hello from fileChooseButton") end
if component == renderButton then print("hello from renderButton") end
if component == cancelButton then print("hello from cancelButton") end
if component == turntableWindow then print("hello from turntableWindow") end
end
-- hookup the callback with all the GUI elements
degSlider:updateProperties { callback = guiCallback }
offsetSlider:updateProperties { callback = guiCallback }
targetSlider:updateProperties { callback = guiCallback }
durationSlider:updateProperties { callback = guiCallback }
frameRateSlider:updateProperties { callback = guiCallback }
frameSlider:updateProperties { callback = guiCallback }
samplesSlider:updateProperties { callback = guiCallback }
fileChooseButton:updateProperties { callback = guiCallback }
renderButton:updateProperties { callback = guiCallback }
cancelButton:updateProperties { callback = guiCallback }
turntableWindow:updateProperties { callback = guiCallback }
-- the script will block here until the window closes
turntableWindow:showWindow()
Next time we'll show you how to do the actual turntable animation rendering. We'll use this script when it's done in the standalone edition. So if somebody comes up with a nicer component, we'll include his instead. This is your chance for eternal glory

cheers,
Thomas