Yaw, Roll and Pitch Animation rendering

Forums: Yaw, Roll and Pitch Animation rendering
Forum for OctaneRender Lua scripting examples, discussion and support.

Yaw, Roll and Pitch Animation rendering

Postby stratified » Sun Dec 29, 2013 5:14 am

stratified Sun Dec 29, 2013 5:14 am
Hi everybody,

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:

animation.png


[youtube]http://www.youtube.com/watch?v=RXdwfk1YoUk&feature=youtu.be[/youtube]

cheers,
Thomas
Last edited by stratified on Sun Dec 29, 2013 6:48 am, edited 1 time in total.
User avatar
stratified
OctaneRender Team
OctaneRender Team
 
Posts: 945
Joined: Wed Aug 15, 2012 6:32 am
Location: Auckland, New Zealand

Re: Yaw, Roll and Pitch Animation rendering

Postby pixelrush » Sun Dec 29, 2013 6:42 am

pixelrush Sun Dec 29, 2013 6:42 am
Interesting. Thanks. Following along.. ;)

I have a small request.
When I run a script the editor gets in the way of viewing progress in the render window.
I cant minimise the editor window or change its size. The running script UI has the focus.
I can move it to one side and back but it would be nice to be able to minimise it...

Also I note that once you have browsed to the output location and entered your details it isn't possible to type a change directly into the field, you need to re-browse.
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: Yaw, Roll and Pitch Animation rendering

Postby stratified » Sun Dec 29, 2013 6:58 am

stratified Sun Dec 29, 2013 6:58 am
When running a script, the rest of Octane is disabled. We do this to avoid nasty side effects. I guess we'll change that once we allow non-modal dialogs in Lua but that won't happen in 1.5.

The file editor is disabled all the time. We only use it to display the path that was selected. We do this because we're lazy, we don't want to filter out all bogus paths ourselves. If you want, you can enable it yourself by modifying the following code:

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


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

Re: Yaw, Roll and Pitch Animation rendering

Postby r-username » Sun Dec 29, 2013 3:09 pm

r-username Sun Dec 29, 2013 3:09 pm
.

Awesome! :D

My hope is that after a few months I will have the ability to write animation scripts like this from scratch, but currently it's not within my grasp. This (and any additional scripts) are a big time saver for my workflow.
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: Yaw, Roll and Pitch Animation rendering

Postby Tugpsx » Sun Dec 29, 2013 8:35 pm

Tugpsx Sun Dec 29, 2013 8:35 pm
Thanks again for this awesome script. Keep them coming
Dell Win Vista 32 | NVIDIA Quadro NVS135M | Core2 Duo T7500 2.20GHz | 4GB
CyberPowerPC Win 7 64| NVIDIA GTX660Ti (3G) GTX480(1.5G) | 16GB
Tugpsx
Licensed Customer
Licensed Customer
 
Posts: 1145
Joined: Thu Feb 04, 2010 8:04 pm
Location: Chicago, IL

Return to Lua Scripting


Who is online

Users browsing this forum: No registered users and 4 guests

Thu Mar 28, 2024 5:53 pm [ UTC ]