Turntable animations in Lua (part 2)

Forums: Turntable animations in Lua (part 2)
Forum for OctaneRender Lua scripting examples, discussion and support.

Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 4:45 am

stratified Fri Dec 20, 2013 4:45 am
Hi all,

In this second part we'll finish the turntable animation script we started (see this post for part 1 http://render.otoy.com/forum/viewtopic.php?f=73&t=37313. It uses some lua API functions introduces in Octane version 1.23 so don't be discouraged if it doesn't work in your version of Octane. I won't go over everything in the script, the UI code was covered in part 1. I won't cover everything only the most important parts but if there are any questions please ask. To use it, just select the desired render target and run the script.

Because we don't want to change our original scene while creating an animation, we take a full copy of the whole scene in a separate root graph. Remember, root graphs created in lua aren't added to the project and are destroyed when the script ends. They come in very handy as temporary storage. We do some extra checks to make sure the user has selected a render target and that that render target has a thinlens camera connected to it:

Code: Select all
-- 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


Another thing we need is animate the camera's position. This was already discussed in another post (see this post http://render.otoy.com/forum/viewtopic.php?f=73&t=37455) so I'm not going to repeat myself:

Code: Select all
local function setCamAnimator(camNode, rotAngle, offsetAngle, targetDistance, nbFrames)
    -- get the original camera settings
    local origCamTarget   = camNode:getPinValue(octane.P_TARGET)
    local origCamPosition = camNode:getPinValue(octane.P_POSITION)
    local origCamUp       = camNode:getPinValue(octane.P_UP)
    local origViewDir     = octane.vec.normalized(octane.vec.sub(origCamTarget, origCamPosition))

    -- calculate the new camera position for each frame
    local positions = {}
    for i=0,nbFrames-1 do
        -- calculate the angle of rotation for this frame
        local angle = math.rad( (i / nbFrames) * rotAngle + offsetAngle )
        -- rotate the viewing direction around the up vector
        local newViewDir = octane.vec.rotate(origViewDir, origCamUp, angle)
        -- scale the new view dir with the target distance
        newViewDir = octane.vec.scale(newViewDir, targetDistance)
        -- calculate the new camera position
        local newCamPosition = octane.vec.sub(origCamTarget, newViewDir)
        -- store the new camera position
        table.insert(positions, newCamPosition)
    end

    -- animate the camera position
    camNode:getConnectedNode(octane.P_POSITION):setAnimator(octane.A_VALUE, { 0 }, positions, 1 / nbFrames)
end


For creating the filename for each frame we created a helper function createSavePath. It takes the original path the user selected and tries to squeeze in the number of the frame. I stole this code from Roeland, he discusses it in this post http://render.otoy.com/forum/viewtopic.php?f=73&t=37387:

Code: Select all
-- 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


The interesting code is in the startRender function. First we need to make sure we set the IS_CANCELLED flag to false. Then we set the shutter time for camera motion blur. Actually adding a slider to control motion blur is to be done. Next we disable most of the UI except the cancel button. We don't want people playing with this while rendering, we do want to allow them to cancel. Then we hook up the animator for the camera. And then (finally) we're ready to start rendering. What we do is run a loop, update the time for the next frame and render that frame. We also update the progress bar and check if we're not canceled. We register a render callback as well but all it does is check if we weren't canceled.

Code: Select all
-- 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.005)

    -- disable part of the ui except for the cancel button
    degSlider       :updateProperties{ enable = false }
    offsetSlider    :updateProperties{ enable = false }
    targetSlider    :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 rotAngle       = degSlider:getProperties().value
    local offsetAngle    = offsetSlider:getProperties().value
    local targetDistance = targetSlider: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, targetDistance, 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
    degSlider       :updateProperties{ enable = true  }
    offsetSlider    :updateProperties{ enable = true  }
    targetSlider    :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


We need to modify the GUI callback to make everything work. The "logic" is here. The duration, framerate and frames sliders are coupled, when we update one we want to update the others accordingly. When the user clicks the button to choose a file, well we want to show a file chooser. When a file is chosen we can enable the render button. If the user clicks the render button, we simply start rendering. And if he clicks the cancel button we cancel the rendering:

Code: Select all
-- 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
        startRender(SCENE_GRAPH, RT_NODE, CAM_NODE, OUT_PATH)

    elseif component == cancelButton then
        cancelRender()
    elseif component == turntableWindow  then
        -- when the window closes, cancel rendering
        if event == octane.gui.eventType.WINDOW_CLOSE then
            cancelRender()
        end
    end
end


In the end, the script looks like this:

Code: Select all
--
-- Turntable 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 = "Turntable Animation Error",
        text  = text,
    }
    if halt then error("ERROR: "..text) end
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, false)
local offsetSlider    = createSlider(0  , -180 , 180, 1, false)
local targetSlider    = createSlider(10 , 0.001, 10000, 0.001, true)
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)

-- 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,
}


------------------------------------------------------------------------------
-- 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


local function setCamAnimator(camNode, rotAngle, offsetAngle, targetDistance, nbFrames)
    -- get the original camera settings
    local origCamTarget   = camNode:getPinValue(octane.P_TARGET)
    local origCamPosition = camNode:getPinValue(octane.P_POSITION)
    local origCamUp       = camNode:getPinValue(octane.P_UP)
    local origViewDir     = octane.vec.normalized(octane.vec.sub(origCamTarget, origCamPosition))

    -- calculate the new camera position for each frame
    local positions = {}
    for i=0,nbFrames-1 do
        -- calculate the angle of rotation for this frame
        local angle = math.rad( (i / nbFrames) * rotAngle + offsetAngle )
        -- rotate the viewing direction around the up vector
        local newViewDir = octane.vec.rotate(origViewDir, origCamUp, angle)
        -- scale the new view dir with the target distance
        newViewDir = octane.vec.scale(newViewDir, targetDistance)
        -- calculate the new camera position
        local newCamPosition = octane.vec.sub(origCamTarget, newViewDir)
        -- store the new camera position
        table.insert(positions, newCamPosition)
    end

    -- animate the camera position
    camNode:getConnectedNode(octane.P_POSITION):setAnimator(octane.A_VALUE, { 0 }, positions, 1 / nbFrames)
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.005)

    -- disable part of the ui except for the cancel button
    degSlider       :updateProperties{ enable = false }
    offsetSlider    :updateProperties{ enable = false }
    targetSlider    :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 rotAngle       = degSlider:getProperties().value
    local offsetAngle    = offsetSlider:getProperties().value
    local targetDistance = targetSlider: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, targetDistance, 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
    degSlider       :updateProperties{ enable = true  }
    offsetSlider    :updateProperties{ enable = true  }
    targetSlider    :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

local function initGui(camNode)
    -- set the initial target distance in the distance slider
    local target   = camNode:getPinValue(octane.P_TARGET)
    local position = camNode:getPinValue(octane.P_POSITION)
    local viewDir  = octane.vec.sub(target, position)
    local tgtLen   = octane.vec.length(viewDir)
    targetSlider:updateProperties{ value = tgtLen }

    -- render and cancel button are disable
    renderButton:updateProperties{ enable = false } 
    cancelButton:updateProperties{ enable = false } 
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()

-- initialize the UI
initGui(CAM_NODE)

-- 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
        startRender(SCENE_GRAPH, RT_NODE, CAM_NODE, OUT_PATH)

    elseif component == cancelButton then
        cancelRender()
    elseif component == turntableWindow  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 }
turntableWindow:updateProperties  { callback = guiCallback }

-- the script will block here until the window closes
turntableWindow:showWindow()


We realize that this isn't a trivial script. So if there are any questions we're more than happy to help. What's still missing so far is controlling motion blur.

Here's a turntable animation of "Mr Beep" rendered with the script so it works (at least on my machine ;).

mr_beep.gif
Mr Beep rotating


turntable_action.png
turntable action


Here's the script for download:

turntable-animation.lua
turntable animation (version 0.3)
(17.04 KiB) Downloaded 209 times


cheers,
Thomas
Last edited by stratified on Thu Jan 02, 2014 8:33 pm, edited 3 times in total.
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 5:26 am

stratified Fri Dec 20, 2013 5:26 am
If somebody's keen ;) We still need tooltips on the buttons and a slider to control the shutter time for motion blur.

cheers,
Thomas
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Turntable animations in Lua (part 2)

Postby pixelrush » Fri Dec 20, 2013 5:36 am

pixelrush Fri Dec 20, 2013 5:36 am
Not looking at me are you? :roll:
OK so virtually anything is programmable by script then.. stuff like 3d spiral paths, flybys and say combining sun transit and interior lights on/off with it... hmmm... ok thanks for the lesson Thomas. Might mess around with it over Xmas break. Too late for setting homework now. :P
BTW when do you guys go on holidays? I guess you post 1.23 on Mon and then come back in New Year 13th?

Edit: fixed typo
Last edited by pixelrush on Fri Dec 20, 2013 7:01 am, edited 1 time in total.
i7-3820 @4.3Ghz | 24gb | Win7pro-64
GTS 250 display + 2 x GTX 780 cuda| driver 331.65
Octane v1.55
User avatar
pixelrush
Licensed Customer
Licensed Customer
 
Posts: 1618
Joined: Mon Jan 11, 2010 7:11 pm
Location: Nelson, New Zealand

Re: Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 5:49 am

stratified Fri Dec 20, 2013 5:49 am
pixelrush wrote:Not looking at me are you? :roll:
OK so virtually anything is programmable by script then.. stuff like 3d spiral paths, flybys and say combining sun transit and interior lights on/off with it... hmmm... ok thanks for the lesson Thomas. Might mess around with it over Xmas break. Too late for setting homework now. :P
BTW when do you guys go on holidays? I guess you post 2.23 on Mon and then come back in New Year 13th?


1.23 should be released soon...

No, we're not closing the office over the holidays. Octane never sleeps ;)

cheers,
Thomas
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 5:50 am

stratified Fri Dec 20, 2013 5:50 am
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Turntable animations in Lua (part 2)

Postby pixelrush » Fri Dec 20, 2013 8:03 am

pixelrush Fri Dec 20, 2013 8:03 am
got it thanks!

ok so looking at the script you have done above the camera position is stored in a table first. is that correct?
so I can scrub through the calculated table to preview/check the camera animation using the Alembic timeline??

for a spiral path there is a linear change between a min/max offset.
for a circular path they just need to be the same value as default.
if there is a height also varying linearly between a upper/lower it should end up as a cool 3d spiral, right?
wonder how that actually looks... might make for motion sickness :roll: :?
i7-3820 @4.3Ghz | 24gb | Win7pro-64
GTS 250 display + 2 x GTX 780 cuda| driver 331.65
Octane v1.55
User avatar
pixelrush
Licensed Customer
Licensed Customer
 
Posts: 1618
Joined: Mon Jan 11, 2010 7:11 pm
Location: Nelson, New Zealand

Re: Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 10:42 am

stratified Fri Dec 20, 2013 10:42 am
Yep, the position for each frame is stored in a table. This table is used to hook up an animator with the node connected to the camera's position pin.

You can't like stop this script and then scrub the animation to see what it does. That's because we animate a copy of the project, in a separate root graph (which is destroyed when the script stops). This animated graph only exists while the script runs. If you want to experiment with this, You should use the stuff described in this post http://render.otoy.com/forum/viewtopic.php?f=73&t=37455.

To do spiraling, you just have to modify the target distance per frame. Should be easy to hook up.

Up and down spiraling requires rotation around the up vector and you have to tilt the camera (rotate around it's right or left vector).

Cheers,
Thomas
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Turntable animations in Lua (part 2)

Postby r-username » Fri Dec 20, 2013 1:55 pm

r-username Fri Dec 20, 2013 1:55 pm
Thanks for the script
I'm currently using blender 2.69 and looking for ways to get camera animations into octane since as far as I know there is no .abc export yet.

Would a script that tells a camera to follow an imported path be hard to code? So it would be:
1. Get selected camera,
2. look at node x or position x or get cam focus point,
3. follow path node x

Some other thoughts for scripts:
- Select a node or node group, save as lua script to a text file, or right-click node "show in script editor"
- Write render window to video file. I do realtime previews using cam studio, gif-cam but it wuld be great to just run a script to dump the preview to a file.

Thanks again for the great software and updates.
i7 960 - W7x64 - 12 GB - 2x GTX 780ti
http://www.startsimple.com/ - http://www.gigavr.com/
r-username
Licensed Customer
Licensed Customer
 
Posts: 217
Joined: Thu Nov 24, 2011 3:39 pm

Re: Turntable animations in Lua (part 2)

Postby pixelrush » Fri Dec 20, 2013 8:12 pm

pixelrush Fri Dec 20, 2013 8:12 pm
>You can't like stop this script and then scrub the animation....

OK but I could break the script into 2 convenient stages. Settings/Preview and then Render Animation.
After adjusting the settings the camera table is re/calculated. Then I include a 'Preview' button to render (but not save) through every 10th frame, say, and at low samples and small resolution to verify the view/path is as intended. Or, on second thought, perhaps its better to have a slider to choose which frame to preview and it rerenders with each change. I can read from the script table like that, can't I? So its a quasi scrub inside the script but showing in the Octane view... we forget about using that nice Alembic slider... ;)
Then if all is satisfactory I push the big red Do Miracle Now button down :o to run the full and final render set. :D

You can tell I am not a naturally gifted coder can't you? :lol:
i7-3820 @4.3Ghz | 24gb | Win7pro-64
GTS 250 display + 2 x GTX 780 cuda| driver 331.65
Octane v1.55
User avatar
pixelrush
Licensed Customer
Licensed Customer
 
Posts: 1618
Joined: Mon Jan 11, 2010 7:11 pm
Location: Nelson, New Zealand

Re: Turntable animations in Lua (part 2)

Postby stratified » Fri Dec 20, 2013 8:48 pm

stratified Fri Dec 20, 2013 8:48 pm
yes, it should be doable to animate an arbitrary path.

Node & graph selection is exposed in Lua, getSelection(). Or I don't really understand the question ;)

Here's a script to capture the previews. Select a render target node and run it (of course you need to modify the path). It will output all the intermediate results on disk. I created an animated gif from them with ImageMagick. ImageMagick has Lua bindings so I'm sure you could do this directly from the script.

Code: Select all
-- get the selected render target
rtNode = octane.project.getSelection()[1]

function renderCallback(result)
    -- create the filename (MODIFY THIS)
    local fileName = string.format("/tmp/airboat/result_%d.png", result.samples)
    print("saving image: ", fileName)
    -- save the intermediate result
    octane.render.saveImage(fileName, octane.render.imageType.PNG8)
end

-- start rendering
octane.render.restart()
octane.render.start{ renderTargetNode = rtNode, maxSamples = 500, callback = renderCallback }


airboat.gif
created from the previews, not much to see because it converges almost immediately on this simple scene


cheers,
Thomas
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand
Next

Return to Lua Scripting


Who is online

Users browsing this forum: No registered users and 2 guests

Mon Nov 18, 2019 5:42 am [ UTC ]