Coroutines in Lua Plugins

Using coroutines to handle tasks that span multiple frames

A number of actions in Visionary Render involve work being done over multiple frames, or seconds. Examples of these are loading and saving files, or responding to property changes.

While it is possible to write Lua code to take this into account, it is often cleaner to keep a sequential list of actions sequential in the code.

Life without coroutines

Consider this example of loading a file and doing something with it:

local function onFileLoaded(file)
  --when the file is loaded, print the name of the first node in the scene
  print(vrTreeRoot().Scenes:child():getName())
end

local function loadFile()
  --register our function to be called when the document is loaded
  __registerCallback("onDocumentLoaded", onFileLoaded)
  --request that the document be loaded
  vrPostCommand("visren_open_document", "path/to/file")
end
In this example, vrPostCommand may not execute immediately. When it does execute, opening a document can take some time so this Lua script can't wait for the file to load and continue where it left off. It has to make use of a callback function to do the work after the file loading operation.

For this contrived example it's not particularly beneficial to introduce coroutines, but what about something like this?

local function timestep(delta)
  -- do something to count the time delta and trigger the screenshot tool when necessary
  if myCount > 0.1 then
    vrLocalUserNode().Toolbox.ScreenCaptureTool.Enabled = true
  end
end

local function onPropertyChange(node, value)
  if not value then
    -- screen capture tool has finished, trigger a fly to the next node in some list
    vrBodyFlyTo(someOtherNode, 0.1)
  end
end

local function myTasks()
  -- rather confusing way to achieve the following steps:
  -- vrBodyFlyTo(someNode, 0.1)
  -- vrLocalUserNode().Toolbox.ScreenCaptureTool.Enabled = true
  -- vrBodyFlyTo(someOtherNode, 0.1)
  -- vrLocalUserNode().Toolbox.ScreenCaptureTool.Enabled = true

  __registerCallback("onTimestepEvent", timestep)
  vrAddPropertyObserver("myObserver", onPropertyChange, "ScreenCaptureTool", "Enabled")
  vrBodyFlyTo(someNode, 0.1)
  --timestep triggers capture tool
  --then onPropertyChange triggers the next fly to
  --then timestep triggers the capture tool again
end
This script is very confusing. The way to achieve the simple list of steps is very complicated.

Wouldn't it be better if the entire code sample could look like this?

local function myTasks()
  vrBodyFlyTo(someNode, 0.1)
  vr_yield(0.1)
  vrLocalUserNode().Toolbox.ScreenCaptureTool.Enabled = true
  vr_yield()
  vrBodyFlyTo(someOtherNode, 0.1)
  vr_yield(0.1)
  vrLocalUserNode().Toolbox.ScreenCaptureTool.Enabled = true
end

The coroutine way

Providing the yield function requires a bit of boilerplate and there are two method implementations required to account for both cases of waiting for time/updates, and waiting for document events.

Resuming

Before we write a yield function, we should write a function capable of resuming the coroutine. This function can then be used as the function parameter to __registerCallback to automatically resume the Lua execution when the specified event is fired by Visionary Render.

First, define a variable local to your plugin - this can be called anything, but I chose lu_co

local lu_co
Our resume function uses this to resume the coroutine:

local function vr_resume()
  -- resume our coroutine
  local ok, err = coroutine.resume(lu_co)
  -- this block will trigger a Lua error if there are errors while running the coroutine.
  -- by default, these errors are not propogated back to the caller unless we do it here.
  if not ok then
    error(err)
  end
end
Now your Lua script can wait for document events. The first example of loading a file now becomes this:

local function loadFile()
  -- register our function to be called when the document is loaded
  __registerCallback("onDocumentLoaded", vr_resume)
  -- request that the document be loaded
  vrPostCommand("visren_open_document", "path/to/file")
  -- wait for the callback
  coroutine.yield()
  -- when the file is loaded, print the name of the first node in the scene
  print(vrTreeRoot().Scenes:child():getName())
end

Time based yield

For simpler things like waiting some number of seconds, or just a single frame for property changes, we can write another helper function, vr_yield.

local function vr_yield(time)
  -- we make use of the __deferredCall utility function to call vr_resume
  __deferredCall(vr_resume, time or 0)
  coroutine.yield()
end
This adds a small wrapper around coroutine.yield which uses the global timestep callback to call our resume function some time in the future.

Calling vr_yield() with no parameters will result in the script being resumed in the next frame. Using a time value (e.g. vr_yield(0.5)) will resume the script approximately half a second from now. The timing is not completely exact because the timestep event is handed a delta time since the last frame. The lower the framerate, the less accurate this will be.

Running a function as a coroutine

There is one final step required to allow these helpers to function, and that is to run the main function as a coroutine.

Given this small function:

local function myTasks()
  vrBodyFlyTo(node, 1.0)
  vr_yield(1.0)
  print("finished!")
end
to allow vr_yield to function, it should be enclosed in:

lu_co = coroutine.create(function() myTasks() end)
vr_resume()
Alternatively, if the function is simple enough, it need not be separate:

local function myTasks()
  lu_co = coroutine.create(function()
    vrBodyFlyTo(node, 1.0)
    vr_yield(1.0)
    print("finished!")
  end)
  vr_resume()
end
You could even write another helper function:

local function co_wrap(func)
  lu_co = coroutine.create(function() func() end)
  vr_resume()
end

local function myTasks()
  co_wrap(function()
    vrBodyFlyTo(node, 1.0)
    vr_yield(1.0)
    print("finished!")
  end)
end
Congratulations - you can now write more logical Lua code in your plugins.


No Results.

Getting StartedArchitectureBest PracticesHow ToAdvanced TopicsChangelogvrtree_cppCoreForeign Function InterfaceMetanodesMigrationsObserversPropertiesTreeUtilitiesAPI DefinitionsVR ExchangePluginsLua API