Script to render imported animations

Forums: Script to render imported animations
Forum for OctaneRender Lua scripting examples, discussion and support.

Script to render imported animations

Postby roeland » Sun Dec 15, 2013 11:26 pm

roeland Sun Dec 15, 2013 11:26 pm
With the ability to import camera paths and rigid body animations via Alembic, we now can render the animation in Octane without having to open a new OCS file all the time. So we will now make a script rendering a basic animation.

Of course we don't have to start from scratch: the UI is similar to the turntable animation script here: http://render.otoy.com/forum/viewtopic.php?f=73&t=37313

I will highlight mostly the rendering parts.

The first step is inspecting the scene and figuring out what to render. we get the first render target in the scene, and also get the total length of the animation via the root node graph. We also set up some global state needed in the render callback.
Code: Select all
local root = octane.nodegraph.getRootGraph()
local alltargets = root:findNodes(octane.NT_RENDERTARGET)
if #alltargets == 0 then
    error("Please set up your render target in the main graph.", 0)
end

local rtnode  = alltargets[1]

local interval = root:getAnimationTimeSpan()

-- cancel flag and other globals

isCanceled = false
startT = 0
endT = 0
dT = 0
currentT = 0


The render callback checks if the rendering was canceled, and it updates the progress bar:
Code: Select all
function renderCallback(result)
    if isCanceled then
        octane.render.stop()
    end
   
    local t = currentT + dT * result.samples / result.maxSamples
    t = (t - startT) / (endT - startT)
    progressBar:updateProperties{progress = t}
end


The render function is called when we click the render button, and contains the main loop rendering all the frames. First we get some settings from our GUI components. Note that for this to work, we need to store these components in global variables (i.e. don't declare them as local):
Code: Select all
    startT = startSlider:getProperties().value
    endT = endSlider:getProperties().value
    dT = 1 / fpsSlider:getProperties().value
   
    if endT < startT then return end

    local file = fileEditor:getProperties().text


Then comes some pattern black magic, to figure out where to put the sequence number in the file name. We figure out if the file name already contains a sequence number, and we split it up in the part before and after the sequence number.
Code: Select all
    -- check if we have a file name given
    local prefix, sequenceMatch, suffix
   
    -- If we have a file name, check where to substitute the sequence number:
    -- pattern tutorial here: http://lua-users.org/wiki/PatternsTutorial
    if file ~= "" then
   
        -- 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
        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"
       
        -- display resulting file name
        fileEditor:updateProperties{text = prefix..sequenceMatch..suffix}
    end


Then comes the render loop. We increment the current time stamp until we get to the end time set up via the sliders. Due to rounding errors, and because the end time is the last animated time stamp, we need to make sure this time stamp is also rendered, even if some small rounding errors occur.

For each frame we update the time stamp of the root graph. This automatically updates all animated attributes in the scene. Then we can call octane.render.start, which will block until the render is finished.

After rendering we check if the rendering is canceled, and if not we save the image if a file name was given. To give some feedback on what we are saving, we update the file text field.

Finally we increment the current time.
Code: Select all
    currentT = startT
    local seq = 0
   
    -- careful with rounding errors
    while currentT < endT + .001 * dT do
        -- time and sequence number
        seq = seq + 1
        root:updateTime(currentT)

        -- render
        local result = octane.render.start
        {
            renderTargetNode = rtnode,
            callback = renderCallback,
            maxSamples = samplesSlider:getProperties().value
        }
       
        if isCanceled then
            -- canceled
            progressBar:updateProperties{progress = 0}
            return
        elseif file ~= "" then
            -- see if a file was given, and replace the sequence number
            local thisFile = prefix..seqPattern:format(seq)..suffix
            fileEditor:updateProperties{text = thisFile}
            octane.render.saveImage(thisFile, octane.render.imageType.PNG8)
        end
       
        -- go to next time stamp
        currentT = currentT + dT
    end
   
    -- set the progress bar to 100%. 1.0 activates indefinite mode, so...
    progressBar:updateProperties{progress = .99999}


Most of the GUI code is very similar to the turntable animation script, so I'm not repeating it here. There are some different sliders, and some sliders and the file name text box are stored in global variables, so we can access them from the callbacks.

We still have to hook up callbacks to our buttons. We will also stop the render if the window is closed.
Code: Select all
function guiCallback(component, event)
    if event == octane.gui.eventType.BUTTON_CLICKED then
        if component == fileChooseButton then
            -- OK, this is not implemented yet...
        elseif component == renderButton then
            isCanceled = false
            renderButton:updateProperties{enable = false}
            cancelButton:updateProperties{enable = true}
            render()
            renderButton:updateProperties{enable = true}
            cancelButton:updateProperties{enable = false}
        elseif component == cancelButton then
            isCanceled = true
        end
    elseif event == octane.gui.eventType.WINDOW_CLOSE then
        isCanceled = true
    end
end


We can do some further improvements to this script. We still have to hook up the output... button and the shutter time slider. For now you can set the shutter time in the time line below the rendered image before you start the script.

A small downside of this script is that it modifies the scene. If you want to render multiple scenes in a batch, this will be a problem, because Octane displays a prompt to save changes before opening a new scene. To avoid this we can copy the root node graph to a temporary root graph created in the script, and render from this one.

But for now, here is the complete script.

Code: Select all
-- gather some info about the stuff to render

local root = octane.nodegraph.getRootGraph()
local alltargets = root:findNodes(octane.NT_RENDERTARGET)
if #alltargets == 0 then
    error("Please set up your render target in the main graph.", 0)
end

local rtnode  = alltargets[1]

local interval = root:getAnimationTimeSpan()

-- cancel flag and other globals

isCanceled = false
startT = 0
endT = 0
dT = 0
currentT = 0

-- the actual rendering is quite easy.

-- Render callback

function renderCallback(result)
    if isCanceled then
        octane.render.stop()
    end
   
    local t = currentT + dT * result.samples / result.maxSamples
    t = (t - startT) / (endT - startT)
    progressBar:updateProperties{progress = t}
end

-- Render main loop

function render()
    startT = startSlider:getProperties().value
    endT = endSlider:getProperties().value
    dT = 1 / fpsSlider:getProperties().value
   
    if endT < startT then return end

    local file = fileEditor:getProperties().text
   
    -- check if we have a file name given
    local prefix, sequenceMatch, suffix
   
    -- If we have a file name, check where to substitute the sequence number:
    -- pattern tutorial here: http://lua-users.org/wiki/PatternsTutorial
    if file ~= "" then
   
        -- 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
        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"
       
        -- display resulting file name
        fileEditor:updateProperties{text = prefix..sequenceMatch..suffix}
    end

    currentT = startT
    local seq = 0
   
    -- careful with rounding errors
    while currentT < endT + .001 * dT do
        -- time and sequence number
        seq = seq + 1
        root:updateTime(currentT)

        -- render
        local result = octane.render.start
        {
            renderTargetNode = rtnode,
            callback = renderCallback,
            maxSamples = samplesSlider:getProperties().value
        }
       
        if isCanceled then
            -- canceled
            progressBar:updateProperties{progress = 0}
            return
        elseif file ~= "" then
            -- see if a file was given, and replace the sequence number
            local thisFile = prefix..seqPattern:format(seq)..suffix
            fileEditor:updateProperties{text = thisFile}
            octane.render.saveImage(thisFile, octane.render.imageType.PNG8)
        end
       
        -- go to next time stamp
        currentT = currentT + dT
    end
   
    -- set the progress bar to 100%. 1.0 activates indefinite mode, so...
    progressBar:updateProperties{progress = .99999}
end

-- 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 startLbl    = createLabel("Start time")
local endLbl      = createLabel("End time")
local fpsLbl      = createLabel("Framerate")
local shutterLbl  = createLabel("Shutter time")
local samplesLbl  = createLabel("Samples/px")

startSlider   = createSlider(interval[1], interval[1], interval[2], .001)
endSlider     = createSlider(interval[2], interval[1], interval[2], .001)
fpsSlider     = createSlider(25 , 10, 120   , 1)
shutterSlider = createSlider(0, 0, 1, 1/25)
samplesSlider = createSlider(400, 1 , 16000, 1)
samplesSlider:updateProperties{logarithmic = 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     ,
        fpsLbl        , fpsSlider     ,
        shutterLbl    , shutterSlider ,
        samplesLbl    , samplesSlider ,
    },
    padding  = { 2 },   -- internal padding in each cell
    inset    = { 5 },   -- inset of the group component itself
}

-- 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
fileEditor = octane.gui.create
{
    type    = octane.gui.componentType.TEXT_EDITOR,
    text    = "",
    x       = 20,
    width   = 400,
    height  = 20,
}

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

-- eye candy, a progress bar
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,
    enable = false,
}

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 animationWindow = octane.gui.create
{
    type     = octane.gui.componentType.WINDOW,
    text     = "Render Animation",
    children = { layoutGrp },
    width    = layoutGrp:getProperties().width, -- same dimensions as the layout group
    height   = layoutGrp:getProperties().height,
}

-- gui callback

function guiCallback(component, event)
    if event == octane.gui.eventType.BUTTON_CLICKED then
        if component == fileChooseButton then
           
        elseif component == renderButton then
            isCanceled = false
            renderButton:updateProperties{enable = false}
            cancelButton:updateProperties{enable = true}
            render()
            renderButton:updateProperties{enable = true}
            cancelButton:updateProperties{enable = false}
        elseif component == cancelButton then
            isCanceled = true
        end
    elseif event == octane.gui.eventType.WINDOW_CLOSE then
        isCanceled = true
    end
end

-- hookup the callback with all the GUI elements
fileChooseButton:updateProperties { callback = guiCallback }
renderButton:updateProperties     { callback = guiCallback }
cancelButton:updateProperties     { callback = guiCallback }
animationWindow:updateProperties  { callback = guiCallback }

animationWindow:showWindow()


Note: This script is now installed with Octane, it is available in the menu as Scripts » Render Imported Animation. This version only works with earlier release candidates.
Render animation.lua
(11.61 KiB) Downloaded 584 times


--
Roeland
User avatar
roeland
OctaneRender Team
OctaneRender Team
 
Posts: 1811
Joined: Wed Mar 09, 2011 10:09 pm

Re: Script to render imported animations

Postby Tugpsx » Mon Dec 16, 2013 2:18 am

Tugpsx Mon Dec 16, 2013 2:18 am
Thanks, This will come in handy. Both the Cancel and Output buttons need linking, they have no function.
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: Script to render imported animations

Postby roeland » Mon Dec 16, 2013 2:21 am

roeland Mon Dec 16, 2013 2:21 am
Yeah it is not finished yet.

With the cancel button you can cancel the rendering after it is started.

--
Roeland
User avatar
roeland
OctaneRender Team
OctaneRender Team
 
Posts: 1811
Joined: Wed Mar 09, 2011 10:09 pm

Re: Script to render imported animations

Postby Tugpsx » Mon Dec 16, 2013 2:35 am

Tugpsx Mon Dec 16, 2013 2:35 am
roeland wrote:Yeah it is not finished yet.

With the cancel button you can cancel the rendering after it is started.

--
Roeland

Don't get me wrong. This is well appreciated. Thanks for the clarification. Will also look into a mouse over option to show description or function.
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: Script to render imported animations

Postby stratified » Mon Dec 16, 2013 3:03 am

stratified Mon Dec 16, 2013 3:03 am
setting tooltips from Lua isn't supported yet, we'll add that.

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

Re: Script to render imported animations

Postby bepeg4d » Mon Dec 16, 2013 10:32 am

bepeg4d Mon Dec 16, 2013 10:32 am
wow, thanks roeland for this :)
the render and cancel buttons work very well, i was able to see and render a sequence with camera mb, fantastic :)
the only issue is that the "output" button doesn't work :(
shouldn't be an "exportToFile" somewhere in the script?
ciao beppe
Attachments
Capture-st122-01.JPG
User avatar
bepeg4d
Octane Guru
Octane Guru
 
Posts: 9959
Joined: Wed Jun 02, 2010 6:02 am
Location: Italy

Re: Script to render imported animations

Postby bepeg4d » Mon Dec 16, 2013 5:08 pm

bepeg4d Mon Dec 16, 2013 5:08 pm
sorry roeland, forgot my last post :)
after reading carefully your script, i have found this:
Code: Select all
 -- if the file name doesn't contain a sequence number, the match fails

so, i have understand that my mistake was to not specify the 0000 in the file name :)
thanks again, i was able to save my first alembic animation with camera mb :D
ciao beppe
Attachments
Capture-st122-02.JPG
User avatar
bepeg4d
Octane Guru
Octane Guru
 
Posts: 9959
Joined: Wed Jun 02, 2010 6:02 am
Location: Italy

Re: Script to render imported animations

Postby roeland » Mon Dec 16, 2013 10:29 pm

roeland Mon Dec 16, 2013 10:29 pm
The output button is not hooked up yet, you have to fill in the file name text box yourself. But it should automatically add a sequence number and a .png extension.

--
Roeland
User avatar
roeland
OctaneRender Team
OctaneRender Team
 
Posts: 1811
Joined: Wed Mar 09, 2011 10:09 pm

Re: Script to render imported animations

Postby p3taoctane » Wed Dec 18, 2013 2:48 pm

p3taoctane Wed Dec 18, 2013 2:48 pm
This might be a dumb-ass question... is the LUA render animation script available for us all to use? I have an animation I would would like to try.

Secondly how do you run it. Import a node etc?

Thanks

I could not code myself out of a wet paper bag and even though I can see the power of this option I know I will be a blood sucking leech and rely on the code monkeys to develop cool scripts. Maybe we can start a Creative Crash section for Octane render scripts : > )
Windows 7 Pro_SP 1_64 bit_48 GB Ram_Intel Xeon X5660 2.80 GHZ x2_6 580GTX_1 Quadra 4800
p3taoctane
Licensed Customer
Licensed Customer
 
Posts: 1418
Joined: Mon Jan 25, 2010 12:53 am

Re: Script to render imported animations

Postby stratified » Wed Dec 18, 2013 7:20 pm

stratified Wed Dec 18, 2013 7:20 pm
sure, you can use all the scripts on the forum however you like...

We realise that not everybody can code and that's fine. We're striving to get a collection of useful scripts available to everybody (now via the forum, later via the LiveDB). We hope to get a scripting "community" going for Octane and maybe some people even start writing scripts for fun and profit.

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

Fri Apr 26, 2024 12:18 pm [ UTC ]