Repository navigation
In App Plugins
Versions: this page covers plugin API 3, available since OxyBrush 3.0.7-beta (download the latest release). Releases before it run API 1 and 2; rows and sections marked (3) or "API 3" need 3.0.7-beta or later. This page is mirrored from
docs/pluginsin the repository.
In-app plugins add commands and panels to OxyBrush. They run inside the app, in a sandbox: a plugin can only do what its manifest declares, it can't touch files or the network, and every change it makes is one normal undo step.
A plugin can:
- read and change the meshes: positions, mask, vertex color, face groups, hidden faces, and, since API 3, the triangles themselves;
- add new pieces;
- draw lines over the viewport;
- show sliders, switches, choices and colors in its own panel;
- show its result live before you apply it (API 3).
You can write one in any language that compiles to WebAssembly (Rust, C, Zig, AssemblyScript and others), or directly in WebAssembly text, the way the plugins that ship with OxyBrush are written.
Also see:
-
Manifest reference: everything
plugin.jsoncan say. - Host API reference: every function a plugin can call.
Python scripts are a different kind of plugin. They run as a separate program over the local agent server, and are covered in Python Plugin Development.
- Plugins in the menu bar lists every in-app plugin. Each one has a submenu with its commands and its panel, where you set it up.
- Settings > Plugins shows what each plugin is allowed to do ("reads meshes", "changes topology", …). It also lists any plugin that failed to load, and why, and has Remove for plugins you installed.
-
Install add-on… installs an
.oxyaddonfile: a plugin packed into one file. Open plugins folder shows where plugins live, and Reload plugins picks up changes. -
The command palette lists every plugin command as
Plugin: <name> — <command>.
Some commands show their result before applying it: Displace and Group by Height, for example. Clicking one opens the plugin's panel and shows the result in place of the active piece. While it's shown:
- moving a setting updates it;
- Accept applies exactly what you see, as one undo step;
- Cancel, closing the panel, or editing the piece some other way leaves the piece as it was.
While a command runs, its panel shows a progress bar (if the plugin reports progress) and Stop. Nothing a stopped command did is applied. A plugin that runs far too long is stopped automatically.
A plugin's panel settings are saved with OxyBrush's settings and come back next time. Reset in the panel puts them back to the plugin's defaults.
A plugin is a folder inside the plugins folder:
plugins/
my_plugin/
plugin.json the manifest: name, commands, settings, capabilities
plugin.wasm the compiled module (or plugin.wat, WebAssembly text)
Plugins > Open plugins folder opens it. It is:
| System | Folder |
|---|---|
| Windows | %LOCALAPPDATA%\OxyBrush\plugins |
| macOS | ~/Library/Application Support/oxybrush/plugins |
| Linux | ~/.local/share/oxybrush/plugins |
The Flatpak keeps it inside its own sandbox; use Open plugins folder to find it.
The plugins that ship with OxyBrush are copied into this folder when it starts, if they aren't already there:
- Squash
- Displace
- Mask by Height
- Color Ramp
- Array
- Measure
- Group by Height
- Flip Faces
Their source (plugin.wat) is a good place to start. A plugin you've
changed is never overwritten. To get a newer version of a bundled plugin,
delete its folder and restart.
This plugin logs a message in the status bar. Make a folder hello in
the plugins folder with these two files.
plugin.json:
{
"name": "Hello",
"version": "1.0",
"description": "Says hello in the status bar.",
"api": 3,
"commands": [{ "id": "hello", "label": "Say hello" }]
}plugin.wat:
(module
(import "oxybrush" "log" (func $log (param i32 i32)))
(memory (export "memory") 1)
(data (i32.const 0) "Hello from a plugin")
(func (export "run") (param $command i32)
(call $log (i32.const 0) (i32.const 19))))Then choose Plugins > Reload plugins, and Plugins > Hello > Say hello.
Every plugin:
- exports its
memory; - exports
run(command: i32), which is called with the index of the command clicked (0 for the first incommands); - may export
overlay(), called each frame while its panel is open, to draw lines (needs theoverlaycapability).
Any language works if it compiles to wasm32-unknown-unknown with no
WASI. This Rust plugin moves the active piece up, except where it is
masked, with a live preview.
Cargo.toml:
[package]
name = "lift"
version = "1.0.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "s"
lto = truesrc/lib.rs:
// The host functions this plugin uses, from the "oxybrush" import module.
#[link(wasm_import_module = "oxybrush")]
unsafe extern "C" {
fn log(ptr: i32, len: i32);
fn param(index: i32) -> f32;
fn active_piece() -> i32;
fn vertex_count(piece: i32) -> i32;
fn read_positions(piece: i32, ptr: i32, max: i32) -> i32;
fn read_mask(piece: i32, ptr: i32, max: i32) -> i32;
fn write_positions(piece: i32, ptr: i32, count: i32) -> i32;
}
fn say(text: &str) {
unsafe { log(text.as_ptr() as i32, text.len() as i32) }
}
/// Called with the index of the command the user clicked.
#[unsafe(no_mangle)]
pub extern "C" fn run(_command: i32) {
unsafe {
let piece = active_piece();
let n = vertex_count(piece);
if n <= 0 {
return;
}
let mut pos = vec![0f32; n as usize * 3];
let mut mask = vec![0f32; n as usize];
read_positions(piece, pos.as_mut_ptr() as i32, n);
read_mask(piece, mask.as_mut_ptr() as i32, n);
// The piece's height, from its lowest and highest vertex.
let ys = pos.iter().skip(1).step_by(3);
let (lo, hi) = ys.fold((f32::MAX, f32::MIN), |(lo, hi), &y| (lo.min(y), hi.max(y)));
let lift = (hi - lo) * param(0) / 100.0;
for (i, m) in mask.iter().enumerate() {
pos[i * 3 + 1] += lift * (1.0 - m);
}
if write_positions(piece, pos.as_ptr() as i32, n) == 0 {
say("Lifted");
}
}
}plugin.json:
{
"name": "Lift",
"version": "1.0",
"description": "Moves the active piece up, except where it is masked.",
"api": 3,
"capabilities": ["read_mesh", "edit_mesh"],
"commands": [{ "id": "lift", "label": "Lift", "preview": true }],
"params": [{ "id": "amount", "label": "Amount (% of height)", "min": 0, "max": 50, "default": 10 }]
}Build it, and put the module next to the manifest as plugin.wasm:
rustup target add wasm32-unknown-unknowncargo build --release --target wasm32-unknown-unknowncp target/wasm32-unknown-unknown/release/lift.wasm plugin.wasmPointers are plain i32 offsets into the plugin's own memory, so a
Vec's pointer can be passed straight to the host.
An add-on is a ZIP of the plugin folder, with plugin.json at the top.
oxybrush-cli (in the portable downloads) makes and checks one:
oxybrush-cli addon pack my_plugin my_plugin.oxyaddonoxybrush-cli addon inspect my_plugin.oxyaddonTo install one, use Plugins > Install add-on…, or open the file with OxyBrush. Installing never runs the plugin. A broken plugin is refused, with the reason, before anything is installed.
- Only the host API. There are no files, no network, no clock, no environment and no WASI. A module that imports anything else doesn't load.
- Only what's declared. Each function checks the plugin's capabilities and returns an error code if it wasn't declared. A manifest with an unknown capability doesn't load, so a plugin never gets more than it asked for.
- Time and memory are limited. A command gets about a billion instructions and an overlay frame about twenty million; a plugin that runs past that is stopped and stays usable. Memory is capped at 512 MB.
-
Edits are data. A plugin never holds the document. It gets a copy of
the scene and returns the changes it wants, which OxyBrush checks and
applies as one undo step per piece, labelled
Plugin: <name> — <command>. A plugin that crashes can't damage your work.
- Handle error codes. Every function that can fail returns a negative code; see the host API reference.
-
Masks.
read_maskgives 1 for fully protected vertices. Most plugins should leave those where they are. -
Large meshes. A piece's data is copied into your memory, about 12
bytes per vertex per array. Grow memory before reading (
memory.grow, or let your language's allocator do it). Give up with a message if it can't grow. -
Progress. For anything that takes more than a moment, call
progressevery few thousand items, and return early when it returns 1. -
Preview commands should be fast. They run on every setting change.
Heavy commands are better without
"preview". -
Debugging.
logwrites to the status bar and the log file (Help > Open log folder). A plugin that fails to load says why in Settings > Plugins.
"api" in the manifest says which version a plugin was written for.
OxyBrush runs every version from 1 to the current one, so old plugins keep
working. A plugin that needs a newer version than the app has is refused
with a message.
| API | Since | Adds |
|---|---|---|
| 1 | 3.0.2-beta | read positions, triangles and mask; write positions; scene JSON; overlay lines; sliders |
| 2 | 3.0.3-beta | normals and colors; write mask and colors; add pieces; commands in the palette |
| 3 | 3.0.7-beta | face groups and hidden faces; replace a piece's mesh (edit_topology); live preview; toggles, choices, colors, labels and step sliders; param_color; progress and Stop; remembered settings |
OxyBrush is an open-source multiresolution digital sculpting tool written in pure Rust with wgpu. Visit the GitHub repository or download the latest version from Releases.
Getting Started
Sculpting & Workflow
- Sculpting & Brushes
- Subdivision Levels
- Topology & Remeshing
- Masking & Face Groups
- Posing & Transform Gizmo
- Vertex Color & Materials
Scripting & Extensibility
Pipeline & Production