Daylight animations in Lua

Forums: Daylight animations in Lua
Forum for OctaneRender Lua scripting examples, discussion and support.

Daylight animations in Lua

Postby stratified » Tue Dec 31, 2013 3:35 am

stratified Tue Dec 31, 2013 3:35 am
Hi everybody,

Here's a script to render daylight animations. It has the same functionality as the daylight animation rendering in pre 1.5 releases. The script is a modified version of the turntable script (posted here http://render.otoy.com/forum/viewtopic.php?f=73&t=37456). Fortunately for us, daylight animations are a bit simpler than turntable animations. All we have to do is animate the hour of the sun direction node connected to the daylight environment node. Here's the code:

Code: Select all
-- Daylight animation rendering, this script should replace the functionality
-- of the daylight animation pre 1.5.
--
-- @description Daylight animation rendering
-- @author      Thomas Loockx
-- @version     0.1
--


-------------------------------------------------------------------------------
-- 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 = "Daylight Animation Error",
        text  = text,
    }
    if halt then error("ERROR: "..text) end
end


-- lets create a bunch of labels and sliders
local startLbl     = createLabel("Start Hour")
local endLbl       = createLabel("End Hour")
local durationLbl  = createLabel("Duration")
local frameRateLbl = createLabel("Framerate")
local frameLbl     = createLabel("Frames")
local samplesLbl   = createLabel("Samples/px")

local startSlider   = createSlider(0, 0, 24, 1, false)
local endSlider     = createSlider(24, 0, 24, 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 =
    {
        startLbl         , startSlider     ,
        endLbl           , endSlider       ,
        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 daylightWindow = octane.gui.create
{
    type     = octane.gui.componentType.WINDOW,
    text     = "Daylight 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 sun direction node connected to the daylight environment
--  * the render target node
-- These copies prevent 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
    if not copyRt or copyRt:getProperties().type ~= octane.NT_RENDERTARGET then
        showError("no render target selected", true)
    end

    -- check if a daylight environment is connected to the render target
    local copyEnv = copyRt:getConnectedNode(octane.P_ENVIRONMENT)
    if not copyEnv or copyEnv:getProperties().type ~= octane.NT_ENV_DAYLIGHT then
        showError("no daylight environment connected to the render target", true)
    end

    -- check if a sun direction is connected to the daylight environment
    local copySunDir = copyEnv:getConnectedNode(octane.P_SUN_DIR)
    if not copySunDir or copySunDir:getProperties().type ~= octane.NT_SUN_DIRECTION then
        showError("no sun direction connected to the daylight environment", true)
    end

    return copyScene, copyRt, copySunDir
end


-- Animates the hour node connected to the daylight environment
local function setHourAnimator(sunDirNode, startHour, endHour, nbFrames)

    -- calculate the new hour for each frame
    local hours = {}
    for i=0,nbFrames-1 do
        -- calculate the hour for each frame
        local hour = startHour + (endHour - startHour) * (i / nbFrames)
        -- store the new hour
        table.insert(hours, { hour, 0, 0 } )
    end

    -- animate the hour
    sunDirNode:getConnectedNode(octane.P_HOUR):setAnimator(octane.A_VALUE, { 0 }, hours, 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, sunDirNode, path)
    -- clear the cancel flag
    IS_CANCELLED = false

    -- motion blur doesn't make sense here so we disable it
    octane.render.setShutterTime(0)

    -- disable part of the ui except for the cancel button
    startSlider     :updateProperties{ enable = false }
    endSlider       :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 startHour = startSlider:getProperties().value
    local endHour   = endSlider:getProperties().value
    local nbFrames  = frameSlider:getProperties().value
    local nbSamples = samplesSlider:getProperties().value
    local outPath   = fileEditor:getProperties().text

    -- set up the animator for the hour
    setHourAnimator(sunDirNode, startHour, endHour, 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
    startSlider     :updateProperties{ enable = true  }
    endSlider       :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()
    -- 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 sun dir in global variables
SCENE_GRAPH, RT_NODE, SUN_DIR_NODE = getSceneCopy()

initGui()

-- 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 a fresh copy to make sure that when we render the second
        -- time, we start with a pristine state
        SCENE_GRAPH, RT_NODE, SUN_DIR_NODE = getSceneCopy()
        startRender(SCENE_GRAPH, RT_NODE, SUN_DIR_NODE, OUT_PATH)
    elseif component == cancelButton then
        cancelRender()
    elseif component == daylightWindow 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 }
daylightWindow:updateProperties   { callback = guiCallback }

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


Here's the code for downloaders:

daylight-animation.lua
daylight animation script
(16.3 KiB) Downloaded 389 times


And here's mr Beep enjoying the sunset:

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

If there are any questions, please ask.

cheers,
Thomas
Last edited by stratified on Tue Dec 31, 2013 7:31 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: Daylight animations in Lua

Postby Tugpsx » Tue Dec 31, 2013 5:27 am

Tugpsx Tue Dec 31, 2013 5:27 am
Thanks for sharing. Interesting I got this vector error issue.
DLScriptCapture.JPG
Day Light script error
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

Re: Daylight animations in Lua

Postby stratified » Tue Dec 31, 2013 7:33 am

stratified Tue Dec 31, 2013 7:33 am
My mistake, setAnimator expects a list of vectors in 1.23, we relaxed this in 1.24.

I've update the initial post, it should work now.

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

Re: Daylight animations in Lua

Postby Tugpsx » Tue Dec 31, 2013 5:09 pm

Tugpsx Tue Dec 31, 2013 5:09 pm
Confirmed working now.

Hand.gif
Animated Daylight
Hand.gif (1.33 MiB) Viewed 4754 times
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

Re: Daylight animations in Lua

Postby stratified » Tue Dec 31, 2013 10:53 pm

stratified Tue Dec 31, 2013 10:53 pm
okay, thanks for testing!

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

Return to Lua Scripting


Who is online

Users browsing this forum: No registered users and 3 guests

Thu Mar 28, 2024 10:39 pm [ UTC ]