Open Brush Docs
  • Home
  • How to get Open Brush
  • Differences between Open Brush and Tilt Brush
  • User Guide
    • Get started with Open Brush
    • Painting with Open Brush
    • The Open Brush UI
      • Admin Panel
        • Settings Panel
        • Labs Panel
      • Tools Panel
        • Selection Options
        • Repaint Options
      • Brushes Panel
      • Extras Panel
        • Environment Panel
        • Lights Panel
        • Backdrop Panel
        • Guides Panel
        • Media Library
        • Camera Paths Panel
        • Snap Settings Panel
        • Transform Panel
        • Layers Panel
      • Experimental Panel
      • UI Differences Between Basic Mode and Advanced Mode
    • Grid and Angle Snapping
    • Repaint Tool and Jitter
    • Selection/Erase Filter
    • Lazy Input
    • Bimanual Input and Revolver
    • World Axis Unlock
    • Saving and sharing your Open Brush sketches
    • Troubleshooting issues with Open Brush
    • Check out Labs features
    • Importing Images, Videos and 3D Models
    • Experimental Mode
    • Make moving creations using audio reactive brushes
    • Using Reference Images on Oculus Quest
    • Remixing and Creative Commons
    • Accessing Autosave Files
    • The Open Brush config file
    • Exporting Open Brush sketches to other apps
      • Exporting to Unreal Engine 5
      • Exporting to Snapchat Lens Studio
      • Configuring Export
    • Plugins
      • Example Plugins
        • Example Pointer Plugins
        • Example Symmetry Plugins
        • Example Tool Plugins
        • Example Background Plugins
      • Writing Plugins
        • Getting Started
        • Tweaking existing plugins
        • Writing a Pointer Plugin
        • Writing a Symmetry Plugin
        • Writing a Tool Plugin
        • Writing a Background Plugin
        • Defining and Drawing Brush Strokes
      • Plugin API Scripting Reference
    • Open Brush Unity SDK
    • Open Brush API
      • Retrieving a preview image
      • API Commands List
    • Cameras and Exporting Video
    • Brushes
      • Brush List
      • Memory limits and brush costs
      • Experimental Brushes
      • Hiding Brushes with Brush Tags
    • Using Open Brush without a VR headset
    • Command Line Arguments
    • Tilt Brush Version 23 Release Notes
  • Get Involved!
    • How to help with Testing
  • Pre-release and Experimental Builds
    • Installing the Beta Release
    • "Experimental Mode" Builds
    • Feature: 3D Shapes Tool
    • Feature: Animation Timeline
    • Feature: Icosa Gallery Support
    • Feature: Brush Editing
    • Feature: Plugin Scripting
    • Feature: Sculpting
    • Combined Testing Build
    • Old or Completed Feature Builds
      • Feature: Polyhedra
      • Feature: Snip Tool
      • Feature: Layers
      • Insominx's (michael-g) Experimental Build
      • XR Framework Beta
      • Feature: Transform Panel and Snap Enhancements
      • Feature: Improved GLTF Importer
      • Feature: Multi-Mirror
      • Feature: New Monoscopic Mode
      • Feature: Improved Import/Export
      • Feature: Multiplayer
  • Case Studies
  • Other Resources
  • Developer Notes
    • UI Elements
    • Unity shader examples
    • Setting up CI for Open Brush using Github Actions
    • Open Brush File Format
    • Previous Github Wiki
      • Brushes
      • BuildingOpenBrush
      • BurstCompiler
      • Comparison
      • MonoscopicMode
      • PseudoCode
      • UserInterface
    • Differences between Standard and Experimental Mode
    • Open Brush AsCanvas Notes
    • Unity Versions
  • Release History
    • v2.10 Multiplayer
    • v2.9 (Maintenance)
    • v2.8 Import/Export
    • v2.7 (Maintenance)
    • v2.6 (Maintenance)
    • v2.5 (Maintenance)
    • v2.4 "Prismatic"
    • v2.2: Settings and Sketches
    • v2.1 Hotfix
    • 🚀v2.0: XR Update
    • v1.0: Happy Birthday to Us!
    • Current Beta Release Notes
  • Docs TODO
  • Contacting Us
Powered by GitBook
On this page
Edit on GitHub
Export as PDF
  1. User Guide
  2. Plugins
  3. Writing Plugins

Writing a Symmetry Plugin

PreviousWriting a Pointer PluginNextWriting a Tool Plugin

Last updated 5 months ago

Symmetry Plugins are similar to Pointer plugins with a few differences:

  1. They can return a list of transforms that represent multiple pointers.

  2. They can modify the color, size and brush type for each of the strokes separately.

  3. They have access to the Symmetry Widget which can be used as a point of origin for each pointer. If you've used the Mirror or features in Open Brush then the Symmetry Widget is like the Mirror Widget that controls the reflection plane and center for those mirrors.

Bear in mind - the number of pointers you generate cannot change once a brush stroke has begun, but can change between each brush stroke.

Make sure you name your symmetry plugin scripts with the prefix "SymmetryScript". For example SymmetryScript.MyMirror.lua

Let's start with the simplest possible Symmetry Plugin:

function Main()
    return {Transform.identity}
end

In lua you can define a list of items by enclosing them in curly braces. This example is almost identical to the simplest possible Pointer plugin except that we wrap the transform in curly braces.

You might expect that it behaves the same - i.e. nothing changes and you paint as normal. However if you try it, you'll find that's not the case. In fact - you can't paint at all - your pointer remains stubbornly stuck to the center of the Symmetry Widget.

Why is this? By default Pointer Plugins and Symmetry Plugins use a different . This is how we describe how where the origin is and how translations and rotations are interpreted. Pointer plugins default to using the current brush controller position as the origin so all coordinates are relative to that. Symmetry plugins on the other hand default to using the Symmetry Widget as the origin. So returning a transform that does nothing means that the pointer is stuck at the widget's center.

You can change the default coordinate space by adding a value to the script settings:

Settings = {space="pointer"}

function Main()
    return {Transform.identity}
end

This plugin will now behave the same as the "do nothing" pointer plugin example. Using space="pointer" in a Symmetry Plugin is useful if you want to ignore the widget and have all the pointers move relative to the brush controller.

Let's do something more interesting in our Symmetry Plugin:

function Main()
    origin = Transform.identity
    brushOffset = Transform:New(Symmetry.brushOffset)
    return {
        Transform:Lerp(origin, brushOffset, 0.2),
        Transform:Lerp(origin, brushOffset, 0.4),
        Transform:Lerp(origin, brushOffset, 0.6),
        Transform:Lerp(origin, brushOffset, 0.8)
    }
end

Here we are returning a list of 4 transforms so we will have four separate pointers all creating a stroke at the same time.

Transform.Lerp is a method that takes two transforms as input and blends them by an amount given by the third number. So Transform.Lerp(a, b, 0.5) will return a transform that is exactly half-way between a and b. It's position will be at the midpoint of a and b and will have a rotation and scale exactly halfway between those of a and b.

What are we passing into Transform.Lerp? origin is set to Transform.identity which is our do-nothing transform. As mentioned before - in this case it will be the center point of the symmetry widget.

Usually the value of the user's brush controller is given byBrush.position. This is because by default, symmetry scripts use coordinates centered on the symmetry widget and you'd have to do a some complicated calculations to get from there to the current brush position. If you use symmetry.brushOffset this is done for you.

So this script will create 4 pointers that are spaced evenly between the symmetry widget and the user's brush controller as they paint. Give it a try!

Finally - let's look at one last symmetry script.

function Main()
    copies = 10
    pointers = Path:New()
    angleStep = 360 / copies 
    for i = 1, copies do
        angle = i * angleStep
        pointer = Transform:New(Symmetry.brushOffset, Rotation:New(0, angle, 0))
        pointers:Insert(pointer)
    end
    return pointers
end

Some things to note here:

  1. Instead of creating a list of transforms using curly braces we are creating a Pathobject and then using Insert to add pointers to it one at a time. Path is just a class that stores a list of transforms. It has some useful methods for transforming and modifying itself so is often more useful than just a simple list.

  2. We are using a loop: for i = 1, copies do You should have come across similar constructs in other scripting languages. Any lua tutorial will explain some small differences with lua loops but in general they should behave as you would expect.

  3. angleStepis always 36 in this case but if you wanted to,copies could be a slider that the user sets so we calculate how many degrees to add to the angle for each copy of the pointer.

  4. You don't have to calculate the position of each pointer around the circle. This is done automatically for you. The final position is calculated based on the symmetry widget's transform and the rotation value you return for each pointer. This saves you from having to do some pretty gnarly maths yourself in symmetry plugins.

brushOffset is a with it's position set to the value ofSymmetry.brushOffset. This is a which represents the position of the user's brush controller relative to the symmetry widget. It is a special value that only makes sense in the context of a Symmetry plugin and you won't use it in other types of plugin script.

Transform
Vector3
MultiMirror
coordinate space