Resource
Cached resource loading. The same path returns the same instance (no duplicate loads). All methods are static.
Resource.loadConfig(path)
Section titled “Resource.loadConfig(path)”Returns: Map (or root type from TOML) — Cached config. Same as Config.load but cached by path; same path returns the same object.
Parameters:
path(String) — Path to the config file (e.g. TOML) relative to the mounted project.
Resource.loadBlend(path)
Section titled “Resource.loadBlend(path)”Returns: BlendResult or null — Loaded blend data (classes, configs, node metadata). Cached per path. null if the file could not be loaded.
Parameters:
path(String) — Path to the.blendfile relative to the mounted project (e.g."Models/Cube.blend").
The result does not include a .nodes map; use instantiate to create scene instances and access nodes.
var blend = Resource.loadBlend("Models/Radio.blend")if (blend != null) { var root = blend.instantiate("Radio", _scene) if (root != null && root.nodes.containsKey("body")) { var bodyNode = root.nodes["body"] }}Resource.loadSound(path)
Section titled “Resource.loadSound(path)”Returns: Source or null — Cached audio source. Same path returns the same Source. null on load failure.
Parameters:
path(String) — Path to the audio file (e.g."sounds/click.wav").
var sound = Resource.loadSound("sounds/click.wav")if (sound != null) Audio.play(sound)Resource.list(dir, ext)
Section titled “Resource.list(dir, ext)”Returns: List of String — The filenames (bare, no path) of the files in a mounted directory whose name ends with ext, sorted. Sub-directories are excluded. Empty list if the directory does not exist. Never throws.
Parameters:
dir(String) — A directory relative to the mounted project (e.g."Models"). The mount sandboxes it: a..or absolute path lists nothing rather than escaping the project.ext(String) — A case-insensitive filename suffix to filter by (e.g.".blend"). Pass""to list every file.
Use it to build a picker over the assets that ship with a project — e.g. a model dropdown over Models/*.blend:
for (name in Resource.list("Models", ".blend")) { System.print(name) // "Character.blend", "Zombie.blend", ...}BlendResult
Section titled “BlendResult”Result of Resource.loadBlend. Exposes .classes and .configs (no .nodes on the blend resource itself). Use instantiate(...) to create scene instances and register the blend’s actions.
Custom data (nodes)
Section titled “Custom data (nodes)”| Method / property | Returns | Parameters | Description |
|---|---|---|---|
nodeCount | Num | — | Number of nodes in the blend |
nodeName(index) | String | index (Num) | Name of node at index |
nodeId(index) | (node ID) | index (Num) | ID of node at index |
nodeCustomStringKeys(index) | (list-like) | index (Num) | Custom string keys on node |
nodeCustomIntKeys(index) | (list-like) | index (Num) | Custom int keys on node |
nodeCustomPropertyString(index, key) | String | index, key | Custom string property |
nodeCustomPropertyInt(index, key) | Num | index, key | Custom int property |
instantiate(className, scene) / instantiate(className, scene, prefix) / instantiate(className, scene, args)
Section titled “instantiate(className, scene) / instantiate(className, scene, prefix) / instantiate(className, scene, args)”Returns: an Entity subclass instance, a Node, or null. If the instantiated
root carries a plume3d_class_name whose script you have imported and whose class is Entity, you get
that class’s instance, bound to the node (so you can use its Node functions/values directly). Otherwise
you get the plain Node (root of the instantiated hierarchy, with a .nodes map for children). null if the
class/node was not found or instantiation failed.
Parameters:
className(String) — Class name (or a node id) as set in the Blender addon (e.g."Radio","Cube").scene(Scene) — Scene to add the instance to.- 3rd arg (optional) — either a name
prefix(String, for the instantiated nodes’ animation actions) or the constructorargs(anything else, e.g. aMap) passed to your class’sinit(config, args).
Creates the node subtree in the scene and registers the blend’s actions. When a class is assigned, it is
newd in Wren and init(config, args) is called with the node’s plume3d_config_path TOML (config) and
your args. See Entity for the base class and the class MyThing is Entity pattern.
The returned root’s .nodes map (nodeId → Node) still lets you look up children.
// A plain node comes back as a Node:var cubeRoot = _blend.instantiate("Cube", _scene)if (cubeRoot != null && cubeRoot.nodes.containsKey("model")) { var modelNode = cubeRoot.nodes["model"]}
// A node with a `class MyPlayer is Entity` (imported) comes back as that instance, bound to the node:import "scripts/player" for Player // so the engine can resolve the classvar player = _blend.instantiate("My_Player", _scene, { "hp": 100 })player.setPosition(0, 1, 0) // forwarded from NodeinstantiateScatter(scene)
Section titled “instantiateScatter(scene)”Returns: Num — The number of InstancedMeshes created (one per scatter prototype). 0 if the blend carries no baked scatter.
Parameters:
scene(Scene) — Scene to add the instanced foliage to.
Emits the blend’s baked foliage / scatter as GPU-instanced draws (engine PLM-252, Block BLD). Author grass/tree scatter in Blender with geometry-nodes, particles, or collection-instances, bake it, and each baked prototype (a Plume_Scatter Empty carrying plume3d_scatter_proto + plume3d_scatter_xforms) becomes one InstancedMesh in the scene — a single GPU-instanced draw per prototype, with per-instance colour from the baked table, per-instance frustum culling, and wind sway (the instanced draw path uses the active shader — load a grass/foliage shader first). Coordinates convert automatically (Blender Z-up → engine Y-up).
var blend = Resource.loadBlend("Models/GrassField.blend")var created = blend.instantiateScatter(_scene)System.print("instantiateScatter created %(created) instanced mesh(es)")See the Blend Scatter Demo example.
instantiateTerrain(scene)
Section titled “instantiateTerrain(scene)”Returns: Num — The number of terrain meshes built (each a render node and a static collider).
Parameters:
scene(Scene) — Scene to add the terrain to. If it has no physics world yet, one is created.
Instantiates every mesh tagged plume3d_terrain (a bool / int, or a string custom property) as a renderable ground node and — creating the scene’s physics world if absent — a static concave triangle-mesh Jolt collider from the same geometry (engine PLM-254, Block BLD). One call turns a Blender-sculpted terrain into a walkable, collidable ground: props, characters, and physics bodies rest on it. Coordinates convert automatically (Z-up → Y-up).
var blend = Resource.loadBlend("Models/FoliageTerrain.blend")var built = blend.instantiateTerrain(_scene) // render node(s) + static collider(s) + physics worldvar grass = blend.instantiateScatter(_scene) // the baked foliage on topSee the Foliage + Terrain Demo example.
instantiateWater(scene)
Section titled “instantiateWater(scene)”Returns: Num — The number of water surfaces built (each a transparent, auto-drawn
render node carrying the stylized-water shader + its look params — from a tagged mesh, or
generated from a tagged box volume).
Parameters:
scene(Scene) — Scene to add the water surfaces to.
Instantiates every mesh tagged plume3d_water as a transparent, auto-drawn water surface
using the shared stylized-water core (Gerstner waves + depth colour + refraction + reflection +
fresnel + procedural foam) — the artist-authoring on-ramp for water,
mirroring instantiateTerrain for ground (engine PLM-274,
ADR 0092). Tag a mesh in Blender, and
Scene.loadBlendScene draws it as water — no Wren wiring.
Coordinates convert automatically (Z-up → Y-up).
Or tag a box plume3d_water plus plume3d_water_volume
and the engine generates the surface plane from the box’s top face — no water mesh authored
(engine PLM-275, ADR 0093); see Box/volume water
below.
var blend = Resource.loadBlend("Models/WaterScene.blend")var built = blend.instantiateWater(_scene) // every plume3d_water-tagged mesh → a water surfaceSystem.print("instantiateWater built %(built) water surface(s)")The plume3d_water custom property is a string that names the water shader — the shader
stays pack content, so the engine never ships or hardcodes a water look:
plume3d_water value | Water shader |
|---|---|
"" · "toon" · "1" · "true" · "water" | water_toon (the default stylized Toon water) |
"realistic" · "pbr" | water_realistic (the WTR2 Realistic variant — multi-octave Gerstner + PBR + world-anchored caustics + crest foam; "pbr" kept as a back-compat alias) |
| any other value | that value verbatim as a shader basename (a pack’s own water shader) |
The surface’s look defaults to a variant-selected block — the byte-for-byte twin of
water_playground’s reference Toon or Realistic look, chosen by the resolved shader (WTR2 #3, ADR
0097). It is overridable by optional float-array custom
properties, each overwriting up to four components of a
set-5 custom-material slot (values are finite-guarded — NaN and
±inf are rejected — so you can tune a subset):
| Custom property | Slot | Meaning |
|---|---|---|
plume3d_water_shallow | p0 | shallow-water colour (Realistic also reads p0.w = absorptionK, the depth-absorption rate) |
plume3d_water_deep | p1 | deep-water colour + depthMaxDistance (Toon: world distance over which shallow→deep runs; p1.w is unused by Realistic) |
plume3d_water_foam | p2 | foam colour + foamDistance (Roystan _FoamDistance — foam reach) |
plume3d_water_waves | p3 | Gerstner (gain, wavelength, steepness, speed) — drives the Realistic surface displacement, and should stay in lockstep with the app-side buoyancy Water.setWaves(...) |
plume3d_water_horizon | p10 | horizon/sky colour + infini-water fade start |
These five apply to both variants. The Realistic variant (plume3d_water="realistic") adds
five more numeric knobs — finite-guarded and range-clamped, because finite alone is unsafe for
a pow() exponent (specPower), a normalizer (heightNorm), or a UV multiplier (causticScale);
an out-of-range or malicious value is clamped into its safe window, not written verbatim:
| Custom property (Realistic only) | Slot | Components (clamped range) |
|---|---|---|
plume3d_water_pbr | p4 | roughness (0–1), metallic (0–1), fresnelF0 (0–1) |
plume3d_water_refract | p5 | refractStrength (0–0.5), distortAmt (0–1), chromAmt (0–1), specPower (1–1024) |
plume3d_water_caustic | p6 | causticScale (0.001–64), causticChroma (0–1) |
plume3d_water_crest | p7 | crestFoamThreshold, crestFoamStrength, heightColorAmount, heightNorm (each 0–16; heightNorm ≥ 0.001) |
plume3d_water_caustic_strength | p11.y | scalar causticStrength (0–8), written component-wise so it never clobbers p11.x = useFoamTex |
One more Realistic-only property, plume3d_water_fog = [r, g, b, density] (rgb 0–1, density
0–8), sets the underwater fog for the auto-registered water volume (see Auto-registration for
underwater below); it is not a set-5 slot but
the registered WaterSurfaceState’s fog. The Toon
default block and the colour/wave overrides above are unchanged.
Under the hood a SceneNode can now carry an optional CustomMaterial — the
Scene.draw() analogue of an immediate-mode custom material, “a scene node drawn with a pack
shader + set-5 params” (additive, inert until set, so every existing node is unchanged). This is the
general capability the water tag rides; it is reusable beyond water. The engine fills the one
measured param — it writes the camera world position into set-5 slot p9 (byte 144) each
frame, so fresnel/refraction/horizon track a moving camera with no per-frame re-upload — while the
artist owns the look (colours, waves, foam). Slot p9 is therefore engine-reserved for
camera position on scene-drawn custom-material nodes.
Real Perlin foam, auto-bound (engine PLM-275). To make loader-created water match the
immediate-mode toon/pbr variants 1:1, instantiateWater loads the
pack’s two foam maps — textures/water_noise.png (Roystan _SurfaceNoise) and
textures/water_distortion.png (_SurfaceDistortion) — from the app’s mounted content once per
scene (the same trusted Texture.load path) and binds them to the water surface’s
set-5 texture slots 0/1, setting useFoamTex (p11.x). These are fixed convention paths,
not read from the untrusted .blend, so they add no new parse surface. If the app doesn’t ship
them, the shader’s procedural fbm foam stands as the fallback (useFoamTex stays 0) — which is
what loader water fell back to before PLM-275. So a plume3d_water-tagged surface (both the
tagged-mesh and box-volume paths) gets the crisp Perlin foam simply by dropping water_noise.png +
water_distortion.png into the app’s textures/ folder (generate them with
tools/testdata/make_water_foam_textures.py); apps/blend_water_demo ships both.
Auto-registration for underwater (cameraWaterState)
Section titled “Auto-registration for underwater (cameraWaterState)”Engine WTR2 #3 (ADR 0097). As it builds the water,
instantiateWater now also registers one primary WaterSurfaceState
— the active water body the underwater path reads — so
Graphics.cameraWaterState() and the underwater post
effect fire on authored .blend water with no manual Graphics.setWaterPlane
call. Before WTR2 #3 the loader built the surface but registered nothing, so authored water never
reported submersion.
The engine’s water body is a single global, so the loader registers exactly one, by an explicit policy:
- a bounded
plume3d_water_volumebox (registered viasetWaterVolume— its world XZ footprint + floorminY+ top-facesurfaceY) is preferred, so “underwater” means inside the box; - otherwise the largest-footprint flat plane (registered via
setWaterPlane, an infinite surface belowsurfaceY).
The underwater fog comes from that body’s deep colour (density 0.16 by default), overridable by
the plume3d_water_fog = [r, g, b, density] prop. A game reads
cameraWaterState() each frame and cross-fades its own
underwater post on the reported amount — see the Blend Water Demo,
which dives the camera into an authored Realistic volume.
Buoyancy is still a separate, app-side path. The registered WaterSurfaceState drives submersion
detection + the underwater look, not floating bodies: Physics.applyBuoyancy reads
a Wren Water (its WaveParams), which the game builds with a
Water.setWaves(...) matching the authored plume3d_water_waves (p3). Loader-owned buoyancy is a
noted follow-up (WTR2 #4).
Box/volume water (plume3d_water_volume)
Section titled “Box/volume water (plume3d_water_volume)”A box tagged plume3d_water and plume3d_water_volume (a truthy int, or any
non-empty string — detected exactly like plume3d_terrain) flips this method from “draw this
mesh” to “generate the surface from this box”: loadBlendScene tessellates a level water
plane at the box’s world top face and gives it the same stylized-water shader + look props — so a
level designer drops a box to define a pool or lake without modelling a subdivided water plane and
without hand-placing it at the right height (engine PLM-275, ADR 0093).
It is folded into the same instantiateWater — no new Wren method or signature; and the box
is never also drawn as a solid prop (instantiateUnits skips every
plume3d_water node).
The generated surface is a horizontal plane spanning the box’s world-XZ extent, centered on it, at
the box’s max-Y (top face) — level for any box rotation, exact for the axis-aligned / yaw-only
common case. It carries the same shader-name and plume3d_water_shallow / _deep / _foam /
_waves / _horizon look overrides as the mesh path, plus two optional volume-only props:
| Custom property | Type | Meaning |
|---|---|---|
plume3d_water_volume | int / string | The marker — a truthy int or non-empty string switches the box to generated-surface mode. |
plume3d_water_subdiv | int, or [subX, subZ] | Overrides the auto tessellation (default ~2 quads / world-unit). Hard-clamped to 1..256 per axis (the memory bound — a giant or hostile box cannot inflate the mesh). |
plume3d_water_level | float | Absolute world-Y waterline (defaults to the box top), for a partial fill. |
Every attacker-controlled value (box verts, node transform, world corners, plume3d_water_level) is
finite-guarded; a non-finite or zero-area box is skipped with a warning (ADR 0093 §Security).
WaterScene.blend (a tagged mesh) and the Toon WaterVolume.blend (a tagged box) are kept for
the loader tests, while apps/blend_water_demo now loads Models/WaterRealistic.blend — a
plume3d_water="realistic" box volume it dives the camera into (WTR2 #3, ADR 0097).
Most callers reach this method through
Scene.loadBlendScene, which composes it after terrain and
before instantiateUnits — and instantiateUnits skips
plume3d_water meshes, so the two never double-draw the same surface.
See the Blend Water Demo example (apps/blend_water_demo).
instantiateUnits(scene)
Section titled “instantiateUnits(scene)”Returns: Num — The number of static mesh props instantiated as render nodes.
Parameters:
scene(Scene) — Scene to add the props to.
Instantiates every remaining static mesh prop — the rocks, fences, buildings, and loose geometry that sit on the level but are neither terrain nor scatter and carry no script (engine PLM-253, Block BLD, ADR 0088). Each becomes a render node with a shared GPU mesh (the loader de-duplicates identical mesh datablocks). It deliberately skips, so the four instantiate calls partition the blend with no double-draw:
plume3d_terrainmeshes — owned byinstantiateTerrain.plume3d_watermeshes — owned byinstantiateWater.- the scatter prototype object (by
node_id, not by mesh datablock, so a hand-placed copy that merely shares the prototype’s mesh still renders as a real prop) — owned byinstantiateScatter. hide_renderobjects — authored invisible.- objects inside a
class_name-scripted subtree — those stay explicit viainstantiate(className, scene), because they carry scripts/actions/drivers the caller wires up.
Props are placed flat by their stored world transform. Most callers reach this method through
Scene.loadBlendScene, which composes it with terrain and
scatter to load a whole environment in one call.
var blend = Resource.loadBlend("Models/FoliageTerrain.blend")var props = blend.instantiateUnits(_scene) // the static rock/fence/building propsSystem.print("instantiateUnits placed %(props) prop(s)")See the Foliage + Terrain Demo example (its two static rock
props are the instantiateUnits witness).
Blender-authored colliders (plume3d_collider)
Section titled “Blender-authored colliders (plume3d_collider)”Any object instantiateUnits places also gets a static Jolt collider on import if it carries a
plume3d_collider custom property — so a tree, rock, fence, or prop blocks movement with no
script (engine PLM-267, ADR 0090). This generalizes the
instantiateTerrain auto-collider to any object, configurable per
object. Three flat ID custom properties control it (written by the addon’s Physics collider
field, or set by hand / a script — the wire format is identical):
| Custom property | Type | Meaning |
|---|---|---|
plume3d_collider | String | Collider shape: none (default / absent — no collider) · box · sphere · capsule · convex · mesh. |
plume3d_collider_layer | Int | Gameplay collision-layer bitmask (the query filter used by spatial queries). Absent / ≤ 0 → the default layer (bit 0). |
plume3d_collider_static | Int | 1 (default) = static body · 0 = kinematic. mesh is always static (Jolt triangle meshes cannot move). |
- Primitives (
box/sphere/capsule) are sized from the object’s mesh bounds with the node’s scale baked into the shape, so one prototype scaled across a scene gets correctly-sized colliders. Capsules are Y-aligned (radius from XZ, half-height from Y). convexbuilds a convex hull of the object geometry;meshuses the exact triangle geometry (concave, static only) — the same pathinstantiateTerrainuses for tagged ground.- The scene’s physics world is created lazily (only if some object is tagged). A malformed tag is inert: an unknown shape string or a degenerate mesh builds no collider — the object still renders.
An untagged .blend is unchanged: the collider is opt-in and additive, and no Wren signature
changes (this is loader-side behaviour on the existing instantiateUnits method — reached in one
call via Scene.loadBlendScene).
The Foliage + Terrain Demo witnesses it: its two rock props carry
plume3d_collider (rock_a = sphere, rock_b = box) and a dropped probe rests on each rock.