Skip to content

Blender Addon

The Plume3D Blender addon lets you attach references to a Wren class and TOML config on objects or collections. Config and scripts live in your project (source control); the .blend file stores only the references (config path, script path, class name).

Download and install Blender 4.2 or newer (Blender 5.0 recommended) from the official Blender release index:

Blender releases → Blender 5.0

Pick the installer for your OS (Windows, macOS, Linux). The addon requires Blender 4.2+ and also ships an extension manifest for the Blender Extensions platform.

Email hello@wyldmagic.gg to request the Plume3D addon after Blender is installed, then follow the installation steps below.

Pipeline: config and scripts outside .blend

Section titled “Pipeline: config and scripts outside .blend”
  • TOML config files (e.g. radio.toml) — Define [Radio] and properties. The addon stores the path, not the file contents.
  • Wren scripts (e.g. main.wren or scripts/radio.wren) — Define one or more classes. The addon stores the script path and class name so the engine knows which file to load and which class to use for the object/collection.
  • Config path (e.g. radio.toml) — Path to the TOML file in the project. Not embedded; the engine loads this from the project when loading the .blend.
  • Script path (e.g. scripts/radio.wren) — Path to the Wren script in the project. Not embedded; the engine loads it at runtime.
  • Class name (e.g. Radio) — Which class in that script to use. Required because a script can define multiple classes. The search button next to the field lists the classes declared in the referenced script.
  • Node Id — Stable identifier for this node within an instantiated hierarchy. The root instance exposes .nodes[node_id] so script code can use .nodes[node_id] to access child nodes. Node ids must be unique — the panel’s Validate button checks this.
  • Shader Name — Optional Slang shader module key for the object (resolution order: material shader > object shader > default).
  • Tags (named layers) — Layer names are defined in your app’s game.toml [Physics] section as tag_layers. Set Game Config Path (on the root collection) to that game.toml so the addon shows these names as checkboxes. Used for raycast and gameplay filtering. When no game.toml is configured, fallback names come from the addon preferences.
  • Collision Layers (named layers) — Layer names are defined in game.toml [Physics] as collision_layers. Same as Tags: set Game Config Path so the addon shows the names. Used for physics collisions and raycast inclusion/exclusion. The default mask is 1 (the engine’s “Default” layer), so untouched objects keep colliding. See Configuration (game.toml) for the full Physics section and collision matrix.
  • Physics collider (any mesh object) — a Collider shape enum (None / Box / Sphere / Capsule / Convex Hull / Mesh) that makes the engine build a static Jolt collider on import (Scene.loadBlendScene / BlendResult.instantiateUnits), so a tree / rock / fence / prop blocks movement with no script. Primitives are sized from the object’s bounds (scale baked in); Convex Hull / Mesh use its geometry (Mesh is static only). Picking a shape reveals Static (uncheck for kinematic) and Collision Layer (bitmask) (the gameplay query-filter layer). This panel appears on any mesh object, independent of the Wren-script config. The addon writes it to the plume3d_collider, plume3d_collider_static, and plume3d_collider_layer custom properties on save; see Blender-authored colliders for the full contract.
  • Water surface (any mesh object) — a Mark as Water checkbox that makes the engine draw the mesh as a stylized water surface on import (Scene.loadBlendScene / BlendResult.instantiateWater) — the artist-authoring on-ramp for water, parallel to the collider panel for props. Ticking it reveals a Style dropdown (Realistic → water_realistic, Toon → water_toon) and Shallow / Deep colour pickers. The addon writes plume3d_water (the style) + plume3d_water_shallow / plume3d_water_deep on save (un-ticking removes them). This panel appears on any mesh object, independent of the Wren-script config. Subdivide the mesh (a plane grid, not a single quad) so the wave displacement has vertices; a box tagged plume3d_water_volume (below) instead auto-generates a subdivided surface. See the Water Blender demo for a hand-authored scene and instantiateWater for the full tag/param contract.

Tag-based authoring conventions (custom properties). Beyond the panel fields above, a few custom properties turn a plain mesh into engine behaviour on import — the “tag a mesh, get a feature” pattern, resolved by Scene.loadBlendScene with no Wren wiring. Set them via Object Properties › Custom Properties (or a small bpy script):

  • plume3d_terrain (a bool / int, or a string) — makes the mesh a renderable ground node and an automatic static concave Jolt collider (instantiateTerrain, engine PLM-254).
  • plume3d_water (a string) — makes the mesh a transparent, auto-drawn stylized water surface (instantiateWater, engine PLM-274, ADR 0092) — the artist-authoring on-ramp for water, parallel to plume3d_terrain for ground. The Water surface panel above sets this for you; set it by hand when you need a style/params the panel doesn’t expose. The string names the water shader (toon → water_toon, realistic / pbr → water_realistic, else the value as a shader basename — the shader stays pack content). Optional float-array props tune the look without touching Wren: plume3d_water_shallow / _deep / _foam / _waves / _horizon (and the Realistic-only _pbr / _refract / _caustic / _crest / _caustic_strength / _fog). See instantiateWater for the full tag table and the Blend Water Demo + Water Blender.
    • plume3d_water_volume (a truthy int / any string) — tag it on a box (alongside plume3d_water) and the engine generates a level water surface at the box’s world top face instead of drawing the box — a level designer drops a box to define a pool / lake with no water mesh to model or place (engine PLM-275, ADR 0093). Optional plume3d_water_subdiv (int, or [subX, subZ]; clamped 1..256) overrides the tessellation and plume3d_water_level (float) sets an absolute waterline. See Box/volume water.

Root-only (paths): Only the root of an instantiation gets Class Name, Config Path, and Class Path. If any ancestor (object parent, collection hierarchy, or a collection-instance Empty) has Enable Wren Script Config set, the addon shows only Node Id, Shader Name, and Tags / Collision Layers for the child; the Paths section is hidden and inherited from the ancestor. If a child still carries old root config, the panel offers a one-click Clear Overridden Config.

Paths are project-relative (forward slashes on every OS) and must live inside the .blend’s project — the directory tree containing game.toml. Picking a file from another project is rejected, and creating a new file never overwrites an existing one.

When loading a .blend, the engine:

  1. Reads references from the .blend (config path, script path, class name per object/collection).
  2. Validates that the config path and script path exist in the project and that the class exists in the script. Raises a runtime exception if not found.
  3. Loads the TOML from the project (e.g. Config.load("radio.toml")).
  4. Loads the script (if not already loaded) and hooks the object/collection to the specified class. Lifecycle: engine calls init() on create, update(dt) each frame, destroy() on remove (no-op if the method is not defined).
  1. In Blender: Edit → Preferences → Add-ons → Install…
  2. Option A — single file: Select plume3d_wren.py (from the extracted addon zip).
  3. Option B — zip: Select the addon zip directly. The zip must contain the addon folder (holding __init__.py, plume3d_wren.py, and blender_manifest.toml) at its root, not loose files.
  4. Option C — extension (Blender 4.2+): Edit → Preferences → Get Extensions → Install from Disk → select the zip (the addon ships a blender_manifest.toml).
  5. In the addon list, set the category to Development or search for Plume3D, then enable the addon.

Tag and Collision layer names are defined in your app’s game.toml in the optional [Physics] section (tag_layers and collision_layers). Set Game Config Path on the root collection to your game.toml so the addon can show those names in the UI. See Configuration (game.toml) for how to set up Physics.

The engine reads class names and node IDs from custom properties (ID properties) in the .blend file. The addon writes your Wren Script Config into these custom properties when you save — only on objects/collections that actually carry config; everything else is left untouched. The panel also has a Write Config to .blend button to sync on demand.

After configuring Plume3D on objects/collections, save the .blend (File → Save or Ctrl+S / Cmd+S). Until you save, the engine will not see classes (e.g. Radio) and instantiate("Radio", ...) will return null.

  1. Open your .blend in Blender with the Plume3D addon enabled.
  2. Configure Wren Script Config on the root collection and objects as needed.
  3. Press Validate Plume3D Config to catch problems (missing files, bad TOML, class not in script, duplicate node ids) before saving.
  4. Save the file (File → Save). The addon writes config to custom properties on save.
  1. In Blender, open the 3D Viewport and press N to open the sidebar (or use the arrow tab on the right).
  2. Click the Plume3D tab. The panel is context-sensitive: it shows the Wren config for whatever you have selected in the Outliner.
  3. Object: Select an object in the Outliner or viewport. The panel shows Object: [name] and its config. Enable Enable Wren Script Config, then set Config Path, Class Path (script file), and Class Name. Use the folder/file icons to browse or create files.
  4. Collection: Select a collection in the Outliner (click the collection name). The panel shows Collection: [name] and its config. Edit Config Path, Class Path, and Class Name the same way.
  5. Class Path is the path to the .wren script that contains the class. Class Name (e.g. Radio) is the class the engine will use. The engine validates that the class exists in the script at load; runtime exception if not found.
  6. Node Id / Tags / Collision Layers: Set Node Id so the instantiated root can look up this node via .nodes[node_id]. Tags and Collision Layers appear as named checkboxes; the layer names come from your game.toml [Physics] section (tag_layers and collision_layers). Set Game Config Path on the root collection to your game.toml so the addon shows these names. See Configuration (game.toml).
  7. Multi-select: select several objects/collections in the Outliner to batch-toggle tag/collision layers across the whole selection (a dash icon marks a mixed bit).

Grass, foliage and trees are authored in Blender and imported as efficient GPU instancing. Modern Blender scatter — geometry nodes, particle hair, collection instances — produces its per-instance transforms only in Blender’s evaluated depsgraph; they are never in the static .blend. The add-on’s Bake Scatter for Plume3D operator (the Plume3D › Scatter Bake panel in the 3D-viewport sidebar) evaluates the depsgraph and writes a compact baked instance table (Plume_Scatter empties carrying plume3d_scatter_* custom properties). The engine loads that table and emits one GPU-instanced draw per prototype — 10,000 grass blades become one draw call.

Workflow:

  1. Model a prototype (a grass blade, a tree) in its own collection.
  2. Scatter it with geometry nodes (recommended), a particle system, or collection instances.
  3. Plume3D › Scatter Bake › Bake Scatter for Plume3D.
  4. Save the .blend, then load it in-engine with Scene.loadBlendScene(path) (or BlendResult.instantiateScatter(scene) directly).

Coordinate/units conversion (Blender Z-up → engine Y-up) is automatic. See the Blend Scatter Demo and Foliage + Terrain Demo for the import side.

Two authoring paths control where scatter spawns. They compose — geometry nodes shapes the density, the mask image trims it further:

  1. Geometry-nodes density mask (already works, no add-on setting) — drive a Distribute Points on Faces density with a vertex-group weight or a texture / attribute, the standard Blender way to paint clearings, thin toward paths, or follow biome edges. Because Bake Scatter walks the evaluated depsgraph, that masked density is already captured — nothing extra to set, and density, scale and rotation can all follow the mask in the nodes. This is the most flexible path.
  2. The add-on’s mask image — a quick top-down mask over any scatter source (including particles / collection instances that have no geometry-nodes density input). In the Scatter Bake panel, under Scatter mask (where to spawn), pick a Mask image and a Seed:
    • The mask is read top-down and stretched to the scatter’s world-XY bounding box (auto-fit — the image covers exactly the scattered area).
    • White = spawn · black = clear · gray = sparse. An instance at mask luminance L is kept with probability L via a deterministic seeded hash, so density scales smoothly and re-bakes are stable (same seed → same result). Paint a black path and no blades bake there; a gray gradient thins them out.
    • The mask is a bake-time input only — it is resolved into the baked instance table and the engine never sees it (nothing is written to the .blend’s engine-read properties). An unreadable or empty mask fails open (spawns everything) — it never silently clears the scene.

Full guide in the engine repo: docs/BLENDER_SCATTER_AUTHORING.md.

  • Load .blend: The engine reads from the .blend config_path, script_path, and class_name per object/collection (references only).
  • Validation: When loading a .blend, the engine checks that each referenced config path and script path exist in the project and that class name exists in the script. Raises a runtime exception if not found.
  • Load config: The engine loads the TOML from the project using config_path — the file is the source of truth.
  • Load script / class: The engine loads the script from the project using script_path and uses class_name to select which class to instantiate.
  • Lifecycle: The engine tries to call init() on create, update(dt) each frame, destroy() on remove; no-op if the method is not defined.

For loading blends in code, see Resource.loadBlend and BlendResult.instantiate.