Feature: Plugin Scripting

Status: Mostly working. Missing features and probably buggy in places.


What does it do?

Unlike the existing OpenBrush API, plugin scripting is designed to run small scripts that directly modify the behaviour of various features while you are actually using them. For example a script might move the pointer as you are painting or add new strokes in response to your actions.

What's it good for?

Changing the way Open Brush responds to user actions. Adding new mirror modes or creating a new custom brush tool.

How do I install it?

Download a build for your headset from the link above and unzip it. You can run the Windows exe directly. To install the Quest apk use SideQuest:

How do I use it?

There are new buttons on the scipts panel that allow you to set an active runtime script in (currently) one of three categories: Tool Scripts, Symmetry Scripts and Pointer Scripts. Use the arrow buttons to choose a script and activate it with the large button on the left of each row.

How do I write my own plugin scripts?

Note that this is in the early stage of development and I'm still changing my mind about many parts
All scripts are written in Lua (this is different to the Http Api where you can use any language as commands are simply messages sent over Http)
The best reference is probably the examples scripts themselves. Most API commands are listed in the Lua Autocomplete File (which you can copy to your scripts folder and many code editors will automatically use it to give you some level of autocomplete).
In addition to the commands and properties listed in the autocomplete file, you can use most static methods from the Unity MathF class. Most of the Lua Standard Library is supported also
The type of script is determined by the filename prefix.
Script types are as follows:

Pointer Scripts

Pointer Scripts can modify the pointer position and/or rotation every frame. You have acceess to the current position and rotation so you can simply add an offset - or you could create an entirely new position or rotation based on any of the other context variables that are available to the script.
Name a Pointer Script with the prefix "PointerScript". For example: PointerScript.Wiggle.lua
A Pointer Script should return x, y, z for pointer rotation and x, y, z for pointer rotation.
For example:
function WhileTriggerPressed()
speed = 5
radius = 0.25
angle = app.time * speed
r = brush.pressure * radius;
pos = {math.sin(angle) * r, math.cos(angle) * r, 0}
rot = {0, 0, 0}
return {pos, rot}

Tool Scripts

Tool Scripts are run when you release the trigger and they have access to the position and rotation of the controller when the trigger was first pressed as well as the position and rotation of the controller when the trigger is released.
Name a Tool Script with the prefix "ToolScript". For example: ToolScript.Circle.lua
Tool Scripts should return a list of points (and optionally rotations) that define a brush stroke.
For example:
function WhileTriggerPressed()
speed = 16
radius = 1
angle = app.time * speed
r = 0
if (brush.triggerIsPressed) then
r = radius * brush.timeSincePressed
pos = {math.sin(angle) * r, math.cos(angle) * r, 0}
rot = {0, 0, 0}
return {pos, rot}

Symmetry Scripts

Symmetry Scripts should return a list of positions and/or rotations that represent additional pointers that will create their own strokes as the user draws with the primary pointer.
The number of pointers should not change once a brush stroke has begun, but may change between each brush stroke.
Name a Symmetry Script with the prefix "SymmetryScript". For example SymmetryScript.FourCopies.lua
Symmetry Scripts should return a list of positions (and optionally rotations) for each additional pointer.
For example:
function Main()
copies = 12
pointers = {}
theta = 360.0 / copies
for i = 1, copies - 1 do
table.insert(pointers, {position={0, 0, 0}, rotation={0, i * theta, 0}})
return pointers

Context Variables

The following realtime values from the sketch are available to use in your scripts (list is currently incomplete. More docs to come):
  • pointer.position: {float x, float y, float z} The current position of the pointer relative to the canvas
  • pointer.rotation: {float x, float y, float z} The current orientation of the pointer relative to the canvas
  • pointer.rgb: {float r, float g, float b} The current brush color
  • pointer.hsv: {float h, float s, float v} The current brush colour converted to HSV
  • pointer.size: float The current brush size
  • pointer.size01: float The current brush size normalized to lie in the range 0 to 1
  • pointer.pressure: float The current pressure of the pointer (how hard the trigger is being pressed)
  • pointer.brush: string The current brush name
  • app.time: float The time in seconds since Open Brush was launched
  • canvas.scale: float The current scale of the canvas
  • canvas.strokeCount: int The total number of strokes

Script Widgets

Scripts can specify parameters that appear as UI widgets such as sliders. These appear in a popup when you click the "3 dots" button on the right of each row. For example you might want a script to have sliders to control "speed" or "wigglyness".
You define the widgets for each parameter in your script. Here's an example:
Widgets = {
speed={label="Speed", type="float", min=0.01, max=32, default=16},
radius={label="Radius", type="float", min=0.01, max=5, default=1},
function Main()
angle = app.time * speed
r = pointer.pressure * radius;
pos = {math.sin(angle) * r, math.cos(angle) * r, 0}
rot = {0, 0, 0}
return {pos, rot}
Here we have defined two widgets. Both are floats (rather than integers) and we have defined a minimum, a maximum and a default value. You can set the ltext abel that appears when you hover over each slider.
The name of each widget (here "speed" and "radius") are then available to the script as variables.

Coordinate Spaces

By default each script type works relative to an origin and has a rotation that makes sense for each of the three types. Pointer and Tool Scripts are relative to the user's brush hand and Symmetry Scripts are relative to the Mirror Widget.
You can override this. For example, here's a PointerScript that is relative to the canvas. We can then position the pointer so it is always at y=0 (the floor) but still tracks the pointer in the x and z directions:
Settings = {
function Main()
return {
{pointer.position.x, 0, pointer.position.z}
Note that we have to manually specific pointer.position.x and pointer.position.y. If we were using space="pointer" (the default) then we wouldn't need to as coordinates are automatically relative to pointer.position
Valid spaces are "pointer", "canvas" and "widget" (the mirror widget)

Known Issues

There's lots more I want to do with this including scripted jitter and scripted geometry creation.

How do I get help

Come over to the Open Brush Discord and chat to me ( @andybak#5425 ). I'm on UK time but I check in fairly regularly.

Can I see it in action?