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