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.
Our component will have heaps of text labels so lets create a utility function to create a text label. All the function does is create a label from the passed in parameters and return it:
- 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
Numeric values in Octane are usually manipulated with sliders so lets create a function to create a slider:
- 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
Now let's make sure our functions pay off and create some sliders and labels. Because slider don't have text, we put a text label in front of each slider to tell what the slider does:
- 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)
With Octane you can choose to manually layout all the components by specifying the x and y offsets of the components. This will get you pixel perfect interfaces but it's tedious. We prefer to use the group component. A group component is a grid of rows and columns. Each cell of the grid takes exactly one component. The group component will make sure that the components are nicely aligned. Optionally the group component can have a border around it with a title. So let's create a group component to layout our labels and sliders:
- 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
}
We don't want to manually specify the path to the output file so let's create a button that when clicked shows the user a dialog where he can choose a file. Next to the button we create a text editor that shows the path the user has chosen. We don't enable the text editor to prevent the user
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 },
}
For eye candy let's add a progress bar. We'd like to have it 80% the width of the file group above
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,
}
We need 2 buttons to start rendering and to cancel if necessary. The same routine, we create the buttons and add them to a group for convenience. On this group, we don't show the border:
- 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,
}
To make sure all groups are nicely stacked, we add all the groups in another "layout" group. For this group don't show a border. Quick tip: notice the debug property in the group properties. When this property is set to true it will draw the grid of the group component. This may come in handy when trying to get a layout right.
- 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
}
Now we have all our components we're ready to create our window. This window has one child, the layout group. We create it with the same size of the layout group to make sure we don't clip our components:
- 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,
}
So how do we actually react to a button click, a slider change or ... . This is done by hooking up a callback function in the component's properties. This function is called when the user does something with the component (e.g. clicks a button). Each component can have it's own callback function but here let's just have a single callback function. The callback function takes 2 parameters. The first is the component on which the event happened, the second is the event. For now let's say hello from each component:
- 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 }
Phew, all we have to do now is show the window by calling octane.gui.showWindow. The script will block on this function. All the logic must be done via the callback functions from now on:
- Code: Select all
-- the script will block here until the window closes
turntableWindow:showWindow()
If all went fine you should have something like this:
- 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()
And when you run it, it should look like this:
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