Skip to content

Documentation Status

This page is a living record of documentation completeness against the Plume3D engine source. Update this file whenever the engine gains new features or doc pages are added/updated.

Engine version at last review: 2026.1.1 — components; Engine.Version() returns the string 202601.001 (YYYYMM.PPP, version.h)
Engine source: Plume3d/Source/Plume3d
Last reviewed: 2026-08-24 (delta — NEW page /where-plume3d-fits — the engine maturity ledger. A public, cited capability comparison against Godot 4.7.2, Unity 6.5 and Unreal 5.8 over 57 capabilities in 10 areas, designed to be re-read on a schedule so readers can watch the gap close. Architecture: the page is generated and committed, not rendered — scripts/build-maturity-page.mjs reads src/data/maturity/capabilities.json (a stable id catalogue) plus append-only rev-YYYY-MM-DD.json readings and writes the .md. This keeps the site’s zero-component, zero-client-JS convention intact, puts the exact published markup in the commit diff (deploy is owner-gated, so the page must be right at commit time), and avoids the trap that markdown links inside a raw HTML block are not processed by this remark pipeline — every one of the ~171 competitor citations is emitted as a raw <a href>. New src/styles/maturity.css (second customCss entry; Starlight’s -low/-high colour pairs flip between themes, so the state pills need no light-mode override). npm run maturity:check added to CI between docs:check and build. Editorial: no numeric score for any engine — a 0–10 composite reads as a statement of fact rather than opinion and is unverifiable under Directive 2006/114/EC, while a per-cell state + vendor citation is verifiable on its face; competitor claims come from published docs only (Unity’s ToS restricts competitive analysis by name); trademarks are text-only with an attribution footnote; the page carries a bias disclosure in its first sentence, a “Do not use Plume3D if…” section naming the alternative, and a corrections invitation. Verification mattered: an adversarial pass over the competitor columns applied 22 corrections across the three engines (a stale Unreal vehicles cell, an invented Unity tool count, Godot cells overstating first-party XR scope and misdating an IK solver) and re-pinned 2 Godot citations off /en/stable/. Baseline: Plume3D ships 19/57; Godot 42, Unity 49, Unreal 54. Next review due 2027-02-24. No new Wren class or example app → check_docs_coverage unaffected (still 93 classes / 126 apps). Engine Hyperscale/projects/plume3d.md, website tickets/PWEB-014, HUMAN-031 (owner sign-off on naming competitors publicly).)

Prior: 2026-08-17 (delta — blend.instantiate now WARNs on an unresolved scripted class (engine PLM-302) — COMPLETE. Follow-up to PLM-301: when a node carries a Blender-assigned class whose script the game hasn’t imported, instantiate still returns a plain Node (the engine can’t import it for you — re-entrancy), but now logs a WARN naming the class and the exact import "…" for … line to add. The import string must match plume3d_script_path minus .wren case-sensitively (Scripts/x ≠ scripts/x; resolves on macOS but not Linux/Windows). A one-line gotcha added to the /api/node Entity section. No new Wren class / example app — a diagnostic on the already-documented instantiate/Entity surface → check_docs_coverage unaffected (still 93 classes / 125 apps); manual doc-sync. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine tickets/PLM-302-instantiate-unresolved-class-warning.md, Hyperscale/projects/plume3d.md.)

Prior: 2026-08-17 (delta — blend.instantiate returns the node’s Wren class instance (engine PLM-301, ADR 0115) — COMPLETE. Instantiating a node that carries a Blender-assigned Wren class (plume3d_class_name + plume3d_script_path) now returns that class’s instance, bound to the node, instead of a bare Node — so game code uses Node functions/values on it directly. Because a Wren class cannot inherit from the foreign Node, a NEW base class Entity wraps a Node and forwards its common API — extend it (class MyPlayer is Entity { construct new(node){super(node)} init(config,args){} }) and the instance reads like the node (player.getWorldPosition(), .setPosition(…), .node escape hatch). Resolution is auto from the node’s imported script (no registry); the class is newd in Wren (instantiate runs inside the game’s init/update wrenCall, so it can’t re-entrantly construct from C++), and init(config, args) receives the node’s plume3d_config_path TOML + the caller’s args (instantiate’s optional 3rd arg — a String is still the legacy animation prefix). Opt-in + backward-compatible: only a class that is Entity is instantiated — a node with no class, or any other imported class, still returns the plain Node, so existing content is unaffected (confirmed 716/716 ctest incl. the blend/combat instantiate tests; no app imports a class matching a class_name). /api/resource Updated (the new return + args 3rd form) and NEW Entity section on /api/node. New demo apps/blend_class_instance (class Thing is Entity + a headless --test) — Example-pages row below. Landing Scripting card notes Entity. Security: no new untrusted-input parse (the class is resolved from the game’s own imported Wren; the config path was already a parsed-TOML surface) → advisory /security-review, no CVE change. +1 Wren class → 93 classes; +1 app (blend_class_instance) → 125 apps. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0115-blend-instantiate-returns-class-instance.md, Hyperscale/projects/plume3d.md, PLM-301.)

Prior: 2026-08-16 (delta — Input — mouse capture (relative/locked mode) + cursor visibility (engine ADR 0114) — COMPLETE. The Input class gains cursor control so FPS/orbit cameras can capture the mouse: Input.setRelativeMouse(v)/relativeMouse() (hide + lock-to-center + relative deltas), Input.setCursorVisible(v)/cursorVisible() (OS cursor visibility when not captured), and Input.mouseDeltaX()/mouseDeltaY() (per-frame relative motion). The built-in cameras (Cam.firstPerson/freeFly/orbit) now drive look from that relative delta, so they never stall at the window edge; the engine auto-releases capture on window focus loss (alt-tab) and restores it on return. Additive — no existing signature changed; the game wires its own release key (e.g. Esc). Landing Input card updated on index.mdx to surface mouse capture (a new user-visible capability within the existing input subsystem). No new Wren class / example app — methods added to the already-documented Input class, and camera_showcase (already an app) demonstrates it → check_docs_coverage unaffected (still 124 apps / 92 classes); manual doc-sync. No new untrusted-input parse (SDL delivers the motion; the flags drive SDL window state) → advisory /security-review, no CVE surface change. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0114-input-cursor-control.md, docs/superpowers/specs/2026-08-16-input-mouse-capture-design.md, Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-13 (delta — Bugfix — an InstancedMesh’s per-mesh shader is now honoured (engine PLM-264) — COMPLETE. An InstancedMesh whose prototype mesh carried its own per-mesh shader (Graphics.newMesh(verts, topo, shader)) rendered all instances at the origin: the Scene.draw() instanced-draw command builder never set cmd.shaderName, so the renderer fell back to the active shader; when that active shader is non-instancing it applies an identity transform → origin collapse. Fixed by propagating the prototype’s per-mesh shader into the instanced draw (2 lines; no renderer change, no new public API, no ADR — the 3-arg newMesh already existed and was documented). Website: /examples/scatter-multi rewritten — it now also guards PLM-264 (two instanced fields with different per-mesh shaders, orange left / cyan right, over a non-instancing active shader); /examples/weather-demo stale limitation corrected (it documented PLM-264 as an open bug with the wrong SSBO root cause and “when PLM-264 is fixed…” — now: fixed, the shared-shader + mode param is a choice, not a workaround). No new Wren class / example app (a bugfix on the already-documented 3-arg newMesh) → check_docs_coverage unaffected (still 124/92); manual doc-sync. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine tickets/PLM-264-… (→ done), Hyperscale/projects/plume3d.md.)
Prior: 2026-08-13 (delta — Water pack follow-ups — per-bend river flow + the buoyant masked boat (engine PLM-273, ADR 0113) — COMPLETE. Two water-pack follow-ups landed as content on a small additive seam. (1) Per-bend river flow (ADR 0113): a spline river’s foam scrolled along one global wind vector, so it drifted sideways across the channel at bends. MeshGen.ribbon gains an optional 5th flow argument (a new additive Wren signature — the 4-arg form is unchanged): flow=true bakes each station’s downstream tangent into the ribbon’s vertex colour, and the shared Toon water shader scrolls its whitewater along that local bend (falling back to global wind where no flow is baked). Device-free geometry (MeshData.tangents + ExtrudeOpts.emitFlow), a vertex-colour carrier (no vertex-format change; lit ribbons that tint by colour, e.g. spline_mesh_demo, use the 4-arg form and are unaffected), and a plume3d_foamMask scroll-override overload. /examples/river-demo rewritten (was “flow is global, per-bend not yet shipped” → now the shipped per-bend flow). (2) Buoyant masked boat (PLM-273): /examples/water-mask-demo rewritten — its open-hull boat is now buoyant (one Jolt body + Physics.applyBuoyancy, four hull-corner probes → it rocks), the mask footprint moved to a free set-5 slot (p6) so it coexists with the Toon colour params, and the game rewrites p6 from the boat body’s world XZ each frame so the discard tracks the floating hull. Also corrected the river page’s stale “masked boat not yet shipped” note. No new Wren class, no new example app — the flow parameter is additive on the existing MeshGen and both demos already existed → check_docs_coverage unaffected (still 124 apps / 92 classes); a new optional param + demo rewrites are not gate-flagged, so this is manual doc-sync. Landing Procedural Meshes card notes the ribbon flow option (an enhancement of the already-carded ribbon, no new card). No new untrusted-input parse (spline tangents are math over app-authored curves; buoyancy reads a Wren Water) → advisory /security-review, no CVE surface change. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0113-per-bend-river-flow.md, tickets/PLM-273-…, Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-11 (delta — Signing /signing — build provenance (reproducible builds + signed SLSA attestation, engine PLM-296) — COMPLETE. Official releases now ship a build-provenance attestation beside the binaries: the unsigned shipping binary is byte-reproducible (rebuild from source → identical bytes; ADR 0112), and the release CI emits an in-toto SLSA v1 statement (source commit + each artifact’s SHA-256) GPG-signed with the release key and attached to the Gitea Release. Keyed, not keyless — the CI is self-hosted Gitea, not a Sigstore-Fulcio-trusted OIDC issuer. The /signing page gains a Verifying a release’s provenance section (gpg --verify the .asc + match the artifact shasum to the statement’s subject.digest.sha256). No new Wren class / example app (a release-artifact + CI capability) → not covered by check_docs_coverage.py (still 124/92); manual doc-sync. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0112-reproducible-shipping-builds.md, scripts/gen_provenance.py + verify_reproducible.sh, .gitea/workflows/release.yml, tickets/PLM-296-… + HUMAN-030-…, Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-11 (delta — Packaging /packaging — --compile-shaders + package.toml entry across run modes (engine PLM-293 items 2–3) — COMPLETE. Two follow-ups from the packaging MVP landed. (1) plume3d-pack gains --compile-shaders (and --shader-include <dir>): it runs slangc on the staged shaders/src/*.slang → shaders/bin/*.spv before bundling, so a game can be packaged straight from shader sources. slangc is resolved from $VULKAN_SDK/common install paths/PATH (the tool never downloads a toolchain), and engine/shaders/include is auto-detected for the -I ABI-header gotcha. (2) a .p3d’s package.toml entry is now honored on --server and --check too (not just the windowed run) — a dedicated-server bundle with a non-main.wren entry serves, and --check <bundle> lints, identically. Website: /packaging options table gains the two flags, a new Compiling shaders during packaging subsection, and the entry line notes all three run modes. No new Wren class / example app (host-CLI flags + a runtime behavior refinement, not a Wren surface) → not covered by check_docs_coverage.py (still 124 apps / 92 classes); manual doc-sync. No new untrusted-input parse (shaders compile from the developer’s own sources; entry stays sanitized by the existing ParsePackageToml) → advisory /security-review, not a website concern. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0110-p3d-app-package-and-plume3d-pack.md (follow-ups resolved), tickets/PLM-293-…, Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-11 (delta — NEW /signing page — code-sign your game for distribution (engine PLM-286/294, HUMAN-027) — COMPLETE. plume3d-pack’s --sign hook lets a developer OS-sign their packaged game with their OWN credentials: macOS Developer ID + --notarize (codesign inside-out → notarytool → staple; proven end-to-end — spctl = Notarized Developer ID), Windows Authenticode via Azure Artifact Signing (the cross-platform dotnet sign tool), Linux GPG (SHA256SUMS + .asc). New /signing page documents the per-platform prerequisites + the --codesign-identity/--notarize/--notary-profile, --azure-endpoint/-account/-cert-profile, and --gpg-key flags, with the honest “provenance + tamper-evidence, not encryption” framing (distinct from the .p3d --dev-key integrity). Sidebar entry after Packaging; /packaging cross-links it. No new Wren class / example app (a host-CLI capability) → not covered by check_docs_coverage.py; manual doc-sync. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/signing-setup.md, tickets/HUMAN-027-…, Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-10 (delta — Packaging /packaging — package-integrity signing (engine PLM-294, ADR 0111) — COMPLETE. Engine PLM-294 adds a developer-certificate PKI: a signed .p3d is verified by the runtime on every launch and refused if the payload was modified (a player editing the packaged scripts/models/maps → the game fails to load). It is the paid licensing tier (the engine is free; an unsigned .p3d runs without verification — the free tier). A licensed dev gets a WyldMagic-signed developer certificate; plume3d-pack --dev-key/--dev-cert embeds a signed chain (root → developer.cert → per-file integrity.manifest). The /packaging page gains a Package integrity (signed builds) section (the --dev-key/--dev-cert flags + the honest “tamper-evidence, not encryption” framing), and its “not covered yet” is corrected: the PLM-293 macOS OpenSSL-dylib gap is resolved (static-linked, self-contained .app), leaving OS code signing/notarization (HUMAN-027) as the follow-up. New untrusted parse (the cert/manifest chain from a mounted .p3d) → the engine ran a blocking /security-review (PASS, no exploitable HIGH/MEDIUM). No new Wren class, no new example app (an engine capability + a host-CLI flag, not a Wren surface) → not covered by check_docs_coverage.py; manual doc-sync. Committed on the Website repo (deploy owner-gated — NOT pushed/deployed). Engine docs/adr/0111-package-integrity-developer-certificate-pki.md (PLM-294), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-10 (delta — Packaging & distribution /packaging — the .p3d app package + plume3d-pack tool + desktop shells (engine PLM-286, ADR 0110) — COMPLETE. Engine PLM-286 builds the end-to-end “bundle an app for distribution” flow: a canonical .p3d package (a zip of the cleaned app dir + a package.toml manifest at its root, schema frozen by ADR 0110, entry finally honored) produced by a new plume3d-pack host tool that wraps it in a native shell around the shipping runtime — a macOS .app, a Windows portable .zip, or a Linux .tar.gz. The runtime auto-loads a bundled .p3d sitting beside the executable, so a packaged app just runs. New /packaging page (this delta) — the .p3d format, the plume3d-pack CLI + options table, the package.toml schema, and the follow-up gaps (installers, signing, mobile/web); sidebar entry added after Command line; landing What’s in place grid gains a Packaging & distribution card (a genuinely new user-visible capability, unlike the PLM-285 CLI refinement which added none). package.toml is a NEW untrusted-input parse (a mounted .p3d is the engine’s prime threat surface) → the engine ran a blocking /security-review (PASS, no HIGH/MEDIUM): an entry that is absolute or contains .. is rejected to main.wren, the id charset is restricted, window dims clamped, and PhysFS independently sandboxes mounted reads. No new Wren class, no new example app — plume3d-pack is a host CLI and package.toml a config schema, not a Wren API surface — so (like the /cli pages) this page is not covered by the engine’s check_docs_coverage.py gate (apps/Wren-class only, unchanged); its presence is manual doc-sync discipline per the engine CLAUDE.md. Committed on the Website repo (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine docs/adr/0110-p3d-app-package-and-plume3d-pack.md (PLM-286), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-10 (delta — CLI reference /cli — per-variant flag set (development vs shipping) noted (engine PLM-285, ADR 0109) — COMPLETE. Engine PLM-285 adds a PLUME_VARIANT build split: the current CLI is the development runtime (init, --tools, --tool, --test/--headless, --check, hot-reload all present); the shipping runtime (built -DPLUME_VARIANT=shipping — the binary a developer packages with a game) compiles that dev-authoring/testing tooling out, so its --help shows only bare-run, --server, and --help. This is a real change to the runtime’s CLI contract (some flags legitimately cease to exist in shipping), so the /cli page gains a Build variants — development vs shipping section stating which commands are development-only. --server (dedicated servers ship), the crash reporter, and error reporting are kept; both variants are Release-optimized; Debug is never shipped. No new app, no new Wren class — the CLI is a host-process command line, not a Wren API surface, so (like the ADR 0107 /cli page) this is not covered by the engine’s check_docs_coverage.py gate (still 124 apps / 92 Wren classes, unchanged); its presence is manual doc-sync discipline per the engine CLAUDE.md. No new untrusted-input parsing → advisory /security-review on the engine side, not a website concern. No landing card (a build-config refinement of the existing CLI, not a new user-visible capability). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine docs/adr/0109-dev-production-build-variants.md (PLM-285), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-09 (delta — CLI reference page — plume3d --help / --tools / init (engine PLM-284, ADR 0107) — COMPLETE. The engine’s plume3d runtime command line (hand-rolled in app_host.cpp) gained three additive commands with no documentation surface until now: --help/-h (full usage screen — previously an unrecognized flag like --help fell through to the positional-argument branch and the engine tried to mount a folder literally named --help); --tools (bare, no value — lists the four built-in editor tools, ui_editor / node_editor / bt_editor / toml_editor, with a one-line description each and a --help footer, plus the launch form plume3d --tool <tool-app> <target-app-dir>); and init [<dir>] (scaffolds a runnable project: a comprehensive commented game.toml with default [Physics] tag/collision layers, a rendered-triangle main.wren, README.md, and the shader — shaders/src/triangle.slang + a precompiled shaders/bin/triangle.spv, since the engine cannot compile .slang at load time — with -i/--interactive prompting for name/version/description/window size/author/terminal and --force allowing overwrite; init --help shows init-specific usage). All three are additive — no existing flag’s spelling or behavior changed. Website: NEW /cli page (this page) — Run a game, init, --tools, and the headless modes (--test/--server/--check) as one CLI reference; cross-linked from Getting started right after the “Run a game” block; sidebar entry added (this site’s sidebar is an explicit list, not autogenerated — a new entry Command line → cli was added after “Testing (Headless)”); landing What’s in place grid gains a Command-line tooling card. No new app, no new Wren class — the engine gate reports 122 apps, 92 Wren classes, unchanged by this page (the CLI is a host-process command line, not a Wren API surface) — so unlike most syncs this page is not covered by the engine’s check_docs_coverage.py gate (it checks apps/* and Wren classes only, not CLI commands); its presence here is manual discipline per the engine CLAUDE.md’s doc-sync mandate, not gate-enforced. No new untrusted-input parsing surface (the init scaffolder’s generated game.toml round-trips through the engine’s existing, already-reviewed config_toml parser) → advisory /security-review on the engine side, not a website concern. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine docs/adr/0107-cli-help-tools-init.md (PLM-284), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-09 (delta — .blend cameras drive the rig system — Cam.fromNode + cameras.toml type="static" + apps/blend_camera_demo (engine ADR 0106) — COMPLETE. A camera authored in Blender is parsed into a passive Scene.Camera (lens/near/far + its SceneNode) but nothing drove it — games wrapped it by hand (audio_player_3d’s FPSCamera), and it could not be handed to the CameraDirector/rig system every code-created rig uses. ADR 0106 unifies them. NEW public Wren method Cam.fromNode(scene, node) — additive on the EXISTING Cam class (no new class, no existing signature changed): builds a static rig pinned to the node’s authored world pose (position + facing; no follow/damping/look) and, if the node carries a Scene.Camera (a .blend-instantiated camera), adopts its lens (blend focal length → fovY, near, far); returns an ordinary CameraRig (Cam.setActive/Cam.blendTo as usual). cameras.toml type="static" is now a REAL fixed rig — was a v1 stub aliased to thirdPerson (ADR 0104): pinned to its target node’s pose, adopting a .blend camera’s lens when the target is one and no explicit fov/near/far overrides it — the data-authored twin of Cam.fromNode. Under the hood: a rigStatic() preset (BodyKind::HardLock + AimKind::SameAsFollow); the director now feeds targetRot (node world rotation via the additive C++ SceneNode::getWorldQuat) so the static aim reproduces the authored facing, and Scene::findCameraByNode (additive C++) finds the passive camera whose lens the rig adopts. Additive throughout → no breakage for the Wren-only consuming games (LowTide/Lexicon). NEW apps/blend_camera_demo (loads Models/Radio_V3.blend, instantiates the geometry + the GameCamera, Cam.fromNode(scene, cam.node) → Cam.setActive, renders the model from the authored 3/4 pose) landed, so engine check_docs_coverage was failing (“1 app(s) absent from doc-status.md: apps/blend_camera_demo”) until the site carried it. Website: api/camera.md — Cam.fromNode(scene, node) added to the Cam factory list (alongside thirdPerson/firstPerson/topDown/orbit/freeFly), and the cameras-TOML type="static" note rewritten from “v1 stub → thirdPerson” to the real pinned-pose + lens-adoption behaviour; NEW examples/blend-camera-demo.md + its doc-status examples row + a curated /examples Camera-section row; landing Camera card EXTENDED (not a new card) to note .blend-authored cameras driving the rig via Cam.fromNode / type="static". +1 app → 124 apps, still 92 Wren classes (Cam is not a new class — fromNode is one additive method; every engine app carries a doc-status row again, check_docs_coverage app check green). No new untrusted-input parsing (the loader already parses BlendCameraData; this only consumes it) → advisory /security-review, not blocking; no CVE surface change (no vendored-submodule bump). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine docs/adr/0106-blend-cameras-drive-rig-system.md (PLM-283 item 3), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-09 (delta — Per-viewport SDF text — Graphics.perViewportOverlays now covers text + apps/split_text_demo (engine ADR 0105) — COMPLETE. The Camera v2 per-viewport HUD (CAM-18, ADR 0064) made Graphics.perViewportOverlays(true) replay the retained-UI HUD into each split-screen cell; SDF text was still drawn once, full-screen, against the main camera — so in a split-screen game a world-space nameplate landed at the main camera’s screen spot, not each player’s, and screen-space HUD text was not scaled into the cells. ADR 0105 extends the SAME flag to SDF text: record_text_draws loops the viewports and, per cell, sets the sub-rect viewport/scissor and re-accumulates the labels — screen-space labels use the full-window ortho (scaled into the cell, like the HUD) and world-space labels are drawn against that viewport’s view/projection (re-projected per camera). Model-space glyph geometry is identical across cells; only the baked MVP push-constant differs, so re-accumulating per cell overwrites the shared text buffer with byte-identical data each pass. No new Wren API and no signature change (the only touched signature is an internal backend method, additively/defaulted) — the same perViewportOverlays(on) flag, so NO frozen-contract change and no consuming-game coordination. Flag off / no viewports → one full-screen draw against the main camera, byte-identical to the legacy path. NEW apps/split_text_demo (a 2-player horizontal split — each cell shows only its own player’s world nameplate (“ONE”/“TWO”) plus a screen-space banner in both cells) landed, so engine check_docs_coverage was failing (“1 app(s) absent from doc-status.md: apps/split_text_demo”) until the site carried it. Website: api/graphics.md — the perViewportOverlays section rewritten to say the one flag now covers HUD + SDF text (world-space re-projected per camera + screen-space scaled per cell, ADR 0105); api/camera.md — the SplitScreen Scope (v1) note updated (overlays are per-viewport under the flag, not “once full-screen”); examples/split-text-demo.md NEW + its doc-status row + a curated /examples Camera-section row; landing Camera card EXTENDED (not a new card — per-viewport text is an extension of the existing per-viewport HUD) to note overlays (HUD + SDF text) replay per cell. +1 app → 123 apps, still 92 classes (no new Wren class). No new untrusted-input surface (no vendored-submodule change) → no CVE scan; /security-review advisory, not blocking (the change is an internal render-path replay of already-trusted, game-authored text). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine ADR 0105 (PLM-283 camera-authoring item 2), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-09 (delta — Camera assets from TOML — Cam.loadAssets / Cam.rig + apps/camera_toml_demo (engine feat/camera-toml-assets, commit a51217d, ADR 0104) — COMPLETE. The Camera epic (ADR 0058) shipped the rig system code-only and deferred “author rigs as data”; this lands it. A game declares its cameras in a cameras TOML and loads them in one call — the same way game.toml declares physics layers or a channels TOML declares audio. Two new frozen Wren methods on the EXISTING Cam class (additive; no new class, no existing signature changed): Cam.loadAssets(scene, path) reads a cameras TOML and, for each [[rig]], picks the preset by type (thirdPerson/firstPerson/topDown/orbit/freeFly/static), applies the overrides using the same field mapping as the CameraRig fluent setters, resolves target by SceneNode id, registers the rig’s name, and marks the active rig live — plus an optional [splitscreen] table that builds a SplitScreen layout — returning the rig count (parsing never throws; malformed → partial/empty); Cam.rig(scene, name) looks a rig up by its TOML name (e.g. to Cam.blendTo it). The method names + the cameras-TOML key names are a frozen contract (ADR 0104), the keys mirroring the fluent setters. NEW apps/camera_toml_demo (a follow third-person rig (active) + an overview orbit rig from one Cam.loadAssets, then blends to the named overview rig — the data-authored twin of camera_showcase) landed, so engine check_docs_coverage was failing (“1 app(s) absent from doc-status.md: apps/camera_toml_demo”) until the site carried it. Website: api/camera.md gains a Camera assets from TOML section (both methods + the full cameras-TOML schema + a Wren snippet); examples/camera-toml-demo.md NEW + its doc-status row + a curated /examples Camera-section row; landing Camera card EXTENDED (not a new card) to note TOML-authored rigs. +1 app → 122 apps, still 92 classes (Cam is not a new class — two additive methods). Untrusted-input surface is toml++ (already the config surface) with a robust never-throw parser → advisory /security-review, not blocking; no new CVE surface (no vendored-submodule change). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine feat/camera-toml-assets (commits b462a36/a51217d/ac3e309, ADR 0104), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-08 (delta — Particle sprite render modes + apps/particle_fx_demo (engine main, commit 99061b9, ADR 0102) — COMPLETE. Water splashes/ripples now ride the built-in particle system: two new non-physics ParticleEmitter render modes that reuse the existing sprite pipeline (PLM-009), so there is NO Vulkan renderer change. (1) SpriteGround (mode 1) — a soft ring texture laid FLAT on the world XZ plane (a new BillboardMode::Ground); bursting one per beat gives concentric, expanding, fading rings = a raindrop / footfall / floating object hitting water. (2) SpriteBillboard (mode 2) — the same ring, camera-facing, as a twinkling sparkle above the surface. PhysicsMesh (mode 0) is the default and unchanged (Jolt body + instanced mesh; splash SPRAY stays this mode). Sprite particles are PINNED kinematic decals — no Jolt body, they stay where they spawn and animate ONLY via a scale-over-life + alpha-over-life curve. Additive Wren surface on the EXISTING ParticleEmitter class (no new class, no signature change to existing methods): setRenderMode(mode), setScaleCurve(startHalfSize, endHalfSize), setAlphaCurve(startAlpha, endAlpha), setColor(r,g,b), setTexture(texture) (a Texture from Texture.load). NEW apps/particle_fx_demo (dark water-like ground + a SpriteGround ripple emitter bursting rings at three sites + a SpriteBillboard sparkle; self-screenshots) landed, so engine check_docs_coverage was failing (“1 app(s) absent from doc-status.md: apps/particle_fx_demo”) until the site carried it. Website: api/particles.md gains a Sprite render modes section (the 5 methods + a mode table + a Wren snippet); examples/particle-fx-demo.md NEW + its doc-status row + a curated /examples Physics-section row; NEW landing card “Water FX particles — ripples & sparkle” on index.mdx (the sprite-particle FX path was not carded before). +1 app → 121 apps, still 92 classes (ParticleEmitter is not a new class). No untrusted-input surface — the sprite modes take plain numbers + an already-loadable Texture (no /security-review / CVE). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit 99061b9, ADR 0102), Hyperscale/projects/plume3d-web.md.)
Prior: 2026-08-08 (delta — Stacked N-effect post-process chain + apps/post_stack_demo (engine main, ADR 0101) — COMPLETE. The engine’s full-screen post path ran only the FIRST addPostEffect; a second was silently dropped. Now all N effects run as a chain — effect i’s output feeds effect i+1’s input, and the last writes the swapchain (up to 8) — so apps compose independent passes (underwater + rain + colour-grade) instead of one mega-shader. Internal to the Vulkan renderer: intermediate stages ping-pong through two scratch colour targets (each with an explicit PLM-224 ATTACHMENT→SHADER_READ barrier), the final stage draws into the swapchain; kMaxPostEffects pre-wired descriptor sets (binding0 = the stage’s input image, binding4 = a per-stage params slice) + two pipeline variants (final/depth vs intermediate/no-depth). N==1 is byte-identical to the old single-effect path. No Wren signature change — addPostEffect(_,_) is the same; only the SEMANTICS change (1→N), so a previously-dropped second effect now runs (LowTide/Lexicon-safe — none stack effects today). NEW apps/post_stack_demo (tint → vignette → desaturate → scanlines, 4 effects) landed, so check_docs_coverage was failing until the site carried it. Website: api/graphics.md — the Post-process section gains a Stacking subsection; examples/post-stack-demo.md NEW + its doc-status row. No new landing card — an enhancement of the already-carded post-effect path. +1 app → 120 apps, still 92 classes. No untrusted-input surface (no /security-review / CVE). Adversarially reviewed (barriers / leaks / MoltenVK / N-edge-cases). Engine main (ADR 0101), Hyperscale/projects/plume3d.md.)
Prior: 2026-08-08 (delta — New example app apps/water_blender — Blender-authored water on a HAND-authored .blend (engine main) — COMPLETE. A NEW apps/* landed, so the engine check_docs_coverage gate was failing (“1 app(s) absent from doc-status.md: apps/water_blender”) until the site carried it — this sync makes it green. A scene modelled in Blender (Island + Seabed + a Water plane) renders its water purely from tags on the mesh: the Water plane tagged plume3d_water="realistic" (+ a shallow/deep/foam/waves/horizon/caustic/crest param block) is drawn by Scene.loadBlendScene → instantiateWater as engine water around the island (waves, depth colour, a shoreline foam ring, the seabed showing through via depth/refraction), the Seabed tagged plume3d_terrain (+auto collider), the Island an ordinary lit mesh — no water wiring in the Wren script. The surface companion to blend-water-demo (which authors a box VOLUME). Authoring gotcha the demo pins down: a mesh tagged plume3d_water must be subdivided for the realistic shader’s vertex wave displacement (this Water is a ~140×140 grid; a single quad renders flat) — a plume3d_water_volume box instead auto-generates a subdivided surface. A DEMO of the existing authored-water loader (PLM-274 / ADR 0092) — NOT a new engine API (no ADR / /security-review / CVE / new Wren class). Website: /examples/water-blender NEW + a curated /examples Blender-section row. No new landing card — a demo of the already-carded plume3d_water tag, matching blend-water-demo. +1 app → 119 apps, still 92 classes. Render-verified (MoltenVK): log instantiateWater(...): 1 water surface(s) built … registered PLANE for underwater; orbit screenshots. Also this session (engine main, docs on existing pages): PLM-282 — the MSAA one-pass offscreen path now materialises the resolve, so per-frame atmosphere no longer greys/tears at 4× MSAA (unblocked Weather’s per-frame cloud-scroll); and the water apps → shared TimeOfDay + Weather packs migration (both apps stopped reimplementing the packs inline; see the updated /examples/water-raymarch + /examples/toon-water-demo rows — Weather’s 4-state Clear/Rain/Snow/Storm, precip bundled). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit 29be39b), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-08 (delta — Full-orientation buoyancy + water feedback fixes (engine main, ADR 0100) — COMPLETE. Owner feedback on water_raymarch: floaters spun wildly / only bobbed (no alignment). Root cause = an ENGINE bug in Physics.applyBuoyancy — it applied each probe’s up-force at a LOCAL offset that was never rotated by the body’s orientation (a documented “full-orientation deferred” v1 limitation), so as a body tilted the force points stopped tracking its corners → the torque never restored → runaway spin + no alignment. Fixed (ADR 0100): a new internal PhysicsWorld::getRotationQuat + quaternion-rotating the probe offsets so each up-force acts at the body’s ACTUAL tilted corner → restoring torque → stable rocking that ALIGNS to the wave slope. No public signature changed (a behavior correction of the frozen Physics.applyBuoyancy, ADR 0091 — strict improvement; LowTide/Lexicon-safe). Also app-side tuning (water_raymarch + toon_water_demo): the seascape water now reflects the weather/time-of-day sky (feeds the sky-horizon into the surface p10 + a fresnel floor), stronger depth-tint on submerged objects, and a much shorter object wake; barrels given 4 rim probes (were 1 centre = bob-only). Website: /api/physics applyBuoyancy Updated — the offsets mechanism corrected to full-orientation (force at the rotated corner → restoring torque → alignment; the “≥3 non-collinear probes for alignment” guidance). No new app/class → still 118 apps, 92 classes. Gates green: engine check_docs_coverage ✔ 118 apps; website docs:check + build ✔. Committed on the Website repo main (deploy owner-gated). Engine branch feat/water-feedback-fixes (ADR 0100), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-08 (delta — New example app apps/toon_water_demo — the owner’s ray-marched TOON water ported onto a real mesh (engine main) — COMPLETE. A companion to apps/water_raymarch: where that renders the reference water’s realistic look, this renders its toon look. A NEW apps/* landed, so the engine check_docs_coverage gate was failing (“1 app(s) absent from doc-status.md: apps/toon_water_demo”) until the site carried it — this sync makes it green. shaders/water_toon.slang is a faithful mesh port of the owner’s renderToonWater: the same Alekseev octave-wave heightfield as the seascape variant (so buoyant floaters bob on the visible swell) with toon SHADING — a banded depth palette (toonBand), crisp cel foam (crispStep) at shorelines/crests, INK graphic marks on steep facets + a dark horizon stroke, banded sun light, and a toonSky rim reflection + sun-gold specular. The see-through is the real engine scene (seabed + physics floaters) via the grab-pass + scene depth, so crates/barrels sit in the water with a crisp foam ring. Reconstructed helpers (crispStep/toonBand/depthPalette) + the INK/FOAM/SUN_GOLD palette are the tunable knobs at the top of the shader (the reference did not paste them). A DEMO of existing capabilities — the set-5 custom material + grab-pass + scene depth — NOT a new engine API (no ADR / /security-review / CVE / new Wren class). Website: /examples/toon-water-demo NEW (what it is, the toon-shading breakdown, controls, the tunable-knobs note) + a curated /examples Rendering-section row. No new landing card — a demo of the already-carded water features (grab-pass/scene-depth/custom-material), matching the water_raymarch precedent. +1 app → 118 apps, still 92 classes. Clean-room reimplementation (no vendored third-party source). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (branch feat/toon-water-demo), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-07 (delta — apps/water_raymarch reworked into the Seascape interactive playground + Water.setSeascape (engine main, commits f192c84→84dd2c4) — COMPLETE. The ray-march example is now a full seascape water playground — the equivalent of water_playground built on the ray-march look rendered on a REAL mesh: the owner’s gameidea.org Seascape algorithm (Alekseev |sin|/|cos| octave waves — 3 for the vertex, 5 for the normal) on water_seascape.slang with Beer-Lambert depth colour, projected caustics, fbm-Voronoi + crest foam, and a procedural-sky reflection; a faithful underwater.slang port of the tutorial’s underwaterFragmentMain; a matching seascape Snell-window underside; a free-fly camera (dive above/below); runtime Seascape↔Toon + weather toggles; and dynamic buoyant crates/barrels. One additive engine surface: Water.setSeascape(v) (ADR 0099) — the CPU mirror of the seascape octaves so buoyancy tracks the rendered swell (the octave sum is centred on the plane by a −0.9·gain bias). Fixes shipped alongside: the underwater post is gated on the camera-vs-still-level (stable, smooth band) so it never over-paints the surface seen from above; the octave waves are centred so the surface crossing has no dead-band; and the demo no longer hitches on launch (the per-frame screenshot dump is behind an OFF-by-default _capture flag) or self-exits. Clean-room reimplementation (no vendored third-party source); no untrusted-input surface touched (no ADR / /security-review / CVE beyond the additive setSeascape, ADR 0099). Website: /examples/water-raymarch rewritten (seascape playground — mesh water, underwater, buoyancy, variant/weather), /api/water +setSeascape, the Water physics + Underwater landing cards updated, this row.)
Prior: 2026-08-07 (delta — New example app apps/water_raymarch — a ray-marched stylized water scene in one full-screen post (engine main, commit f192c84) — COMPLETE. A NEW apps/* landed, so the engine check_docs_coverage gate was failing (“1 app(s) absent from doc-status.md: apps/water_raymarch”) until the site carried it — this sync makes it green. The app ray-marches an entire stylized-water scene — seabed + sky + foamy wave crests + turquoise caustic shallows — inside ONE full-screen post shader (shaders/water.slang), with no scene geometry. The owner wrote the shader for the Slang Playground (a gameidea.org stylized-water adaptation) and ported it to the Plume3D post ABI: import playground/rendering→#include "plume3d_post.slang", [playground::TIME]→plume3d_time(), [playground::MOUSE_POSITION]→post param p0.xy (the app writes the normalized mouse each frame), resolution→the scene-colour texture’s GetDimensions, [shader("compute")]→[shader("fragment")]; V is flipped for the bottom-left post UV origin. A tiny throwaway trigger quad is drawn purely to satisfy the engine’s can_draw gate (so the offscreen+post path runs; the water post overwrites the whole frame, quad never visible), then Graphics.addPostEffect("shaders/water", [mouseX01, mouseY01, 0, 0]) each frame — drag the mouse to orbit, idle → self-screenshot (water_raymarch.png) + exit, mouse-move keeps it live. A DEMO of existing capabilities — the full-screen post path + Input — NOT a new engine API (no ADR / /security-review / CVE / new Wren class). Website: /examples/water-raymarch NEW (what it is, the can_draw-trigger + mouse-param mechanics, the Playground→post-ABI porting table, the mouse-orbit controls) + a curated /examples Rendering-section row. No landing card — a demo of the already-carded post-effect path, not a new capability. Also fixed a stale leftover in the /examples/water-playground row: “bounded water volumes not yet wired into cameraWaterState” was true at ADR 0094 but WTR2 #3/#4 wired Graphics.setWaterVolume + the loader (ADR 0097/0098). +1 app → 117 apps, still 92 classes. Gates green: engine check_docs_coverage ✔ exit 0 — 117 app(s), 92 Wren class(es) (apps/water_raymarch no longer absent; the 8 pre-existing method-level advisories are unrelated — AnimController / Async / Node / PhysicsContact); website docs:check 167 rows ✔; build 171 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit f192c84), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-07 (delta — Water Fidelity (Block WTR2 #4) — 5-octave realistic buoyancy height (Water.setRealistic) + the Graphics.setWaterVolume doc/honesty fix (engine main, commits 122c46d [ADR 0098] + 63b6ab8) — COMPLETE. Two small water syncs folded into one unit. (1) WTR2 #4 — Water.setRealistic(v) (ADR 0098, 122c46d): a NEW additive Wren method on the frozen Water class (ADR 0091). The device-free water_wave.h height mirror (what Water.heightAt / Physics.applyBuoyancy read) mirrored the legacy 3-wave plume3d_waterVertex, but the WTR2 primary surface is the 5-octave plume3d_waterVertexRealistic — so water_playground (the only demo that renders waves AND floats bodies; its water is water_realistic) had floaters bobbing on a different surface than drawn. setRealistic(true) switches the height query to the exact 5-octave mirror (directions d1..d5 + per-octave length/steepness/speed factors), so buoyancy tracks the rendered swell. Default is unchanged (3-wave) — flipping it would silently change every existing consumer’s (incl. the Wren-only games’) buoyancy feel — so a game opts in next to setWaves, exactly as apps/water_playground does (_water.setRealistic(true)). Additive, no existing signature changed (LowTide/Lexicon-safe); the mirror is unit-tested against a hand-computed 5-octave reference so it can’t drift from the shader. (2) Graphics.setWaterVolume doc + honesty fix (63b6ab8): the method IS exposed to Wren — setWaterVolume(minX,minZ,maxX,maxZ,minY,surfaceY,fogR,fogG,fogB,fogDensity) (a BOUNDED water box; cameraWaterState reports submerged only inside it) — and is now auto-called by the WTR2 #3 loader (ADR 0097), but the site’s ADR-0094 sync had documented it as “NOT exposed to Wren”. Corrected: setWaterVolume now has its own /api/graphics section beside setWaterPlane/cameraWaterState, and the stale “not exposed” claim in the ADR-0094 delta below + the /api/graphics row + the landing card is fixed. Website: /api/water Updated (a new ### setRealistic(v) section under Configuring the waves + a buoyancy note that Realistic-rendered water should opt in); /api/physics Updated (the applyBuoyancy water param notes the setRealistic(true) option for realistic water); /api/graphics Updated (a new ### Graphics.setWaterVolume(...) section — the 10-float bounded box + the loader auto-call cross-ref; the “Scope of this API today” note rewritten — both a plane and a bounded volume are drivable). Landing: the “Underwater & interactive water” card updated (adds setWaterVolume for bounded pools; the “two methods” count fixed) and the “Water physics — height query & buoyancy” card notes setRealistic(true) mirrors the 5-octave realistic surface — no new card (both are fidelity/accuracy refinements of already-carded capabilities). NOT in this unit (still the separate follow-up): the WTR2 #2 world-space underwater pass + ripple wake. Security: WTR2 #4 is a device-free height helper + one additive Wren method — no untrusted-input surface (ADR 0098 ran no /security-review); setWaterVolume was already-shipped engine code — docs only. No vendored-submodule change → no CVE trigger. No new Wren class or app → still 92 classes, 116 apps (Water.setRealistic now documented → the check_docs_coverage method-level advisory list drops from 9 to 8). Gates green: engine check_docs_coverage ✔ exit 0 — 116 app(s), 92 Wren class(es); website docs:check 166 rows ✔; build 170 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commits 122c46d — WTR2 #4/ADR 0098, 63b6ab8 — setWaterVolume Wren wiring), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-07 (delta — Water Fidelity (Block WTR2 #3) — the plume3d_water loader wires the Realistic variant + auto-registers the underwater volume (engine main, commit 3bb0846, ADR 0097) — COMPLETE. A loader follow-up to the camera-submersion capstone below (ADR 0094): the Blender plume3d_water tag now drives the WTR2 Realistic look and the underwater path end-to-end. Three loader behaviours, all internal to BlendResult.instantiateWater — no public C++ or frozen-Wren signature changed (LowTide/Lexicon-safe): (1) Realistic variant reachable — blendWaterShaderName now maps plume3d_water="realistic" and the "pbr" alias → water_realistic (the WTR2 multi-octave Gerstner + PBR + world-anchored caustics + crest-foam shader that replaced the superseded water_pbr); before this both spellings wrongly resolved to water_pbr, so the tag could only reach the Realistic look by spelling the raw basename. "toon"/"water"/"1"/"true"/empty → water_toon (unchanged). (2) Per-variant params + new Realistic props — the loader emits a variant-selected set-5 default block (the byte-for-byte twin of water_playground’s applyRealistic()/applyToon()), plus new authorable Realistic-only float-array props plume3d_water_pbr (p4 roughness/metallic/F0), _refract (p5 refractStrength/distortAmt/chromAmt/specPower), _caustic (p6 causticScale/causticChroma), _crest (p7 crestFoamThreshold/Strength/heightColorAmount/heightNorm), _caustic_strength (p11.y, written component-wise so it never clobbers p11.x=useFoamTex), and _fog ([r,g,b,density] for the underwater fog) — each finite-guarded AND range-clamped (finite alone is unsafe for the specPower pow() exponent, the heightNorm normalizer, the causticScale UV multiplier). The existing colour/wave props (_shallow/_deep/_foam/_waves/_horizon) and _volume/_level/_subdiv are unchanged. (3) Auto-registration for underwater — instantiateWater now registers one primary WaterSurfaceState (a plume3d_water_volume box as a bounded volume via setWaterVolume, else the largest flat plane via setWaterPlane), so Graphics.cameraWaterState + the underwater post now fire on authored .blend water with no manual setWaterPlane — closing the “bounded-volume submersion is a follow-up” gap the ADR 0094 delta below flagged, for the authored/loader path. Buoyancy stays a separate app-side path (Physics.applyBuoyancy reads a Wren Water; the game builds Water.setWaves(...) matching the authored plume3d_water_waves — loader-owned buoyancy is WTR2 #4). The verification demo apps/blend_water_demo was retargeted to a Realistic volume fixture (Models/WaterRealistic.blend, tools/testdata/make_water_authored_fixture.py): it loads the box, dives the camera below y=0, and runs the underwater post driven by cameraWaterState(); the old Toon WaterVolume.blend + mesh WaterScene.blend are kept for tests. Website: /api/resource Updated (the instantiateWater shader-name table fixed to realistic/pbr→water_realistic + a WTR2-#3 note; a variant-selected look intro; a Realistic-only props table with clamp ranges + the plume3d_water_fog note; a new Auto-registration for underwater subsection; the fixture line → WaterRealistic.blend); /examples/blend-water-demo Updated (retargeted to the Realistic volume + camera-dive/underwater — intro three-step, scene table, init/draw snippet with cameraWaterState, the Realistic fixture-prop block, APIs + Notes); /api/graphics Updated (the “Scope of this API today” note corrected — a box-tagged pool now auto-registers a bounded volume via the loader/ADR 0097, no longer a “not yet driven” follow-up; the screen-space-approximation caveat kept). Landing: the existing “Blender water tag” card updated (realistic→water_realistic, the new clamped Realistic props, and the auto-registration so cameraWaterState/underwater work on authored water with no setWaterPlane), and the “Underwater & interactive water” card’s honest-scope clause corrected (an authored box-tagged pool now auto-registers a bounded volume) — no new card (a fidelity/wiring extension of the already-carded water tag, not a new capability). Out of THIS scope (noted follow-up): Graphics.setWaterVolume was separately exposed to Wren (engine 63b6ab8) but still lacks its own /api/graphics section — a pre-existing method-level advisory gap, outside ADR 0097; flagged for a separate sync. Security: the loader touches the untrusted-.blend surface but adds no new byte parser — reads only already-parsed prop maps; the guard logic is extracted to a pure, unit-tested header (water_prop_clamp.h / test_water_prop_clamp.cpp: NaN/±inf/out-of-range/oversized-arity → default/clamped, no OOB) and the non-volume SceneNode TRS is now finite-guarded symmetrically; a blocking /security-review ran (ADR 0097). No vendored-submodule change → no CVE trigger. No new Wren class or app → still 92 classes, 116 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 116 app(s), 92 Wren class(es); website docs:check 166 rows ✔; build 170 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit 3bb0846 — WTR2 #3, ADR 0097), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-07 (delta — Water Fidelity (Block WTR2) camera-submersion API + interactive water capstone (engine feat/water-fidelity, ADR 0094) — COMPLETE. The water program’s final piece: the camera can go UNDER the water and the renderer reports it. Two new public frozen Wren methods on Graphics (ADR 0094): setWaterPlane(surfaceY, fogR, fogG, fogB, fogDensity) (register an infinite flat water surface + its underwater fog — sticky renderer state, like setFog/setSky/setPlanarReflection) and cameraWaterState() → List [submerged, amount, waterLevel, fogR, fogG, fogB, fogDensity] (camera position recovered engine-side from the view matrix; amount is the smooth 0→1 submersion to cross-fade the underwater post + waterline on). Additive, no existing signature changed (LowTide/Lexicon-safe). The underwater look + the waterline meniscus quad are otherwise game-side content on set-5 params (no engine change). Honesty correction vs the source material: ADR 0094’s decision also names Graphics.setWaterVolume(...), but it is NOT actually exposed to Wren in the shipped build (not declared in wren_engine_module.cpp, no signature in the wren_api.cpp resolver) and the C++ graphicsSetWaterVolume is never called (the loader does not wire it yet) — so bounded-volume submersion is a documented follow-up, and the underwater effect stays a screen-space approximation (world-space pass deferred). Documented that way, not as shipped. [Corrected 2026-08-07 (see the WTR2 #3 + #4 deltas above): this held at the ADR-0094 sync but is now resolved — Graphics.setWaterVolume was wired to Wren in engine 63b6ab8, the loader auto-calls it (ADR 0097), and it is documented on /api/graphics; only the screen-space underwater approximation remains a follow-up.] New app apps/water_playground (the interactive capstone — free-fly + dive, runtime variant/weather toggle, buoyant floaters) — it was ABSENT from doc-status, so the engine check_docs_coverage gate was failing on it; now fixed. Website: /examples/water-playground NEW (full page — controls, the submersion API pattern, the meniscus, buoyancy, the scripted self-verify tour); /api/graphics Updated (a new Water & camera submersion (WTR2) section — setWaterPlane + cameraWaterState with the return-List table + the honest scope note); /examples index + row added; landing card added on index.mdx (Underwater & interactive water (camera submersion) — no gate covers the card, added by hand). No new Wren class (methods on the existing Graphics) → still 92 classes; +1 app → 116 apps. MDX gotcha respected: the water-playground frontmatter description uses em-dash not : (a colon-space is a YAML mapping — it broke the first build; fixed); the landing card avoids leading-underscore tokens. Security: website docs only + two additive engine methods reviewed in ADR 0094 (renderer state, camera pos recovered engine-side; no untrusted-input surface) — no CVE//security-review trigger on the docs side. Gates green: engine check_docs_coverage ✔ exit 0 — 116 app(s), 92 Wren class(es) (apps/water_playground no longer absent; 8 pre-existing advisory method-level warnings unrelated to water); website docs:check 166 rows ✔; build 170 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (feat/water-fidelity merged, ADR 0094), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Loader-tagged water auto-binds the real Perlin foam → loader water matches the variants 1:1 (engine PLM-275, commit b02315c) — COMPLETE. A small engine (loader) follow-up to the real-Perlin-foam-texture delta below, which had noted that blend_water (loader-created) water used the procedural fbm fallback — that gap is now closed. BlendResult.instantiateWater (both the tagged-mesh and box-volume paths) now loads the pack’s two foam maps itself — textures/water_noise.png (Roystan _SurfaceNoise) + textures/water_distortion.png (_SurfaceDistortion) — once per scene via the existing trusted WrenVm::loadTextureFromFile (the same path Texture.load uses), binds them to the water surface’s set-5 texture slots 0/1, and sets p11.x = useFoamTex = 1, so a plume3d_water-tagged surface reproduces the immediate-mode toon/pbr variants 1:1 (the tag "toon"/"pbr" is the only difference). The paths are FIXED convention paths, not read from the untrusted .blend (no new byte parser, no new untrusted-input surface — it reuses the already-shipped texture-load path); if the app doesn’t ship them the shader’s procedural fbm foam stands (useFoamTex stays 0). An app opts in simply by dropping water_noise.png + water_distortion.png into its textures/ folder (generator tools/testdata/make_water_foam_textures.py); apps/blend_water_demo now ships both. Also: buildBlendWaterCustom default foamDistance 1.2→2.5 and the volume fixture’s plume3d_water_foam reach 1.5→3.0 (make_water_volume_fixture.py), so the loader’s textured foam has the same surface coverage as water_demo. No frozen Wren / public signature changed (a fixed-path texture load inside the existing method; set-5 params + textures are shader-interpreted). Website: /api/resource Updated (a Real Perlin foam, auto-bound paragraph under instantiateWater — the two convention paths + slot 0/1 bind + useFoamTex + procedural fallback + generator); /examples/blend-water-demo Updated (an auto-bound-foam note after the shader-wiring paragraph + the fixture snippet’s plume3d_water_foam.w 1.5→3.0 to match make_water_volume_fixture.py). No landing-card change (a fidelity refinement of the already-carded water foam / Blender water tag, not a new capability). MDX gotcha respected: every Roystan property (_SurfaceNoise, _SurfaceDistortion) and the useFoamTex token is backticked (no space-then-underscore opening emphasis); index.mdx untouched. Security: loader C++ that reuses the trusted, already-shipped fixed-path texture-load — no new untrusted-.blend read, no new parser, no descriptor / frozen-contract change (no ADR, /security-review or CVE trigger — the engine commit ran none for the same reason). No new Wren class or app → still 92 classes, 115 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 115 app(s), 92 Wren class(es); website docs:check 165 rows ✔; build 169 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit b02315c — PLM-275), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Real tileable Perlin foam texture — Roystan _SurfaceNoise / _SurfaceDistortion (engine PLM-275, commit 7513e4c) — COMPLETE. A shader + content follow-up to the faithful-rewrite delta below: the owner asked for “a real Perlin noise texture for the foam” (the procedural value-noise fbm read soft/cloudy). plume3d_foamMask in water_common.slangh now branches on a new set-5 param p11.x = useFoamTex — 1 samples a real, tileable Perlin _SurfaceNoise texture (custom slot 0) at a UV warped by a _SurfaceDistortion texture (custom slot 1, RG) for the crisp, detailed cel foam; 0 keeps the procedural fbm fallback (used by a loader-tagged plume3d_water surface that has no game-side handle to bind textures). The two maps are bound via Mesh.setCustomTexture(0/1, Texture.load(...)) and ship as water_noise.png / water_distortion.png, generated by tools/testdata/make_water_foam_textures.py (pure-Python Pillow, seamlessly tileable periodic Perlin — lattice coords wrapped mod the period so the scrolling foam never seams). water_demo / river_demo / buoyancy_demo load + bind them + set p11.x=1; blend_water (loader-created) uses the fallback. No frozen Wren / public signature changed (set-5 params + textures are shader-interpreted; setCustomTexture is pre-existing). Website: /examples/water-demo Updated (a Foam textures subsection + the p11.x = useFoamTex param row + the two custom-texture slots; the Procedural foam intro + _SurfaceDistortion bullet + shoreline-AA note now describe the real-texture path with the procedural fallback; the Notes “custom water textures … ready to swap” follow-up corrected to partly landed). No landing-card change (a fidelity refinement of the already-carded water foam, not a new capability). Security: shader-source + generator script + doc only — no engine C++ / descriptor / frozen-contract / .blend-parser change (no ADR, /security-review or CVE trigger). No new Wren class or app → still 92 classes, 115 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 115 app(s), 92 Wren class(es); website docs:check 165 rows ✔; build 169 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit 7513e4c — PLM-275), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Faithful 1:1 water shader rewrite — set-5 param semantics corrected (engine PLM-275, commit 358bb27) — COMPLETE. A shader-only follow-up to the box/VOLUME parity delta below: the owner found the stylized water read “wrong and bad” (dark corrugated bands, glare blobs, blocky quantization), so water_toon.slang / water_pbr.slang were rewritten as faithful 1:1 ports — water_toon is now Roystan “Toon Water”: a FLAT plane with only a depth-based 2-tone colour + crisp white depth-foam (no waves / normals / specular / fresnel / reflection); water_pbr is the gameidea “stylized water” look: the same flat depth base + gentle Gerstner + light refraction + soft fresnel sky rim + a small glint (no heavy planar-reflection mix). No frozen Wren / public signature changed (set-5 params are shader-interpreted). This corrected the set-5 water PARAM SEMANTICS that the two deltas below documented WRONG: plume3d_water_deep / p1.w is now depthMaxDistance (the world distance over which shallow→deep runs — was “absorptionK” / “Beer–Lambert K”); plume3d_water_foam / p2.w is now foamDistance (Roystan _FoamDistance, foam reach — was “foam max depth”); p7 is now (surfaceNoiseCutoff [Roystan 0.777], foamScrollX, foamScrollY, surfaceDistortAmount [0.27]) — the depth-scaled distorted-noise Roystan foam (was “foamCutoff / edgeFoamWidth”); p6 (specStep / sparkle / bandCount / rim) and p11 (rain / caustics) are no longer read by the faithful shaders (the caustic + rain-bloop helpers remain in water_common.slangh but are uncalled). Authoritative layout: the header of engine/shaders/include/water_common.slangh. Website corrections: /examples/water-demo Updated (the two variant rows rewritten to the faithful ports, the shared-core row, the p0..p10 param table + the p6/p11-unused note, the foam/caustics/rain section, and the reflection/specular Notes bullets); /examples/river-demo Updated (p7 semantics + p6 marked legacy in the toon snippet); /examples/blend-water-demo Updated (fixture snippet plume3d_water_deep.w → depthMaxDistance 4.0, plume3d_water_foam.w → foamDistance 1.5, matching make_water_volume_fixture.py); /api/resource Updated (the instantiateWater look-override table — plume3d_water_deep / _foam 4th-component meanings). No landing-card change (a fidelity/semantics correction of the already-carded water, not a new capability). Security: shader-source + doc only — no engine C++ / descriptor / frozen-contract / .blend-parser change (no ADR, /security-review or CVE trigger). No new Wren class or app → still 92 classes, 115 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 115 app(s), 92 Wren class(es); website docs:check 165 rows ✔; build 169 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit 358bb27 — PLM-275), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Blender box/VOLUME water tag + vivid stylized-water parity (engine PLM-275, commit f781551, ADR 0093) — COMPLETE. Extends the plume3d_water mesh tag (PLM-274, ADR 0092, the delta below) with two things. (1) A box/VOLUME water tag (ADR 0093) — additive, folded into the existing method. A Blender box tagged plume3d_water plus plume3d_water_volume (a truthy int / any non-empty string — detected like plume3d_terrain) makes Scene.loadBlendScene generate a level water surface plane 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. Folded into the same BlendResult.instantiateWater — NO new Wren method or signature (the frozen Wren contract is untouched); instantiateUnits already skips every plume3d_water node so the box is never drawn as a solid prop. New optional volume-only props: plume3d_water_subdiv (int, or [subX, subZ]; hard-clamped 1..256 per axis — the memory bound) overrides the auto tessellation (~2 quads/unit); plume3d_water_level (float) sets an absolute world-Y waterline. The generated surface is a level rectangle from the box’s world AABB (non-rectangular volumes / bottom-face floor tint / tilted surfaces are non-goals). Fixture tools/testdata/make_water_volume_fixture.py → apps/blend_water_demo/Models/WaterVolume.blend; the demo now loads the box volume (the mesh-tag WaterScene.blend is kept for the loader tests). (2) Visual parity — the water now matches the reference tutorials (param + shader tuning, no new capability). The stylized water is a vivid saturated cyan → blue (was muddy/near-black navy) with a blue reflected sky: deep colour de-crushed to a saturated cobalt/royal blue, reflection weight 0.35→0.60 + horizon fade 0.92→0.70 in water_common.slangh + the water_* demos, and a flipped seabed tilt sign uncovered the (already-blue) sky. Website changes: /api/resource Updated (a new Box/volume water (plume3d_water_volume) subsection under instantiateWater — the marker + _subdiv/_level table, the world-top-face generation, the finite-guard + subdiv-clamp security note; the intro + Returns now name the box path); /examples/blend-water-demo Updated (the box WaterVolume.blend scene table + loadBlendScene snippet + a box-volume authoring block with the vivid fixture palette; the old “tagging a volume/box … is a non-goal” note corrected — it ships); /blender-addon Updated (the plume3d_water authoring bullet gains a plume3d_water_volume / _subdiv / _level box sub-bullet). Landing: the existing “Blender water tag” “What’s in place” card updated — title now “a mesh or a box volume”, a new box/VOLUME sentence (generate at the box top face + _subdiv/_level + same method), and the vivid cyan→blue / blue-sky palette noted (the landing step is not skipped). Security: the box-volume path touches the untrusted-.blend surface but adds no new byte parser — it reads already-parsed prop maps only; every box vert / node transform / world corner / plume3d_water_level is finite-guarded (a non-finite or zero-area box is skipped + warned), the subdivision is hard-clamped 1..256 on every path (auto + both override forms — the real memory bound), and meshIndex is bounds-checked (/security-review CLEAN, ADR 0093 §Security); parity is pure content (no new CVE surface — no vendored-submodule change). No new Wren class or app → still 92 classes, 115 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 115 app(s), 92 Wren class(es) accounted for (the 8 method-level advisories are pre-existing and unrelated — AnimController/Async/Node/PhysicsContact); website docs:check 165 rows ✔; build 169 pages ✔ (only the pre-existing benign sitemap site warning). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commit f781551 — PLM-275, ADR 0093), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Water visual parity + the Blender plume3d_water tag (engine PLM-273 / PLM-274, ADR 0092) — COMPLETE. Two Water-pack changes that landed on engine main after the buoyancy sync below. (1) Water visual parity (PLM-273, commit a7c54d8) — pure content, no engine change. The stylized water’s procedural foam is now faithful to the tutorials it follows (gameidea.org stylized water + roystan.net toon water): Roystan depth-scaled shoreline foam — a foamDepth01 = depthBelow/foamMax scales the foam-noise cutoff so foam is a crisp band hugging shorelines/objects, clean in deep water — plus his _SurfaceDistortion done procedurally (a second, slower noise field warps the foam-noise UV so the edges churn like whitewater). Also documented as SHIPPED (present in the shared core, previously mis-listed as “not shipped”): rain bloops (expanding impact ring-ripples gated by a rain-intensity param — the water counterpart of the Weather wet-ground ripple) and procedural caustics (opposed-scrolling noise, depth-faded). Source of truth: engine/shaders/include/water_common.slangh, apps/{water,river,buoyancy}_demo. (2) The Blender water tag (PLM-274, commit 8fe8c19, ADR 0092) — a small ADDITIVE engine feature. A NEW Wren method BlendResult.instantiateWater(scene) (folded into Scene.loadBlendScene, so a whole-scene load now also builds water) + a NEW Blender authoring convention parallel to plume3d_terrain (PLM-254): tag a mesh with a custom property plume3d_water (a string naming the water shader — toon→water_toon, pbr→water_pbr, else verbatim as a shader basename; the shader stays pack content) and loadBlendScene draws it as a stylized water surface — no Wren wiring. Optional float-array look overrides plume3d_water_shallow/_deep/_foam/_waves/_horizon (set-5 p0/p1/p2/p3/p10, NaN-guarded). Under the hood a SceneNode now carries an optional CustomMaterial (the general “scene node drawn with a pack shader + set-5 params” capability, additive/inert-until-set); the engine fills the camera world position into slot p9 each frame while the artist owns the look. NEW app apps/blend_water_demo (fixture tools/testdata/make_water_scene_fixture.py → Models/WaterScene.blend). Additive — no existing signature changed (LowTide/Lexicon-safe), ADR 0092, engine_api.json regen. Website changes: NEW /examples/blend-water-demo page + curated /examples index (Blender/scene) row; /api/resource Updated (BlendResult gains instantiateWater — the full plume3d_water* tag table + the SceneNode custom-material/slot-p9 note; instantiateUnits skip-list now names plume3d_water); /api/scene Updated (loadBlendScene composes instantiateWater after terrain, before units); /blender-addon Updated (a Tag-based authoring conventions note — plume3d_terrain + plume3d_water); /api/water Updated (“See also” → the Blender water tag path); /examples/water-demo Updated (a Procedural foam, caustics & rain section + a p11 param row; rain bloops + caustics corrected to shipped; the foam parity described). Landing: NEW “Blender water tag (author water in Blender)” “What’s in place” card (a genuinely new user-visible capability — a Blender authoring on-ramp + a new Wren method, not just content), and the existing “Stylized water (PBR & Toon)” card’s foam/bloops/caustics tail corrected (faithful Roystan foam described; rain bloops + caustics moved from “not shipped” to shipped). Truthful remaining follow-ups (still NOT shipped): a masked buoyant open-hull boat, custom water textures (procedural stand-ins ready to swap), and per-bend river flow; PLM-274 v1 non-goals — tagging a volume/box to generate the plane, per-frame near/far auto-fill, flow-along-a-tagged-curve. Security: parity is pure content (no engine source/descriptor/frozen-contract change → no ADR//security-review/CVE trigger); the water tag touches an untrusted-.blend surface but reads only already-parsed strings + float arrays (no new byte parser), every set-5 write a hard-coded slot, the override loop capped at 4 + finite-guarded, meshIndex bounds-checked → /security-review run (ADR 0092 §Security), no new CVE surface (no vendored-submodule change). No new Wren class → 92 classes; +1 app (blend_water_demo) → 115 apps. Gates green: engine check_docs_coverage ✔ exit 0 — 115 app(s), 92 Wren class(es) accounted for (BlendResult.instantiateWater no longer in the advisory list; the 8 remaining method-level advisories are pre-existing and unrelated — AnimController/Async/Node/PhysicsContact); website docs:check 165 rows ✔; build 169 pages ✔. Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commits a7c54d8 water parity — PLM-273, 8fe8c19 blend water tag / ADR 0092 — PLM-274), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Water height query + buoyancy + rivers + underwater (engine PLM-273 / PLM-270 / PLM-271, ADR 0091) — COMPLETE. Four related Water-pack changes that landed on engine main after the surface-pack sync below. (1) The physics half is now a real engine feature — a NEW frozen Wren class Water (the CPU mirror of the stylized water shader’s Gerstner waves: Water.new(), setWaves/setWind/setBaseY, applyToMesh(mesh) → writes the waves into the mesh’s set-5 p3 so render == query, heightAt(x,z,time) → the surface Y matching the rendered waves; water_wave.h reproduces water_common.slangh exactly so a float bobs on the waves you SEE) plus a NEW method Physics.applyBuoyancy(node, water, time, offsets, strength, density) — multi-point Jolt buoyancy: offsets = a flat List of local [x,y,z] probe points (a raft’s 4 corners → rocks/tilts; empty → a single centre bob), force per probe = strength·submersion/density at that world point, density (rel. to water, clamped ≥0.05) the responsiveness knob (low floats high + snappy, high sits low + sluggish). Additive — no existing signature changed (LowTide/Lexicon-safe), ADR 0091, engine_api.json regen. (2) NEW app apps/buoyancy_demo (rafts rock on 4 corners; cubes at density 0.35/0.7/1.25 float at different heights). (3) apps/river_demo shipped — a Catmull-Rom Spline swept into a MeshGen.ribbon with the shared Toon water flowing downstream off the wind vector (pure content, Block SPL + the water pack). (4) apps/water_demo underwater — now also dips the camera below the surface + applies an underwater post effect (tint→deep + fog + wobble + caustic + vignette). Website changes: NEW /api/water class page (mirrors the sibling Spline/MeshGen/Scatter/Physics pages — the check_docs_coverage CLASSES check keys off it) + /api/physics Updated (new Buoyancy section for applyBuoyancy); NEW /examples/buoyancy-demo page; /examples/river-demo upgraded from the prior Planned “no doc page yet” gap row to a real page now that rivers committed; /examples/water-demo Updated (a Below the surface underwater section + the three follow-ups it listed as “not shipped” — rivers, underwater, buoyancy — corrected to shipped); curated /examples index rows for river + buoyancy + the water-demo underwater note. Landing: NEW “Water physics — height query & buoyancy” “What’s in place” card (a genuinely new engine capability — a frozen Wren class + a Physics method, unlike the pure-content surface pack), the existing “Stylized water (PBR & Toon)” card’s “rivers/underwater/buoyancy … not yet shipped” tail corrected to shipped, and Water added to the Scripting card’s class list. Truthful remaining PLM-273 follow-ups (still NOT shipped): rain bloops on water, a masked buoyant open-hull boat (the water-mask discard exists; the buoyant hull does not), custom water textures, and per-bend river flow (the river flows off one global wind vector). Security: the buoyancy/height-query is pure value-holder + math + game-provided bounded List — no asset/.blend parser touched, no new untrusted-input surface (ADR 0091: /security-review advisory, no CVE trigger); the river + underwater are pure content. +1 new Wren class → 92 classes; +1 app (buoyancy_demo) → 114 apps (river_demo was already counted as a working-tree app). Gates green: engine check_docs_coverage ✔ exit 0 — 114 app(s), 92 Wren class(es) accounted for (the 8 method-level advisories are pre-existing and unrelated: AnimController/Async/Node/PhysicsContact); website docs:check 164 rows ✔; build 168 pages ✔ (only the pre-existing benign sitemap site warning). Committed on the Website repo main (deploy owner-gated, website HUMAN-001 — NOT pushed/deployed). Engine main (commits 065735d height-query + buoyancy / ADR 0091, 5779e85 river, 10ba108 underwater — PLM-273/270/271), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Stylized Water pack — PBR + Toon surface sharing one core (engine PLM-273) — COMPLETE (surface half). Content pack #5 of the Environment & Stylization program (Toon → Sky → Weather → Foliage → Water). Stylized water in two variants side by side sharing ONE core — PBR (realistic) left, Toon (anime / Inasa-Fujio) right, over the same waves — built with ZERO engine change (no engine source, descriptor layout or frozen contract), the same engine-vs-pack posture as the Toon / Foliage packs. The shared core water_common.slangh (mesh ABI, #include "plume3d.slang") is the whole surface: 3 Gerstner waves (hand-unrolled, MoltenVK-safe) steered by Graphics.setWind with an analytic normal + procedural scrolling ripple normals (no texture); depth colour + Beer–Lambert underwater absorption via PLM-263 scene depth (set 0 binding 5, 1 − exp2(−K·depthBelow)); A#3 refraction of the opaque scene behind (binding 6) at a normal-distorted screen UV, foreground halo-guarded; A#4 planar reflection (binding 7) biased to the sky/horizon colour (the reflection pass renders no sky) + Schlick fresnel; and an infini-water horizon fade (a clip-space ScreenPos varying gives a resolution-free screen-UV grab-pass, since a world-space plane can’t derive NDC from SV_Position). The two variants are thin and differ only in the style remap: water_pbr.slang = continuous depth colour + soft Blinn spec + soft (smoothstep) foam; water_toon.slang = floor()-quantized colour bands + a hard stepped foam line (tiny-AA) + a stepped specular glint / sparkle + a bright contact rim. Tuned entirely through set-5 custom params (Mesh.setCustomColor at byte offsets, not slot indices — p0–p5 shared geometry/depth/refraction/foam, p6/p7 Toon-only, p8 camera planes, p9 cam pos, p10 horizon + fade), gated by the existing Graphics.opaqueCaptureEnabled(true) (the one gate feeding both the opaque colour and depth copies) + a passthrough post effect; the water planes are subdivided MeshGen.plane, blendMode = "alpha" (transparent phase). Built on already-shipped surface only — set-5 params + PLM-263 depth + A#3 colour + A#4 reflection — so no new Wren class, no engine_api.json change, no api/* page. Website changes: NEW /examples/water-demo page (apps/water_demo — the shared core, both variant remaps, the set-5 param table, the opaqueCaptureEnabled gate, and the honest follow-up list) + the curated /examples index (Mesh & graphics) row; landing NEW “Stylized water (PBR + Toon)” “What’s in place” card (a new user-visible capability — a stylized-water content pack with no engine change; the historically-skipped landing step, done). Truthful follow-ups (PLM-273, NOT shipped): spline rivers + flow (apps/river_demo — an uncommitted engine working-tree WIP, given an honest Planned “no doc page yet” doc-status row so the coverage gate accounts for it without claiming it ships), an underwater camera post, and Jolt buoyancy (engine PLM-270 height query / PLM-271); rain bloops on water + a masked open-hull boat (water-mask-demo) ride these. Security: pure content — no engine source change, no descriptor / frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger, per PLM-273). No new Wren class → 91 classes; +1 committed app (water_demo; the engine check_docs_coverage gate now reports 113 apps accounted for — water_demo committed on main, and river_demo covered as a Planned gap while it stays an uncommitted engine WIP). Engine main (commit bdfa446, PLM-273), Hyperscale/projects/plume3d-web.md)
Prior: 2026-08-06 (delta — Scatter masking — control WHERE grass / foliage / trees spawn (engine PLM-268) — COMPLETE. A Blender-authoring capability on the existing Bake-Scatter workflow — NOT a new Wren class / method / CLI / config-schema, and no engine change at all. The mask is a bake-time Blender-only input (a Scene RNA property + the geometry-nodes depsgraph), resolved into the baked/culled instance table; the engine only ever sees the culled instance table (nothing written to the .blend’s engine-read properties). Two authoring paths, both documented, that compose: (1) a geometry-nodes density mask — a Distribute Points on Faces density driven by a vertex-group weight or a texture / attribute — is captured automatically by the bake (it walks the evaluated depsgraph, so no add-on setting; density, scale and rotation can all follow the mask in nodes); (2) the add-on’s explicit mask image — for any scatter source (incl. particles / collection-instances with no geonodes density input): Plume3D › Scatter Bake › Scatter mask (where to spawn) → pick a Mask image + a Seed. The image is read top-down and stretched to the scatter’s world-XY bounding box (auto-fit — it 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 (SplitMix64), so density scales smoothly and re-bakes are stable (same seed → same result). It is fail-open — an unreadable / empty mask spawns everything, never silently clearing the scene. Website changes: /blender-addon Updated — a new “Bake Scatter: grass / foliage / trees” section (the site’s first documentation of the Bake-Scatter panel/operator) with a “Scatter mask: where things spawn” subsection covering both authoring paths, mirroring how the same page documents the PLM-267 Physics collider add-on field. Roadmap corrections mirroring the PLM-267 pattern (a shipped feature promoted out of every “roadmap” list): /examples/foliage-pack-demo Updated (its four “spawn masking = roadmap / PLM-268” references — the workflow step, the tree-authoring-needs table, the program-ties list, and the Notes roadmap — corrected to shipped, cross-linked to the new add-on section) and the curated /examples index row corrected. Landing: the “Grass / Foliage / Trees” “What’s in place” card’s stale “spawn masking … on the roadmap” clause corrected (masking now ships — both paths described), and the “Blender foliage & terrain import” card’s instantiateScatter description extended to note you mask where scatter spawns at bake time — NO new landing card (this extends the already-carded Blender-authored foliage capability, exactly as the PLM-267 colliders rode those same two cards, not a wholly new capability). Security: no engine source change and no new untrusted-input parsing — the mask is consumed inside Blender (the trusted authoring tool) at bake and never becomes an engine .blend read; the engine loads only the already-hardened baked instance table (ADR 0087 / the PLM-249 parser). No ADR, no /security-review trigger, no CVE surface; a bad mask is inert (fail-open). No new Wren class → 91 classes; no new app → 111 apps (PLM-268 touched only the add-on + docs/tests, no apps/*). Follow-up (deferred polish, not shipped): a vertex-group-weight mask source on the add-on + an in-Blender paint helper. Engine main (commit 6996f26, PLM-268), Hyperscale/projects/plume3d.md)
Prior: 2026-08-06 (delta — Blender-authored object colliders — the plume3d_collider tag (engine PLM-267, ADR 0090) — COMPLETE. A Blender-authoring capability on the existing Scene.loadBlendScene / BlendResult.instantiateUnits surface — NOT a new Wren class / method / CLI / config-schema. Tagging any imported object plume3d_collider makes the loader build a static Jolt collider on import, so a tree / rock / fence / prop blocks movement with no script. It generalizes the PLM-254 plume3d_terrain auto-collider to any object, configurable per object. The frozen tag contract (ADR 0090): plume3d_collider = none (default/absent → no collider) · box · sphere · capsule · convex · mesh; plume3d_collider_layer (gameplay collision-layer bitmask, the spatial-query filter; absent/≤0 → default layer bit 0); plume3d_collider_static (1 = static default, 0 = kinematic; mesh is always static — Jolt triangle meshes can’t move). Primitives (box/sphere/capsule) are sized from the object’s mesh bounds with the node scale baked into the shape (so one prototype scaled across a scene gets correctly-sized colliders; capsules Y-aligned); convex = a convex hull of the geometry; mesh = the exact triangle geometry (concave, static only — the same path instantiateTerrain uses). The physics world is created lazily (only if some object is tagged) and a malformed tag is inert (unknown shape / degenerate mesh → no collider, object still renders). Authored via the add-on’s new Physics collider field (shape enum + Static + Collision Layer, on any mesh object, independent of the Wren-script config) or by setting the custom property directly (identical wire format). Website changes: /api/resource Updated (new Blender-authored colliders (plume3d_collider) subsection under instantiateUnits — the tag table + sizing/lazy-world/inert-malformed notes, mirroring how instantiateTerrain documents its collider), /api/scene Updated (loadBlendScene now notes each prop also gains a collider from its tag), /blender-addon Updated (the Physics collider add-on UI field), /examples/foliage-terrain-demo Updated (its two rock props now carry plume3d_collider — rock_a = sphere, rock_b = box — and the demo drops a green probe onto each that rests on the rock, the instantiateUnits-collider witness), and /examples/foliage-pack-demo Updated (its PLM-267 roadmap entries corrected — authored-object colliders now ship; only per-scattered-instance colliders remain a follow-up, ADR 0090 non-goal). Landing: the existing “Blender foliage & terrain import” “What’s in place” card extended to describe colliders from Blender tags, and the “Grass / Foliage / Trees” card’s stale “per-tree colliders” roadmap note corrected — NO new landing card (this extends the already-carded Block BLD import capability, mirroring how the terrain auto-collider rides that same card, rather than a wholly new capability). Security: no new untrusted-input parsing — the tag values are interpreted from custom-property maps already regionFits-bounded at load (ADR 0087 / the PLM-249 parser), so ADR 0090 adds no new .blend read → no /security-review trigger, no new CVE surface; a malformed tag is inert. No new Wren class → 91 classes; no new app (foliage_terrain_demo already tracked → 111 apps). Engine main (commit ecdb70e, PLM-267, ADR 0090), Hyperscale/projects/plume3d.md)
Prior: 2026-08-06 (delta — Water masking demo (apps/water_mask_demo, engine PLM-273) — COMPLETE. A demo / examples-only sync — no engine change, no new Wren surface. It proves the owner ask “keep water from rendering inside boats and things”: a transparent water plane whose fragment shader discards fragments inside a boat’s world-XZ footprint, so an open-hull boat sits DRY (its interior shows the seabed) instead of flooded. The footprint is game-driven, not hardcoded — passed via Block MAT set-5 custom params (customParam(0) = centre + half-extent, customParam(1) = rotation + camera near/far), read in the mesh-ABI shader (#include "plume3d.slang") which discards where the rotated world-XZ point is inside the box. Arbitrary exclusion shapes (many boats, docks, terrain cut-outs) drive the same discard from a sampled mask texture (a top-down footprint rendered via an A#7 capture bound at a set-5 slot) — identical mechanism, only the mask source changes. The water also paints a depth-based shoreline foam around the rocks/hull via the PLM-263 scene depth (set 0 binding 5), linearizing both the sampled scene depth and the water’s own SV_Position.z with the same camera near/far so the eye-space gap is consistent; gated by the existing Graphics.opaqueCaptureEnabled(true) + a passthrough post effect, water blendMode = "alpha" (transparent phase, after the opaque depth copy). Built on already-public surface only (MeshGen, Graphics.newMesh/drawMesh/setViewMatrix/setProjectionMatrix/setViewProjectionEnabled/opaqueCaptureEnabled, Mesh.setCustomColor/blendMode, Scene.addCamera) → no new Wren class, no engine_api.json change, no api/* page, and NO new landing “What’s in place” card — masking is composed from the already-documented set-5 custom material + the PLM-263 depth (which already has its landing card), not a new capability. Website changes: NEW /examples/water-mask-demo page (apps/water_mask_demo — the set-5 footprint mask + discard, the mask-texture generalization, the PLM-263 shoreline, the opaqueCaptureEnabled gate, and the two authoring gotchas) + the curated /examples index (Mesh & graphics) row; cleared the temporary “no doc page yet” debt row carried for apps/water_mask_demo since the PLM-263 sync — mirroring how scatter_multi / toon_pack_demo were cleared once their engine work committed. Two authoring gotchas the demo pins down: (1) an immediate-mode demo must drive view/proj explicitly each frame (setViewMatrix/setProjectionMatrix) — Cam.orbit is the rig system and needs Scene.draw(), else a near-eye-level water plane renders only its far half; (2) setCustomColor’s first arg is a BYTE OFFSET (0 → customParam(0), 16 → customParam(1)), not a slot index. Security: pure demo — no engine source change, no descriptor/frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger). No new Wren class → 91 classes; +1 tracked app (water_mask_demo → 111 apps, already committed on engine main). Engine main (commit 692835e, PLM-273), Hyperscale/projects/plume3d.md)
Prior: 2026-08-06 (delta — Scene depth in the mesh ABI — set-0 binding 5 (engine PLM-263, ADR 0089) — COMPLETE. A render-foundation (Block RFX) change, not a Wren-API change — the Water-pack enabler. A mesh shader (typically a water surface) can now read the opaque scene depth behind/under it at set 0 binding 5, the primitive for shoreline blend, intersection foam, and depth-tinted colour. Read through the canonical mesh ABI — #include "plume3d.slang" → plume3d_sceneDepthLoad(int2(SV_Position.xy)) (raw device depth [0,1]), with plume3d_linearizeDepth(raw, nearZ, farZ) for an eye-space distance (the mesh ABI carries no near/far, so the caller passes its own camera planes). Exposed by mirroring A#3’s opaque-colour copy for depth: after the opaque phase (only when opaque capture is on) the engine copies sceneDepth_ → a separate opaqueDepth_ image, and a transparent (blendMode = "alpha") draw samples the copy — so a water surface reads the scene behind it without touching the live depth attachment it is being tested against (depth-attachment feedback). Binding 5 is a Texture2D<float> read via .Load() (a texel fetch — a D32 sampled through a SamplerState generates an undeclared split sampler in MSL on MoltenVK; the post pass proves .Load avoids it), always declared (the PLM-178 superset idiom — inert for shaders that ignore it, Slang strips it), defaulting to the reused 1×1 depth image when capture is off (zero cost — no copy runs — and never a crash). Gated by the existing Graphics.opaqueCaptureEnabled(true) — the same gate as opaque colour (A#3); so no new Wren surface, no engine_api.json change (LowTide/Lexicon unaffected) and no api/* page — this is a mesh-ABI capability, not a scripting change. It joins the other global mesh set-0 feeds: refraction opaque colour at b6 (A#3) and planar reflection at b7 (A#4) — one more feed, carrying depth; the post ABI has had sampleable scene depth since A#2 (this brings the read to mesh shaders, usable mid-scene by a transparent draw). Website changes: NEW /examples/scene-depth-demo page (apps/scene_depth_demo — what binding 5 is, the plume3d_sceneDepthLoad/plume3d_linearizeDepth accessors, the opaqueCaptureEnabled gate + the copy-not-feedback rationale, and the demo — four opaque bands at four different depths + a transparent quad reading binding 5 → a shoreline that tracks the bands, the varying gradient proving the per-pixel read; a constant/default read would be uniform) + the curated /examples index (Mesh & graphics) row next to its refraction/reflection siblings; landing NEW “Scene depth for mesh shaders (shoreline / foam enabler)” “What’s in place” card under the rendering cluster (a new user-visible capability — the historically-skipped landing step, done). Security: renderer / descriptor-layout change, no new untrusted-input parsing → /security-review advisory, not blocking; no CVE surface (ADR 0089). No new Wren class → 91 classes; +1 app (scene_depth_demo → 110 apps — it was the sole cause of the engine check_docs_coverage gate going red on main). Engine main (commit 86be54a, ADR 0089), Hyperscale/projects/plume3d.md, PLM-263)
Prior: 2026-08-06 (delta — Grass / Foliage / Trees content pack (engine PLM-266) — COMPLETE. Content pack #4 of the Environment & Stylization program (Toon → Sky → Weather → Foliage → Water). Stylized, instanced grass, foliage and trees with wind sway + back-light translucency (leaves glow when the sun is behind them), built with ZERO engine change — the whole look is one instanced shader shaders/src/foliage.slang riding capabilities the engine already ships (A#1 frame time + wind, enhanced instancing, A#6 atmosphere). The shader hand-declares its minimal descriptor set (set-0 view/proj + frame block, set-2 instance SSBO) — it must not #include "plume3d.slang" — and uses the active-shader path (a per-mesh shader on an instanced prototype breaks the instance SSBO, engine PLM-264). Owner emphasis — grass/foliage/trees are primarily authored in Blender: the demo apps/foliage_pack_demo is the shader/engineering proof (a procedural grass field + procedural trees — a lit trunk + a canopy of instanced leaf cards, all foliage in one InstancedMesh); the production path is Blender-authored — author a blade/leaf/tree in Blender → Bake Scatter (the plume3d_wren add-on, engine PLM-250) → import with Scene.loadBlendScene / BlendResult.instantiateScatter (Block BLD, one GPU-instanced draw per prototype) → render with shaders/foliage as the active shader so the imported vegetation gets wind + translucency. Tree-specific authoring needs are ROADMAP, not shipped: multi-part authored-tree prototypes + distance LOD / billboard imposters (PLM-266 follow-ups, LOD needs PLM-024), per-tree colliders (PLM-267), and spawn masking (PLM-268); wind-in-foliage audio (PLM-272); real grass/tree art (HUMAN-024), packaging/licensing (HUMAN-025). Website changes: NEW /examples/foliage-pack-demo page (apps/foliage_pack_demo — the foliage shader (wind + height gradient + two-sided leaf lighting + back-light translucency), how the demo fills one InstancedMesh via addInstanceColored/setInstanceParams, and — prominently — the Blender-authored workflow with the roadmap table) + the curated /examples index (Mesh & graphics) row; landing NEW “Grass / Foliage / Trees (instanced wind + translucency; Blender-authored)” “What’s in place” card (a new user-visible capability — the historically-skipped landing step, done). Security: pure content — no engine source change, no descriptor/frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger, per PLM-266). No new Wren class → 91 classes; +1 app (foliage_pack_demo → 109 apps — it was the sole cause of the engine check_docs_coverage gate going red on main). Engine main (commit 57a60e5 — foliage shader core + demo, zero engine change; 465e589 PLM-266 → done + Resume Point), Hyperscale/projects/plume3d.md, PLM-266)
Prior: 2026-08-06 (delta — Weather content pack (engine PLM-265) — COMPLETE. Content pack #3 of the Environment & Stylization program — it combines both halves of Plume3D’s engine-vs-pack boundary the earlier packs proved separately: the Toon pack’s shader half and the Sky pack’s policy half. Rain, snow, thunder, wind, dynamic clouds, height fog and rain-splash ripples, driven by a pure-Wren Weather state machine (clear/rain/snow/storm) over the engine’s neutral knobs, with ZERO engine change. Four content shaders: precip.slang (rain and snow in ONE instanced shader — per-instance mode params.z 0 rain / 1 snow, GPU-animated fall from the frame time + per-instance seed, so ~2,200 rain + 2,200 snow particles cost one GPU-instanced draw each; hand-declares its minimal set-0 + set-2 SSBO descriptor set — not #include "plume3d.slang", whose extra samplers corrupt the set-2 binding under MoltenVK), clouds.slang (a dynamic cloud sky on Graphics.setSky — a horizon→zenith gradient + an animated fbm cloud layer, coverage a param, the driver advancing a scroll each frame so they drift; #include "plume3d_sky.slang"), wet_ground.slang (the lit ground + expanding rain-splash ripples from the frame time, swapped in while raining), and the reused A#6 scene.slang lit ground. Scripts/Weather.wren is the reusable driver: each state drives the cloud sky + coverage, setAmbient, distance + height fog (setFog/setFogHeight), global setWind (rain slants + snow flutters with it), and toggles the two precip InstancedMeshes; storm auto-fires a decaying lightning ambient-flash. Website changes: NEW /examples/weather-demo page (apps/weather_demo — the four shaders + the Weather state machine, how it’s driven — loadShader "shaders/precip" first as the active instanced shader, the buildPrecip fields via addInstanceColored/setInstanceParams, Weather.new(rain, snow) + setState/update/bolt, the setSky "shaders/clouds"/setFogHeight/setWind policy, and the wet-ground swap — plus the per-state table and the two engine notes) + the curated /examples index (Mesh & graphics) row; landing NEW “Weather (rain/snow/thunder/wind + dynamic clouds + fog)” “What’s in place” card (a new user-visible capability — a weather system as a content pack with no engine change; the historically-skipped landing step, done). Two engine notes baked into the pack’s shape: (1) an instanced shader must hand-declare only set-0 (view/proj + frame) + the set-2 instance SSBO — the full mesh ABI corrupts the set-2 binding under MoltenVK (particles collapse to origin); (2) rain + snow share ONE active precip shader because a per-mesh newMesh(.., shader) on an instanced prototype breaks the instance SSBO (engine PLM-264, filed) — the per-instance mode param is the shipped workaround. Deferred (owner ask): rain bloops on water need a water surface → they ride with the future Water pack (phase 3; the wet_ground ripple is the reusable pattern); thunder audio is a documented Source.queueSamples extension (v1 ships the visual lightning flash). Security: pure content — no engine source change, no descriptor/frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger, per PLM-265). No new Wren class → 91 classes; +1 app (weather_demo → 108 apps — it was the sole cause of the engine check_docs_coverage gate going red on main). Engine main (commits f876efb + e02e801 — Weather pack, zero engine change), Hyperscale/projects/plume3d.md, PLM-265)
Prior: 2026-08-06 (delta — Sky / Weather content pack (engine PLM-262) — COMPLETE. Content pack #2 of the Environment & Stylization program, and the policy half of the engine-vs-pack boundary — where the Toon pack (below) proved the shader half, this proves the policy half: a stateful day/night cycle built entirely on engine knobs, with ZERO engine change. A pure-Wren time-of-day driver (apps/sky_weather_demo/Scripts/TimeOfDay.wren) paints a full day/night cycle — sky colour, sun direction & colour, ambient fill, distance fog — by driving the engine’s neutral Block RFX A#6 atmosphere knobs (all shipped, ADR 0076): TimeOfDay.new(scene, "shaders/sky"), then tod.apply(t) each frame with t in [0,1) (0.00 midnight, 0.28 sunrise, 0.50 noon, 0.72 sunset, ~0.80 dusk). apply(t) linear-interpolates keyframe tables (horizon / zenith / sun colour / sun energy / ambient) and writes each into Graphics.setSky (gradient mode 0 — params[0..2] horizon, params[4..6] zenith), Graphics.setAmbient, and Graphics.setFog (colour matched to the horizon so the far scene dissolves into the sky), then aims a directional sun Light (type = 1) along a dawn→noon→dusk arc (elevation + east→west sweep) with its setColor/energy from the tables. The engine stays policy-free — it exposes the knobs and the pass and takes no opinion on time of day; the whole day curve is pack Wren. Re-theme by editing the keyframe tables — still no engine change. Website changes: NEW /examples/sky-weather-demo page (apps/sky_weather_demo — the TimeOfDay driver + apply(t) usage, the four engine knobs it drives with their signatures, the policy-vs-engine split, the weather-extension note, and the v1 shader note) + the curated /examples index (Mesh & graphics) row; landing NEW “Sky / Weather (time-of-day driver)” “What’s in place” card (a new user-visible capability — a day/night cycle as a content pack with no engine change; the historically-skipped landing step, done). Extension (documented, not built): a weather state machine (clear/overcast/rain/storm) layers on top exactly the same way — more Wren policy over the same knobs (lerp sky/fog toward grey, drop sun energy, add a rain particle preset); cloud layers are a pack shader. Left for a game to author (engine non-goal). Security: pure content — no engine source change, no descriptor/frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger, per PLM-262). v1 note: reuses the stock A#6 sky/scene shaders verbatim (one pre-existing non-fatal MoltenVK SPIR-V→MSL pipeline-variant warning, same as sky_demo); renders correctly. No new Wren class → 91 classes; +1 app (sky_weather_demo → 107 apps). Engine main (commit 833ccd6 — Sky/Weather pack, zero engine change), Hyperscale/projects/plume3d.md, PLM-262)
Prior: 2026-08-06 (delta — Toon / Stylized-Shading content pack (engine PLM-261) — COMPLETE; replaces the temporary Toon debt row from the prior Block BLD sync with the real docs. The first content pack of the Environment & Stylization program: a complete stylized look — a cel/toon surface shader (shaders/toon.slang — N·L quantized into hard light bands, the darkest band tinted toward a cool shadow colour, plus a view-space Fresnel rim hardened into a toon rim light; #include "plume3d.slang", tuned through Block MAT set-5 custom params) plus a depth-edge ink outline post effect (shaders/outline.slang — a 4-tap plume3d_sceneDepth cross → central-difference gradient → dark ink over plume3d_sceneColor on silhouettes + depth steps; #include "plume3d_post.slang", added each frame with Graphics.addPostEffect("shaders/outline", [inkR,inkG,inkB, thicknessPx, depthThreshold, edgeGain])). Built with ZERO engine change — the reference proof of the engine-vs-pack boundary (the engine ships neutral, look-free capabilities; the pack ships the look), on only the public mesh ABI plume3d.slang + post ABI plume3d_post.slang + the set-5 params. The key finding: the outline needs no new engine pass because RFX A#5 (the post stack, Graphics.addPostEffect) + A#2 (sampleable scene depth) already expose everything a depth-edge outline reads. Website changes: NEW /examples/toon-pack-demo page (apps/toon_pack_demo — the two shaders, how each is added from Wren, the set-5 tuning-params table + the outline post params, and the v1 limitation) + the curated /examples index (Mesh & graphics) row; landing NEW “Stylized shading / Toon” “What’s in place” card (a new user-visible capability — cel shading + ink outlines as a content pack with no engine change; the historically-skipped landing step, done). Cleared the debt row: the temporary apps/toon_pack_demo “no doc page yet” row (parked out-of-scope during the prior Block BLD sync while the Toon app was untracked mid-run) is removed and replaced by the real example row — mirroring how scatter_multi was cleared once PLM-257 committed. Documented v1 limits: the depth-based outline catches silhouettes + depth steps but not interior creases on a continuous surface (those need a scene normal buffer — a future engine enhancement, not a pack change); the toon surface uses a single fixed key-light direction (wiring a scene light in is left to the consuming game). Security: pure content — no engine source change, no descriptor/frozen-contract change, no new untrusted-input parsing (no ADR, no /security-review, no CVE trigger, per PLM-261). No new Wren class → 91 classes; no new tracked app → 106 apps (toon_pack_demo was already scanned by the coverage gate as an untracked working-tree app in the prior sync; it is now committed on engine main). Engine main (commit 5c986c8 — Toon pack, zero engine change), Hyperscale/projects/plume3d.md, PLM-261)
Prior: 2026-08-06 (delta — Block BLD: whole-scene load — Scene.loadBlendScene + BlendResult.instantiateUnits (engine PLM-253, ADR 0088) — COMPLETE (Block BLD engine core done). The level-load entry point that composes the earlier Block BLD primitives (instantiateScatter/instantiateTerrain, below) into one call. No new Wren class — two additive methods. (1) BlendResult.instantiateUnits(scene) → Num (foreign, PLM-253, engine d9d7719): instantiate every remaining static mesh prop (rocks, fences, buildings — loose geometry that is neither terrain nor scatter and carries no script) as a render node with a shared GPU mesh (the loader de-dups identical mesh datablocks); returns the count. It skips, so the four instantiate calls partition the blend with no double-draw: plume3d_terrain meshes (owned by instantiateTerrain), the scatter prototype object by node_id — not by mesh datablock, so a hand-placed copy that merely shares the prototype mesh still renders as a real prop — hide_render objects, and objects inside a class_name-scripted subtree (those stay explicit via instantiate(className, scene)). (2) Scene.loadBlendScene(path) → BlendResult | null (pure-Wren composition, PLM-253, engine d9d7719/e394984): load a .blend and instantiate its whole environment in ONE call — terrain (render + auto static collider), baked scatter (GPU-instanced foliage), and static props — and return the BlendResult so the caller instantiates any class_name-scripted units (characters/cameras/scripted props) explicitly with blend.instantiate(name, this); those are intentionally NOT auto-instantiated (they carry scripts/actions/drivers the caller wires up). A convenience composition of the four primitives; call them individually for finer control (order, selective categories, per-unit handling). Caveat (documented on both pages): instantiate(name) also resolves a bare node_id, so instantiating a class-less prop by its node_id after loadBlendScene draws it TWICE — give explicitly-instantiated units a class_name (which loadBlendScene skips), not just a node_id. /api/resource Updated (instantiateUnits added to BlendResult) + /api/scene Updated (a loadBlendScene section + the double-draw caveat). /examples/foliage-terrain-demo rewritten: it now loads its whole level with a single scene.loadBlendScene call — terrain (render + collider), 256 GPU-instanced grass blades, and two static rock props (the instantiateUnits witness), physics probes resting on the terrain collider; and the terrain surface now renders (engine PLM-259 RESOLVED — the renderer resolves plume3d_shader_name by basename — so the prior “WIP: terrain surface does not rasterize” caveat is removed). Landing: the “Blender foliage & terrain import” card updated for the whole-level loadBlendScene + instantiateUnits. Security: no new untrusted-input parsing — loadBlendScene composes existing calls, instantiateUnits reads the already-parsed result through the existing hardened getOrBuildBlendGpuMesh; .blend parsing itself is unchanged (ADR 0088 → no /security-review trigger, no new CVE surface). No new Wren class → 91 classes; no new app → 105 apps (both demos already counted in the same-day PLM-252/254 sync below). Engine main (d9d7719 + e394984 → Block BLD engine core COMPLETE; bd8c154/88c7bde PLM-259 terrain-surface render), Hyperscale/projects/plume3d.md, Block BLD, PLM-253/259, ADR 0088)
Prior: 2026-08-06 (delta — Block BLD: Blender foliage & terrain import (engine PLM-252/254) — COMPLETE. The .blend loader now brings an artist-authored level’s ground cover and terrain in as efficient runtime geometry. No new Wren class — two additive methods on the existing BlendResult class (from Resource.loadBlend): (1) BlendResult.instantiateScatter(scene) → Num created (PLM-252, engine 0b930d5): emits the blend’s baked foliage/scatter — the Plume_Scatter Empties carrying plume3d_scatter_proto + plume3d_scatter_xforms (ADR 0087 encoding), authored in Blender with geometry-nodes / particles / collection-instances and baked — as one GPU-instanced InstancedMesh per prototype, with per-instance colour from the baked table, per-instance frustum cull + wind; returns the number of InstancedMeshes created. (2) BlendResult.instantiateTerrain(scene) → Num built (PLM-254, engine 0674003/d3bb47e): every mesh tagged plume3d_terrain (a bool/int, or a string custom property) is instantiated as a renderable ground node AND — creating the scene’s physics world if absent — given a static concave triangle-mesh Jolt collider from the same geometry; returns the number built. Coordinates convert automatically (Blender Z-up → engine Y-up). Documented alongside the existing instantiate(className, scene[, prefix]). /api/resource Updated (both methods on BlendResult). New /examples/blend-scatter-demo (apps/blend_scatter_demo — GrassField.blend’s 196 baked blades → one GPU-instanced patch; one screenshot at frame 60) and /examples/foliage-terrain-demo (apps/foliage_terrain_demo — a plume3d_terrain mesh auto-collidered + 256 baked blades; three dropped physics spheres rest on the hills, proving the collider; screenshot at frame 180). Documented WIP: the terrain surface render of the synthetic bpy.from_pydata fixture does not yet rasterize (draw submitted/un-culled/attributed yet no fragments while real Blender meshes render) — engine PLM-259, a fixture/mesh-data quirk, not a limit of instantiateTerrain; the collider is proven by the resting spheres. Landing: NEW “Blender foliage & terrain import” “What’s in place” card (new capability — the historically-skipped step, added). Also cleared the Block SPL leftover: apps/scatter_multi (the PLM-257 per-draw instance-SSBO fix — two GPU-instanced meshes rendering independently, green left / blue right; engine c934b81) had only a placeholder “no doc page yet” row from the SPL sync — now a full /examples/scatter-multi page + a real doc-status row. Security: the baked-scatter instance table + terrain tag are a .blend-parser (untrusted-input) surface, hardened at the loader (bounded instance/vertex caps, regionFits field-offset checks) in the engine tickets; /security-review folds into the Block BLD parser work engine-side. No new Wren class → 91 classes; +3 apps (blend_scatter_demo + foliage_terrain_demo + scatter_multi → 105 apps). Engine main (scatter 0b930d5, terrain 0674003/d3bb47e, multi-instance c934b81), Hyperscale/projects/plume3d.md, Block BLD, PLM-252/254/257)
Prior: 2026-08-05 (delta — Block SPL trio: surface queries + spline mesh extrusion + scatter/placement (engine PLM-247/245/246, ADRs 0083/0084/0085) — COMPLETE; Block SPL DONE. The three features that close Block SPL (splines / geometry / scatter). (1) Surface queries (PLM-247, ADR 0083): three additive static methods on the existing Physics class (no new class) — Physics.raycastDown(scene, x, y, z, maxDist) (cast straight down), Physics.groundPoint(scene, x, z) (project a column onto the nearest ground below), Physics.randomPointOnGround(scene, minX, minZ, maxX, maxZ, seed) (a seed-deterministic pick in a rect, projected down). Each returns a flat [hx,hy,hz, nx,ny,nz] List (world point + surface normal) or null. Load-bearing: they only hit registered colliders (addStaticMesh / addStaticHeightfield) — a purely visual terrain is invisible; and start just above the surface for precision (a huge start height drifts the Jolt hit). The placement primitive behind scatter + terrain-conform. No new demo app (its visual check folds into scatter_demo). (2) Spline mesh extrusion (PLM-245, ADR 0084): two additive static methods on the existing MeshGen class (no new class) — MeshGen.ribbon(spline, width, samples, conformScene) (a flat ribbon swept along a Spline; pass a Scene as conformScene and each station snaps onto the ground via PLM-247 so the ribbon hugs terrain, or null = flat) and MeshGen.tube(spline, radius, sides, samples) (a capped closed tube). Both sample the spline’s rotation-minimizing frames and return a standard Mesh. Device-free modules/geometry/extrude (links only modules/spline); conform is an injected callback the binding builds from the scene’s physics. (3) Scatter / placement (PLM-246, ADR 0085): ONE NEW frozen public Wren class Scatter of static factories that fill an InstancedMesh in C++ (never a 10k-item Wren List, no per-instance Wren callback) and return the count placed — Scatter.onSurface(mesh, scene, minX, minZ, maxX, maxZ, count, seed) (project count candidates onto the ground; natural defaults), Scatter.onSurfaceFull(…, minScale, maxScale, slopeMaxDegrees, alignToNormal, seed) (full knobs — scale range, slope rejection, upright↔slope-align), Scatter.alongSpline(mesh, spline, count, widthJitter, minScale, maxScale, seed) (line instances along a curve, jittered ±widthJitter). Load-bearing: scale is baked into the transform (mirrored in params.x — a shader must not re-apply it as scale); baseColor is left default (prototype colour shows); placement is seed-deterministic (PCG32/SplitMix64, the Random family); surface placement needs a registered collider; one GPU-instanced mesh per frame → fill one mesh from several factories. New /api/scatter page + /api/physics Updated (Surface & ground queries section) + /api/mesh-gen Updated (Spline extrusion section). New /examples/spline-mesh-demo (apps/spline_mesh_demo — a river ribbon conformed onto a heightfield + an arching tube; one screenshot at frame 60) and /examples/scatter-demo (apps/scatter_demo — ~10,600 foliage instances placed onto a heightfield and along a terrain-following path into one InstancedMesh; GPU instancing + per-instance culling + wind; one screenshot at frame 60). Landing: NEW “Scatter / Placement” “What’s in place” card (new capability), Procedural Meshes card extended (ribbon/tube spline extrusion), Physics card extended (surface/ground queries); the Scripting class list gains Scatter; API index gains Scatter (+ the previously-missing Spline row). No engine runbook this trio (ships ADRs 0083/0084/0085 only), so no guide page — like the MeshGen / Spline keystones. Security: surface-query + extrusion are pure math over app-authored numbers / registered colliders — no untrusted input, no parser, no new dependency; scatter’s core is device-free with an injected ray-down — /security-review not required (ADRs 0083/0084/0085). +1 Wren class (Scatter → 91 classes), +2 apps (spline_mesh_demo + scatter_demo → 102 apps). Engine main (surface-query 37ee4e1, extrusion 6fdda9b, scatter 01af70e → Block SPL complete 2a39638), Hyperscale/projects/plume3d.md, Block SPL, ADRs 0083/0084/0085)
Prior: 2026-08-05 (delta — Procedural mesh library: the new public Wren class MeshGen (engine ADR 0082, PLM-244, Block SPL) — COMPLETE. Part of Block SPL (splines / geometry / scatter). One NEW public Wren class in the engine module — MeshGen — a collection of static factories that build common primitives, so apps stop hand-rolling grids and boxes vertex-by-vertex. Each returns a standard Mesh (triangles, per-vertex normals + UVs, white vertex colour) you draw with Graphics.drawMesh(mesh, shader, model) or attach to a scene node: MeshGen.plane(width, depth, subX, subZ) (flat XZ plane, subX×subZ quads, faces +Y), MeshGen.grid(nx, nz, cellW, cellD) (exactly nx×nz vertices — the terrain base), MeshGen.box(sx, sy, sz) (AABB = ±(sx,sy,sz)/2, per-face flat normals), MeshGen.cylinder(radius, height, segments, caps) (about Y; caps=true adds top+bottom fans), MeshGen.cone(radius, height, segments, cap) (apex +h/2, base −h/2), and MeshGen.heightmap(heights, nx, nz, cellW, cellD, yScale) (a flat List of nx·nz samples, row-major X fastest → a displaced grid with recomputed area-weighted normals; wrong count → empty mesh). Conventions (load-bearing): every mesh is centered at the origin, Y up, with unit-length per-vertex normals, UVs in [0,1], CCW-outward winding; tangents are deferred (v1 = normals only — VertexPC has no tangent slot, so normal-mapped shaders derive TBN from screen-space derivatives); degenerate params are clamped (sub≥1, nx/nz≥2, segments≥3), never a crash. The generators come from a device-free modules/geometry core (no renderer/VM dep); the binding assembles a Plume3D::Mesh at the boundary (ADR 0082). New /api/mesh-gen page (the six factories + the centered/Y-up/unit-normal conventions + tangents-deferred) + new /examples/mesh-primitives (apps/mesh_primitives — one of each factory in a row plus a sine-wave heightmap terrain, lit N·L so the generated normals read correctly; one screenshot at frame 60). Landing Procedural Meshes “What’s in place” card added; the Scripting class list gains MeshGen. No engine runbook this sync (ships ADR 0082 only), so no guide page — like the Spline keystone / A#7/A#4. Security: pure in-process math over app-authored numbers — no untrusted input / parser / new dependency, /security-review not required (ADR 0082). +1 Wren class (MeshGen → 90 classes), +1 app (mesh_primitives → 100 apps). Engine main (commit f9e80f9), Hyperscale/projects/plume3d.md, Block SPL, ADR 0082)
Prior: 2026-08-05 (delta — Enhanced instancing: widened per-instance SSBO + per-instance frustum cull (engine ADR 0081, PLM-248, Block SPL) — COMPLETE. Additive, frozen-contract methods on the existing InstancedMesh class (no new class): addInstanceColored(x,y,z,scale, r,g,b) (add an instance carrying a per-instance base colour → index), setInstanceParams(index, scaleMul, windPhase, lodBias) (per-instance shader params — scaleMul varies height/scale, windPhase offsets wind sway so instances don’t move in lockstep, lodBias reserved for a future distance-LOD system / currently inert), and setCullingEnabled(enabled) (opt-in per-instance frustum culling — the single instanced draw submits only the instances whose world AABB is in the camera frustum), plus the cullingEnabled getter and visibleCount (instances drawn last frame — a diagnostic; equals instanceCount with culling off, drops below it as off-screen instances are removed). The GPU contract: these ride a widened 96-byte instance SSBO record [transform (64B), baseColor (16B), params (16B)] — the shader struct Plume3D_Instance in the shipped plume3d.slang ABI — reusing the existing set-2 instance buffer (no extra buffer or descriptor), with the transform at offset 0 so transform-only shaders are unaffected. New /examples/grass-field (apps/grass_field — 4096 GPU-instanced grass blades, each with its own colour + height + wind-phase, per-instance culling dropping ~1700/4096 off-screen, SSBO auto-grows 1024→4096; one screenshot at frame 80). /api/instanced-mesh gains the Per-instance colour, params & culling section + the cullingEnabled/visibleCount property rows; the landing Instanced Mesh “What’s in place” card extended (per-instance colour + height + wind sway + frustum culling). No engine runbook this sync (ships ADR 0081 only), so no guide page — like A#7/A#4. Known limit (documented): one GPU-instanced mesh per frame (the instance SSBO is written/bound at offset 0; multi-prototype scatter is a filed follow-up) — grass_field (one instanced mesh) is unaffected. No new Wren class → 89 classes; +1 app (grass_field → 99 apps). Engine main (commit cb77a13), Hyperscale/projects/plume3d.md, Block SPL, ADR 0081)
Prior: 2026-08-05 (delta — A 3D spline / curve primitive: the new public Wren class Spline (engine ADR 0080, PLM-243, Block SPL keystone) — COMPLETE. The keystone of Block SPL (splines / geometry / scatter). One NEW public Wren class in the engine module — Spline — a 3D spline you build from control points and evaluate two ways. Build: Spline.new() (empty), addPoint(x,y,z) / addPointFull(x,y,z,width) (append a control point, width for tapering ribbons), setType("catmullrom" (default, interpolating) | "bezier" (anchor,ctrl,ctrl,anchor…)), setClosed(Bool) (open or closed loop), setRoll(radians) (constant roll about the tangent). Read: pointCount / length (getters). Evaluate by parameter t∈[0,1]: position(t) / tangent(t) → an [x,y,z] number List (like Camera.getViewMatrix). Evaluate by arc-length distance s∈[0,length] (constant-speed): positionAtDistance(s) → [x,y,z], tForDistance(s) → Num. Orientation frames (mesh-extrusion input): frameAtDistance(s) → a rotation-minimizing frame as a flat 13-number List [px,py,pz, tx,ty,tz, nx,ny,nz, bx,by,bz, width] (position, tangent, normal, binormal, width), and sampleFrames(count) → a List of count such frames evenly spaced by arc length. Load-bearing notes: vectors are [x,y,z] Lists and a frame is a flat 13-number List; the parameter t is NOT constant-speed (equal t steps ≠ equal distance — use positionAtDistance for even spacing); the frames are rotation-minimizing so a swept profile never twist-flips at an inflection point. New /api/spline page (the full method surface + the vector/frame List formats + the t-vs-distance note) + new /examples/spline (apps/spline_demo — a 5-point Catmull-Rom curve drawn as a line strip at constant arc-length spacing, with sampleFrames(16) rendered as gizmos: tangent red / normal green / binormal blue; one screenshot at frame 90). Landing Geometry & Splines “What’s in place” card added; the Scripting class list gains Spline. No engine runbook this keystone (ships ADR 0080 only), so no guide page — like A#7/A#4. +1 Wren class (Spline → 89 classes), +1 app (spline_demo → 98 apps). Engine main (commit 69ecc4b), Hyperscale/projects/plume3d.md, Block SPL, ADR 0080)
Prior: 2026-08-05 (delta — Height fog (aerial-perspective blend) + fix to A#6 ambient/fog delivery (engine ADR 0079, Block RFX A#6 follow-up) — COMPLETE. One additive, engine-core, opt-in method on the existing Graphics class (no new class): Graphics.setFogHeight(baseY, falloff) — enable exponential HEIGHT fog (aerial perspective) on top of the existing setFog distance fog. Geometry below world-height baseY gains extra fog ramped by falloff per world unit, so the ground / low receding terrain hazes into the sky at the horizon with no hard floor↔sky seam; falloff = 0 turns it off (distance fog still applies). Applied as a per-fragment world-height term in the lit shader (rides the LightingData fog tail’s former pad — no UBO layout change). Author note (load-bearing): set the fog colour (setFog) to the sky horizon colour for a seamless aerial blend. Plus a behavioural FIX (not new API): in the first A#6 build setAmbient / setFog were silently inert — a renderer bug built the lit shader’s lighting UBO from the defaults, so neither knob reached the shader (this was the “dark scene”); the follow-up wires them through, so both now actually apply. Apps that never called them are byte-identical (still the 0.03 ambient default, fog off). No new page: apps/sky_demo already has an example page — it now uses setFogHeight + a real sky-like environment cubemap (its screenshots read as a proper sky with the ground blending into the horizon); its example row note updated. /api/graphics gains a setFogHeight entry (near setFog, with the fog-colour = sky-horizon note) + a “now effective” note on setAmbient/setFog; landing Rendering area’s Sky, fog & ambient card extended to mention height / aerial fog. No engine runbook this follow-up (ADR 0079 only), so no guide page — like A#7/A#4. Known limit (documented): the height term uses a per-fragment world-height weight (no camera position — the crowded-set-0 cameraPos binding fails MoltenVK MSL conversion); an exact view-ray aerial-perspective integral is a follow-up (ADR 0079 §“Why per-fragment height”). No new Wren class → 88 classes; no new app → 97 apps. Engine main (commit 57628de), Hyperscale/projects/plume3d.md, Block RFX A#6 follow-up, ADR 0079)
Prior: 2026-08-05 (delta — Planar reflection (engine ADR 0078, PLM-239) — COMPLETE. Block RFX A#4. Two additive, engine-core, opt-in methods on the existing Graphics class (no new class): Graphics.setPlanarReflection(enabled, planeY) (render the scene once more from the main camera mirrored across the horizontal plane y = planeY into a half-res reflection target — sticky state like sky/ambient, single-camera, cost = a second opaque scene draw; disabling frees the target’s VRAM) and Graphics.setPlanarReflectionScale(scale) (reflection-target size as a fraction of the swapchain, (0,1], default 0.5 = half-res). The linchpin: the reflection is exposed globally at mesh set 0 binding 7 (cloning the A#3 binding-6 machinery for a new binding) — a water/floor shader #include "plume3d.slang" (the mesh ABI) and reads plume3d_reflection(screenUv) screen-space with no per-draw setCustomTexture wiring; until enabled the binding is a 1×1 white default (inert, never a crash). Key notes (load-bearing): the reflector must be TRANSPARENT (mesh.blendMode = "alpha") so it is excluded from its own reflection — the pass renders the opaque phase only, and an opaque reflector on the plane would occlude everything above it from the mirrored camera; the reflected content should be above the plane. New /examples/planar-reflection (apps/planar_reflection_demo — a row of five coloured cubes floating above a transparent water plane at y=0 with setPlanarReflection(true, 0.0); the water samples binding 7 and shows the cubes mirrored below; one screenshot at frame 90). /api/graphics gains a Planar reflection (A#4) section (the 2 methods + the binding-7 plume3d_reflection screen-space sampling note + the transparent-reflector rule + v1 limits); landing Rendering area gains a Planar reflection “What’s in place” card. v1 limits (documented): single-camera (one reflection plane, skipped under split-screen / composite); oblique below-plane clipping of opaque geometry beneath the water is a follow-up (ADR 0078), as are a per-material winding-flip variant, an HDR reflection format, and multiple planes. No engine runbook this sync (A#4 ships ADR 0078 only), so no guide page — like A#7. No new Wren class → 88 classes; +1 app (planar_reflection_demo → 97 apps). Engine main (commit 9c6bc51), Hyperscale/projects/plume3d.md, Block RFX A#4, ADR 0078)
Prior: 2026-08-05 (delta — Script render-to-texture capture (engine ADR 0077, PLM-242) — COMPLETE. Block RFX A#7. Three additive, engine-core, opt-in methods on the existing Graphics class (no new class): Graphics.newCaptureTarget(w, h) (allocate a persistent sRGB colour capture target — returns a Texture, null on failure), Graphics.newDataCaptureTarget(w, h) (a UNORM/linear target for data / interaction / displacement maps where you don’t want sRGB), and Graphics.submitCapture(target, view, proj) (per frame in draw(), render the whole scene from a view/proj — each a List of 16 Nums, e.g. camera.getViewMatrix() / camera.getProjectionMatrix(aspect) — into the target). The linchpin: a capture is a Texture, sampled through the existing custom-material set 5 path with Mesh.setCustomTexture(slot, capture) — no new descriptor binding (it reuses the offscreen render pass + ADR 0073’s set 5). Usage notes (load-bearing): submit in draw() not update() (pending submissions clear between the two); the capture camera must be a real scene camera (Scene.addCamera — a bare Camera.new() is inert and submitCapture silently drops); allocate a target once (persistent, teardown-only cleanup — v1 has no release API, so per-frame allocation leaks). New /examples/rtt-monitor (apps/rtt_monitor_demo — a top-down ring of coloured cubes with a picture-in-picture “monitor” panel showing the same cubes from a second low-front camera, sampled via a set-5 shader; one screenshot at frame 90). /api/graphics gains a Render-to-texture capture (A#7) section (the 3 methods + the set-5 sampling note + v1 limits); landing Rendering area gains a Render-to-texture capture “What’s in place” card. v1 limits (documented): single-camera (one capture per frame, skipped under split-screen / RTT-composite); submitCapture records the whole scene including any screen-space overlay (a monitor in the scene it captures shows the expected live video-feedback recursion) — a drawSubset filter is a follow-up (ADR 0077). No new Wren class → 88 classes; +1 app (rtt_monitor_demo → 96 apps). Engine main (commit 85fc234), Hyperscale/projects/plume3d.md, Block RFX A#7, ADR 0077)
Prior: 2026-08-05 (delta — Sky pass + atmospheric fog + ambient + environment cubemap (engine ADR 0076, PLM-241) — COMPLETE. Block RFX A#6. Four additive, engine-core, opt-in atmosphere knobs (raw-linear, policy-free — the time-of-day/weather curve is pack Wren). New methods on the existing Graphics class (no new class): Graphics.setAmbient(r,g,b) (raw-linear ambient fill — replaces the old hardcoded 0.03 shader constant; default 0.03 so unset scenes are pixel-identical; LightingData UBO tail, set 0 b1), Graphics.setFog(r,g,b,density,start,end) (atmospheric distance fog — density>0 = exponential exp2(-density·dist), else linear over [start,end]; applied as a lit-shader term so it composes with every path), Graphics.setSky(shaderName, mode, params) (a full-screen sky drawn first at z=far with depth off; mode 0 = procedural gradient, mode 1 = env cubemap; params a List of ≤8 Nums, the demo’s gradient reads params[0..2] horizon + params[4..6] zenith; the sky shader #includes plume3d_sky.slang, the sky ABI, NOT the mesh plume3d.slang), and Graphics.setEnvironmentMap(handle) (points the mode-1 cubemap sky at a cube handle). Plus a new static on the existing Texture class: Texture.loadCubemap(paths) — 6 face images in order +X,-X,+Y,-Y,+Z,-Z (all the same square size), returns a cube handle Num (0 on failure). Disambiguation, load-bearing: setFog is ATMOSPHERIC distance fog — DISTINCT from the gameplay fog-of-war (Graphics.fogSoftness / FogOfWar.*, ADR 0067); the API/guide/example all call this out so pack authors don’t conflate them. New /examples/sky-demo (apps/sky_demo — gradient sky → cubemap sky, ambient, distance fog, a diagonal row of lit cubes receding into the fog; two screenshots) + a new /guides/sky-fog-ambient how-to (translated from the engine runbook docs/runbooks/sky-fog-ambient.md). /api/graphics gains a Sky, fog & ambient (A#6) section (the 4 methods + the fog-of-war disambiguation + v1 limits); /api/texture gains a Cubemaps section (loadCubemap); landing Rendering area gains a Sky, fog & ambient “What’s in place” card. Known limits (documented): the cubemap sky renders, but lit-shader IBL/reflections (sampling the env cube from a mesh shader) is a follow-up — a combined SamplerCube in the crowded mesh set-0 fails MoltenVK MSL conversion (ADR 0076 §5); and sky/fog reach the single-viewport post + plain paths in v1 (fog-of-war/composite/split-screen scenes get fog but not the sky pass yet). No new Wren class → 88 classes; +1 app (sky_demo → 95 apps). Engine main (commit 6ab9864), Hyperscale/projects/plume3d.md, Block RFX A#6, ADR 0076)
Prior: 2026-08-03 (delta — Opaque colour capture for transparent refraction (engine ADR 0075, PLM-238) — COMPLETE. Block RFX A#3. Additive, engine-core, opt-in. New method on the existing Graphics class (no new class): Graphics.opaqueCaptureEnabled(enabled) — when true, the offscreen scene render splits opaque → copy → transparent so a transparent mesh (water/glass, blendMode = "alpha") can sample the captured OPAQUE scene behind it and distort it (refraction). The capture is read through the mesh ABI (#include "plume3d.slang" — NOT the post ABI) at mesh set 0 binding 6: plume3d_opaqueColor(screenUv), a combined Sampler2D read with .Sample() at a screen-space UV (offset it to refract). Requires the offscreen path — pair it with at least one Graphics.addPostEffect (a passthrough is fine), single-camera; without a post effect binding 6 stays the 1×1 white default and the surface reads white (no refraction) — inert, never a crash. New /examples/refraction (apps/refraction_demo — four opaque colour bands behind one transparent quad; the band boundaries are straight outside the quad and wavy inside it, proving the opaque phase is captured and reaches transparent draws) + a new /guides/opaque-color-refraction how-to (translated from the engine runbook). /api/graphics gains an Opaque colour capture (A#3) section (opaqueCaptureEnabled + the plume3d_opaqueColor mesh-ABI binding 6); landing Rendering area gains a Transparent refraction (opaque capture) “What’s in place” card. No new Wren class → 88 classes; +1 app (refraction_demo → 94 apps). Engine main, Hyperscale/projects/plume3d.md, Block RFX A#3, ADR 0075)
Prior: 2026-08-03 (delta — Full-screen post-process effects + scene depth (engine ADR 0074, PLM-240/237) — COMPLETE. Additive, engine-core, opt-in. New methods on the existing Graphics class (no new class): Graphics.addPostEffect(shaderName, params) (per-frame, called in draw() like a draw call — shaderName is a shader from Graphics.loadShader, params a List of up to 16 numbers) and Graphics.clearPostEffects(). When at least one post effect is added, the engine renders the scene to an offscreen colour target plus a sampleable depth target, then runs the post shader full-screen — sampling scene colour + scene depth + the frame clock + params — into the swapchain; with no effects added the normal path is unchanged. A new post-shader ABI include ships at engine engine/shaders/include/plume3d_post.slang (the post ABI — NOT the mesh plume3d.slang): a post shader #include "plume3d_post.slang" and provides only [shader("fragment")], reading plume3d_sceneColor(uv) / plume3d_sceneDepth(uv) (0 near..1 far, texel .Load) / plume3d_time() / plume3d_postParam(i); the engine binds sceneColor/sceneDepth/sampler/frame/params at set 0 bindings 0..4. v1 limits: a single effect, an LDR (B8G8R8A8_SRGB) intermediate, single-camera only (skipped when fog-of-war / split-screen / RTT composite owns the frame). New /examples/post-depth (apps/post_depth_demo — quads at different depths read back as distinct grays via a depth-visualizing post effect, proving the scene renders offscreen and its depth is sampleable) + a new /guides/post-effects-and-scene-depth how-to (translated from the engine runbook). /api/graphics gains a Post-process effects (A#5/A#2) section (methods + the post ABI + scene depth). No new Wren class → 88 classes; +1 app (post_depth_demo → 93 apps). Engine main (commit 3c544a2), Hyperscale/projects/plume3d.md, Block RFX A#5/A#2, ADR 0074)
Prior: 2026-08-03 (delta — Guides: two task-focused how-to pages for the new rendering primitives (engine ADR 0072/0073). A new Guides sidebar section (Starlight autogenerate over src/content/docs/guides/) that translates the engine RUNBOOKS into user-facing how-tos: /guides/frame-uniform-and-wind (animate any shader from the global frame clock + Graphics.setWind) and /guides/custom-material-packs (bind your own textures + params at set 5 to ship a content-pack shader). Each keeps the runbook’s step shape (include plume3d.slang → shader code → Wren → build → verify) and cross-links to the matching /api/graphics / /api/mesh sections + the /examples/wind-sway / /examples/custom-material-pack demos, and to each other (a real animated pack shader uses both). Docs-only, no engine/API change → 92 apps / 88 classes unchanged; +2 doc pages (new Guides — Status table below). Follows the 2026-08-03 API/example sync below. Engine-side runbooks docs/runbooks/frame-uniform-and-wind.md + docs/runbooks/custom-material-packs.md)
Prior: 2026-08-03 (delta — Extensible custom materials (set 5) + a global frame/wind uniform (engine ADR 0073/0072, PLM-234/236) — COMPLETE. Two additive, frozen-contract rendering primitives, both authored against a new shared shader include #include "plume3d.slang" (the canonical Plume3D shader ABI, shipped at engine/shaders/include/plume3d.slang, which names the engine’s set/binding layout once so a pack shader doesn’t copy-paste it). (1) Block MAT — extensible custom-material set 5 (ADR 0073, PLM-234): the linchpin that lets a content pack’s Slang shader bind its own textures + params with no engine change — before this the descriptor ABI was hardcoded (the fixed six-map PBR set) and a shader could not add a texture. New methods on the existing Mesh class (no new class): setCustomTexture(slot, texture) (a loaded Texture → custom slot 0..7), setCustomParamFloat(byteOffset, value) / setCustomColor(byteOffset, r,g,b,a) / setCustomParams(list) (write into a 256-byte params blob), and clearCustom(). The engine binds them at set 5: binding 0 = Texture2D customTextures[8], binding 1 = one shared customSampler, binding 2 = a per-draw 256-byte CustomParams UBO (16×float4, byteOffset/16 = index); a shader reads plume3d_customTex(slot, uv) / plume3d_customParam(index). Bounds are enforced engine-side (slot 0..7, params clamped to 256 bytes). New /examples/custom-material-pack (apps/custom_material_pack — a pack shader binds a gradient ramp + a scrolling flow map + a tint + a scroll speed via set 5; screenshot-verified: a neutral grayscale ramp renders as a warm-tinted gradient, proving both custom textures AND params reach the shader). (2) Block RFX A#1 — global frame-uniform block (ADR 0072, PLM-236): the universal prerequisite for animated shaders (wind sway, water waves, moving clouds). A global block at set 0, binding 4: {time, delta, frameIndex, wind} (time/delta/frameIndex host-owned; wind app-set). New method on the existing Graphics class (no new class): Graphics.setWind(x, y, z, strength) (direction need not be normalized; strength scales it), read by shaders via plume3d_time() / plume3d_windDir() / plume3d_windStrength(). New /examples/wind-sway (apps/wind_sway_demo — a shader sways + colour-pulses with plume3d_time(); two screenshots at different frames differ, proving the frame clock reaches the GPU). /api/mesh gains a Custom material (set 5) section + the set-5 binding contract; /api/graphics gains a Global frame uniforms section + setWind; landing Rendering area gains an Extensible materials & animated shaders “What’s in place” card. No new Wren class → 88 classes; +2 apps (custom_material_pack + wind_sway_demo → 92 apps). Engine-side Hyperscale/projects/plume3d.md, Block MAT + Block RFX, ADR 0072/0073)
Prior: 2026-08-02 (delta — A Blender-authored bone-parented MESH now bone-follows the animated skeleton (engine PLM-225, ADR 0069) — COMPLETE. PLM-222 placed a bone-parented mesh at its authored REST pose (following the armature object); only the combat sensor tracked an animated bone. The visible mesh now bone-follows too — when an attack clip swings the arm, the sword swings with it, exactly where its sensor is — automatic, engine-driven, no Wren code and no attachToBone call: at instantiate each bone-parented mesh child auto-registers with its authored bone offset, and every LateUpdate the engine sets its local from the character’s posed ArmatureState (armature-local bone × offset, so a child-of-armature mesh’s world = armatureNode.world × posedBone × offset, exactly the combat sensor’s world). At bind pose it reduces to the PLM-222 rest placement → no rest regression. No Wren surface change — the frozen Combat/Hitbox/Hurtbox shape is identical; only the previously-documented “a Blender-authored mesh follows the armature object, not yet an animated bone” caveat is now obsolete. /api/combat + /examples/combat-blender-demo note the visible mesh follows. End-to-end test test_combat_bonefollow (a Character.blend-derived rig plays a clip; the bone-parented sword’s distance to its bone head stays constant as the bone swings AND both move — with the rig at a non-identity world so it guards the child-of-armature double-transform). Root cause fixed along the way: posed bone world matrices refresh only at draw, so the follow recomputes them each LateUpdate (also removing a one-frame lag). No new class → 88 classes; no new app → 90 apps. Suite 586/586, 0 warnings under -Werror. Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0069)
Prior: 2026-08-02 (delta — Combat.onExit fires on explicit box removal, not only natural separation (engine PLM-223, ADR 0069) — COMPLETE. Closes a documented combat gap. Detection is Jolt’s contact queue, so onExit came from a deferred OnContactRemoved — but a box remove()d/GC’d while still overlapping loses its Jolt body first, and the queued removal is dropped (the partition sees one combat body), so no onExit fired for a pair torn down explicitly. The engine now synthesizes it: each box tracks its live overlap set (maintained on the raw contact Begin/End), and remove() queues an onExit for every partner still overlapping, drained on the next combat dispatch. Deduped — a separated pair already cleared its set, and removing both sides severs the reverse link, so exactly one onExit fires per pair; scene teardown synthesizes none. No Wren surface change — the frozen Combat.onExit is identical; the /api/combat contract note is corrected (“fires on natural separation AND explicit removal”). Device-free regressions in test_combat_hitbox (remove mid-overlap → one onExit; separate-then-remove → no duplicate). No new class/app (90 apps / 88 classes). Suite 585/585, 0 warnings under -Werror. Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0069)
Prior: 2026-08-02 (delta — Blender-authored combat volumes ride their bones at the authored offset (engine PLM-222, ADR 0069) — COMPLETE. Closes the last combat follow-up. A bone-parented Blender object (a sword mesh + its hitbox) was landing at the armature origin, not on its bone: Blender parents an object to the bone tail and — since Blender 5.0 no longer serializes the object world matrix (it moved to a runtime-only struct, recomputed on load) — the parser’s parent-relative transform dropped the bone offset, so the visible mesh AND the combat volume both sat at the origin. BlendResult.instantiate now reconstructs the authored placement from the serialized bone rest + bone length (boneTail = boneRest × Translate(0,0,-boneLength)) × the parent-relative transform: the mesh node is re-placed on its bone, and the combat volume carries a bone-local localOffset so — once bindController is called — the sensor rides the bone at the same spot and bone-follows animation. No Wren surface change — the frozen Combat/Hitbox/Hurtbox/bindController shape is identical; only the previously-documented “a volume rides its bone origin, not the authored offset (PLM-222)” caveat is now obsolete. /examples/combat-blender-demo now binds a controller so its volumes ride the bones — render-verified: the sword sits on the hand and both strikes land. Tests: test_combat_hitbox (device-free bone-offset composition, incl. a rotated + a posed bone), test_combat_wren (end-to-end: two Fighters, bind controllers, lunge, a bone-attached sword strikes the body). No new class → 88 classes; no new app → 90 apps. Suite 582/582, 0 warnings under -Werror. Follow-up: a Blender-authored mesh follows the armature object, not yet an animated bone (the combat volume already does). Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0069)
Prior: 2026-08-02 (delta — Fast swings no longer tunnel: swept anti-tunnel for combat hitboxes (engine PLM-221, ADR 0069) — COMPLETE. Closes the one documented gap in the sensor-based combat model. Jolt sensor contacts are discrete, so a hitbox moving farther than its own thickness in a single frame (a very fast attack or a frame-rate spike) could skip clean past a thin hurtbox. The engine now closes that automatically: after the per-frame sensor pass, CombatScene::sweepFastHitboxes sweeps each such fast hitbox from its previous pose to its current one (PhysicsWorld::shapeCastVsShape, no bodies) against the active hurtboxes and reports the crossings the discrete pass missed — sharing the same once-per-swing dedup (extracted into registerStrike), so a swept catch and a sensor catch of one crossing still fire a single onHit. Ordinary bone-driven swings (cm/frame) never trip it, so it costs nothing on normal frames; it’s always on, no opt-in. No Wren surface change — the frozen Combat/Hitbox/Hurtbox shape is identical; only the previously-documented “fast attacks can tunnel (PLM-221)” caveat is now obsolete. /api/combat caveat rewritten (“Fast swings don’t tunnel”). Device-free regressions in test_combat_hitbox (a hitbox teleported across a thin wall in one frame lands exactly once; a slow approach is left to the sensor — no double count). No new class → 88 classes; no new app → 90 apps. Suite 579/579, 0 warnings under -Werror. Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0069)
Prior: 2026-08-02 (delta — On-screen fog of war + fog ownership (engine PLM-210, Block LSV) — COMPLETE. The last LSV piece: the fog-of-war field is now rendered on-screen, not just queryable data + a HUD. A depth-based post-process reconstructs each pixel’s world position (from the scene depth × the inverse view-projection, exactly like Camera::screenToWorld) and darkens the lit world by the 3-state field — Unknown → near-black, Explored → dim + desaturated, visible → full colour — with no game render code; it runs automatically for the scene whose camera drives the view once the game has configured + revealed. Because it reconstructs from depth, it is pixel-exact at any camera angle (render-verified top-down AND oblique — no smear on tall geometry). New render dial Graphics.fogSoftness(v) — 0 = crisp retro cells, 1 = a smooth feathered fade (LINEAR mask + eased smoothstep), default 0.6; render-only, so the frozen FogOfWar.* gameplay surface is unchanged. No new Wren class (a method on the existing Graphics). Also the fog ownership refactor: the per-scene FogState was a wren_api static map that never freed on scene teardown (a leak) — now WrenVm-owned (a per-scene registry with weak_ptr pruning + shutdown clear, mirroring combatScenes_). New /examples/fog-demo (apps/fog_demo — a top-down scene, the player wanders, the engine darkens by the field; render-verified). Landing Light, Sound & Vision card updated (dropped the “still landing: on-screen fog render” clause). The fog renderer allocations were audited for exact create↔destroy pairing (owner rule — no leaks) + an adversarial memory-safety review. +1 app (fog_demo → 90); no new class → 88 classes. Suite 576/576, 0 warnings under -Werror. Block LSV fully COMPLETE. Engine-side Hyperscale/projects/plume3d.md, Block LSV + PLM-210)
Prior: 2026-08-01 (delta — Blender-authored combat on Jolt sensor bodies (engine PLM-198, ADR 0069) — COMPLETE. The combat substrate’s detection is re-architected onto Jolt sensor bodies (supersedes ADR 0066’s shape-cast detection; the “engine detects, game owns health” boundary + opaque payload are unchanged). A hit/hurt box is now a kinematic Jolt sensor riding its bone; Jolt reports the overlap and the engine partitions the contact drain into combat events. Hit/hurt geometry is authored in Blender — a mesh given a Passive+“Animated” rigid body, bone-parented, placed in a dedicated Plume_Hitboxes / Plume_Hurtboxes collection (object name = tag); blend_load surfaces bounded BlendCombatVolumes that instantiate turns into bone-attached sensor boxes. No NEW Wren class — additive methods on existing classes: Combat.onStay/onExit (lifecycle beside onHit), hitbox.independent/reHitSeconds (opt-out of per-owner swing-dedup / re-strike a persisting overlap — flurry & DoT), hitbox.bindController/hurtbox.bindController (link a Blender-authored box to the character’s AnimController), node.hitboxes/node.hurtboxes (tag→box maps), node.attachToBone/detachFromBone (engine-driven weapon/VFX socket). A Plume3D Blender add-on Combat Volumes panel one-clicks the authoring convention. /api/combat rewritten for the sensor model + Blender authoring; new /examples/combat-blender-demo (apps/combat_blender_demo — two Fighters with Blender-authored volumes, a lunge lands via Combat.onHit; render-verified 2 strikes); /examples/combat-demo reconciled (hand-built boxes, sensors). Landing Action Combat card updated. The .blend combat parse is a NEW untrusted-input surface → blocking /security-review PASSED (bounded volume/hull caps + field-offset checks; zero confirmed findings). Caveats: Jolt sensors have no CCD, so a one-frame teleport past a thin target can tunnel (realistic bone-swings caught frame-by-frame; Physics.castShape stays the swept one-shot) — tracked PLM-221; a volume rides its bone origin, not the authored mesh offset — PLM-222. Regression fixed: combat sensor bodies are now excluded from Physics.* world queries (a bullet raycast must not hit an invisible hitbox) — test_combat_queries. Headless tests: test_combat_hitbox (sensor mechanism), test_combat_wren (lifecycle/attachToBone/instantiate/independent/reHitSeconds), test_blend_load (combat-volume parse), test_combat_queries (query-exclusion). No new Wren class → 88 classes; +1 app (combat_blender_demo → 89). Suite 572/572, 0 warnings under -Werror. Block CMB — Blender-authored hitboxes COMPLETE on feat/blender-hitboxes. Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0069)
Prior: 2026-08-01 (delta — Data-driven behaviour-tree authoring for the GUI editor (engine PLM-211…219, ADR 0068) — COMPLETE. A GUI can’t draw a Wren closure, so a data-authored tree references named leaves + typed params; the engine runs the tree, the game owns HP/phase policy (like combat + LSV). Additive to ADR 0065 (the Bt.leaf closure surface is unchanged). Bt.registerLeaf(name, paramSpec) { |bb,dt,params| } — a named leaf the editor lists + a .bt.toml references (arity-3; params is a bound Map). Bt.leafCatalog — the registered names + schemas (the editor palette). Bt.load(path) — build an engine-ticked BehaviourTree from a hardened, bounded, cycle-checked .bt.toml (unknown-leaf / bad-param / cycle → Bt.loadError, tree left intact). New 1 Wren class PhaseEvaluator.evaluate(currentPhase, health01, elapsed, thresholds) — a forward-only boss-phase index the game feeds health01 into (the engine never reads HP) + writes to a blackboard int a phase_switch node reads; a mini-boss is 2 phase entries, a boss 4+, same data machinery. New /api/behaviour-tree-authoring page + /examples/bt-editor (apps/bt_editor — palette from the live catalog, canvas with node drag, a schema-driven param inspector, a PhaseSwitch node, subtree delete, save→.bt.toml with a Bt.load round-trip validation, and a live Run 1 tick preview — the richer UX is PLM-218) + /examples/bt-boss-demo (apps/bt_boss_demo — a phaseless enemy, a 2-phase mini-boss, a 4-phase boss, all one schema differing only in data; PLM-219). Landing Behaviour-Tree Authoring card added; Scripting class list +1. The .bt.toml loader is a NEW untrusted parser → blocking /security-review PASSED (injection-proof named-leaf model, atomic apply, TOML_EXCEPTIONS 0). Headless tests: test_ai_behaviour_tree (Parallel/Succeeder), test_ai_phase, test_bt_asset, test_bt_toml (malicious corpus), test_bt_load_wren (registerLeaf + load + params flow + GC-safety + phase_switch). +1 Wren class (PhaseEvaluator → 88), +2 apps (bt_editor + bt_boss_demo → 88). Suite 566/566, 0 warnings under -Werror. Block BTA COMPLETE (PLM-211…220) on feat/bt-authoring. Engine-side Hyperscale/projects/plume3d.md, Block BTA + ADR 0068)
Prior: 2026-08-01 (delta — Light, Sound & Vision as mechanics (engine PLM-199/203/206, ADR 0067) — IN PROGRESS. The Hadal P1 identity pillar: the engine SAMPLES, the game OWNS the policy (stealth, HP, detection — no rules ship in the engine). 3 new Wren classes + 1 method, over device-free unit-tested cores. Illumination.illuminanceAt(scene, x,y,z, mask) → a continuous 0..1 “how lit is this point” (per-light falloff matching the render — reads-lit == looks-lit — with a masked-LOS occlusion; no binary helper, the game thresholds it; bidirectional — your lamp lights you). FogOfWar — a world-XZ visibility field: configure bakes an occluder grid from static bodies, reveal(x,z,radius) (radius scaled by illuminance = darkness shrinks your view), state→0/1/2, persistent Explored, both team + per-player, + a wall-penetrating sonar channel (mark/tick/isRevealed). NoiseBus.emitOccluded(scene, …, mask) — a wall between a sound and a listener blocks it (the same wall muffles audio + shrinks what the AI hears). SoundEmitter — unifies the audible (a Source) + audible-to-AI (a NoiseBus) sides. New /api/light-sound-vision page + /examples/lsv-demo (apps/lsv_demo — a player crosses light/shadow while a HUD meters EXPOSURE + NOISE). Landing Light, Sound & Vision card added; Scripting class list +3. Headless Wren tests: test_illumination/_wren, test_visibility_field/test_fog_wren, test_audio_occlusion, test_noise_occlusion_wren. +3 Wren classes (Illumination/FogOfWar/SoundEmitter → 87), +1 app (lsv_demo → 86). Suite 546/546, 0 warnings under -Werror. Also Source.setOcclusion(gain, gainHF) — the audible muffle (the same wall that shrinks AI hearing muffles the audio; broadband gain + a per-source EFX low-pass). DONE since: PLM-202 (audio muffle) + PLM-207 (long-range — satisfied by architecture: audio sources are render/stream-independent). STILL LANDING (tracked PLM-210, renderer/refactor, no Wren change): the fog-of-war on-screen render composite (fog is queryable data + HUD today, not yet a world-space screen overlay) + the fog WrenVm-ownership refactor. Engine-side Hyperscale/projects/plume3d.md, Block LSV + ADR 0067)
Prior: 2026-08-01 (delta — Action combat: hit detection, the game owns health (engine PLM-196, ADR 0066). The action-combat substrate for Block CMB (Hadal). 5 new Wren classes in the engine module — the engine DETECTS hits, the game OWNS all health. Shape (capsule/box/sphere — immutable, reused by the box factories + queries). Hitbox / Hurtbox — bone-attached boxes (capsule/box/sphere(node, controller, bone, …)) with .layer/.tag/.owner/.payload/.enabled; a hitbox never strikes its own owner, strikes each victim once per swing, and Hitbox.bindWindow(controller, name) gates it to an animation hit-window (authored as Blender pose-markers). A disabled hurtbox is i-frames. Hit (read-only: attacker/victim/hitboxTag/point/normal/payload). Combat — Combat.onHit(scene, fn): the engine resolves overlaps each frame (swept, so a fast attack can’t tunnel a thin target) and calls fn(hit) at the tail of update() — no game-called tick (it would re-enter the Wren VM). There is deliberately no HP/damage/death type in the engine — the opaque payload (a game number set on the hitbox, carried into the Hit untouched) is the only wire across the health boundary; what a hit costs is 100% the game’s Wren. Plus 3 swept/overlap/pierce queries on Physics: castShape (swept, all bodies crossed), overlapShape (at-pose narrow-phase), raycastAll (piercing) → a List of RaycastHit. New /api/combat page + /examples/combat-demo (apps/combat_demo — a sword hitbox lunges at a body hurtbox; Combat.onHit reads the payload as damage off a plain Wren HP field; death disables the hurtbox). Landing Action Combat “What’s in place” card added; the Scripting class list gains the 5 classes. Six headless Wren tests (test_combat_wren) self-validate strike/dedup/i-frames/owner-layer/swept/queries/GC-safety. +5 Wren classes (Shape/Hitbox/Hurtbox/Hit/Combat → 84), +1 app (combat_demo → 85). Suite 524/524, 0 warnings under -Werror. Engine-side Hyperscale/projects/plume3d.md, Block CMB + ADR 0066)
Prior: 2026-07-31 (delta — Scriptable AI: behaviour trees + decision/FSM + noise (engine PLM-189, ADR 0065). A game can now author enemy AI in Wren (not just script it imperatively) — the follow-up ADR 0057 explicitly deferred. 19 new Wren classes in the engine module, thin bindings over the tested modules/ai cores. Behaviour tree — BehaviourTree (new(), setRoot(node), enabled=, blackboard(); the engine ticks it — no tree.tick, because a game-called tick would re-enter the Wren VM, which is forbidden), Bt factories (sequence/selector/inverter/repeat/cooldown/leaf, leaves are closures { |bb, dt| … } → BtStatus), BtNode (opaque, consumed-on-use), Blackboard (typed bool/int/number/string store). Decision / memory — LostTargetTracker lost→search→give-up FSM (+TargetState/LoseTargetMode/GiveUpMode), AggroTable (+static shouldSwitchFocus commitment gate), Tactic distance-band selection, StatGate (+GateWhen/GateEffect), TensionModel L4D pacing director (+TensionPhase, +static threatScoring). Noise — NoiseBus detect-by-sound (+static isAudible min-of-radii), AlertMemory, PainMemory. Plus two methods on the existing Ai class: Ai.nearestVisibleTarget(...) (range + FoV + native masked-raycast LOS + nearest, in one call — no Wren callback, so it’s leaf-safe) and Ai.tickInterval (the engine’s tree cadence; 0 = per-frame, default 10 Hz). /api/ai extended with all of it; /examples/ai-hunters reworked to drive the full loop from one Wren tree (perceive by sight+sound → navmesh-chase to a ring slot + tactic → lose → search last-known → give up; render-verified, hunters coloured by FSM state). Landing AI & Navigation “What’s in place” card updated; the Scripting class list gains the new classes. Three headless Wren tests (test_ai_bt_wren, test_ai_decision_wren, test_ai_noise_wren) self-validate the surface. 79 Wren classes (was 60 — +19), 84 apps (unchanged — ai_hunters reworked, not added). Suite 501/501, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Block AI + ADR 0065)
Prior: 2026-07-31 (delta — Split-screen: per-viewport HUD overlays (PLM-190, CAM-18, ADR 0064). Additive to the existing Graphics (no new class; +1 app, split_hud_demo → 84 apps). Graphics.perViewportOverlays(on) — when set and the frame has split-screen viewports, the retained-UI HUD is replayed into each cell (scissored) instead of drawing once full-screen, so a per-player HUD (health bar, score) stays inside its own region and never spills across a divider. The renderer builds the UI batches once, then replays them into each viewport’s sub-rect (a sub-rect VkViewport scales the full-window HUD in + a matching scissor clips it). Render the HUD once, full-window; the engine handles the per-cell replay. New /examples/split-hud (apps/split_hud_demo — a 4-way grid where a .ui HUD appears in every cell, clipped; render-verified). /api/graphics gains the perViewportOverlays method. Scope: screen-space retained UI only (world-space sprites / world text are scene content; screen-space SDF text per viewport is a follow-up). No new Wren class → 60 classes; 84 apps (was 83 — +split_hud_demo). Suite 495/495, 0 warnings. Completes the PLM-190 remaining-items sweep (Node.setQuat, per-player input, per-viewport HUD). Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0064)
Prior: 2026-07-31 (delta — Split-screen: per-player input routing (PLM-190). Additive to the existing SplitScreen (no new class/app). SplitScreen.perPlayerInput(on) (default on) — player i’s camera reads gamepad i (right stick = look, left stick + triggers = free-fly move) instead of the shared mouse/keyboard, so each split-screen player drives their own view; falls back to mouse/keyboard for any player whose gamepad is not connected, and off shares the input. Backed by CameraDirector.setPlayerIndex. /api/camera SplitScreen gains the perPlayerInput row. test_split_screen verifies the per-player routing + toggle. No new Wren class → 60 classes; no new app → 83 apps. Suite 495/495, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, PLM-190)
Prior: 2026-07-31 (delta — Node: setQuat — exact, gimbal-free rotation (PLM-190, ADR 0063). Additive to the existing Node (no new class, no new app). Node.setQuat(x, y, z, w) sets a node’s local rotation as a quaternion — exact at every orientation, including ±90° pitch where Euler setRotation hits gimbal lock. The camera system writes it (CameraDirector write-back), so Camera.worldToScreen is precise for first-person / top-down rigs; any Euler setter reverts the node to Euler mode. The quat→matrix convention matches modules/camera’s mat4FromQuat, so the node’s world matrix agrees with the rendered view. /api/node gains the setQuat row. test_camera_math verifies the −90°-pitch case (matrix basis == mat4FromQuat, forward == (0,−1,0), Euler-setter revert). No new Wren class → 60 classes; no new app → 83 apps (both unchanged). Suite 494/494, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0063)
Prior: 2026-07-31 (delta — Camera: raw Viewport primitive — picture-in-picture / minimap (PLM-190, CAM-19, ADR 0062). One new public Wren class in the engine module — Viewport — a first-class composite viewport (a screen region + its own camera) for picture-in-picture / minimap / security-cam, independent of SplitScreen, over the RTT composite path (ADR 0061). Viewport.new(scene) + fluent setters: rect(x,y,w,h) (screen fractions), z(order) (higher = on top; the main Cam view sits behind), opacity(a), border(width), borderColor(r,g,b), and a camera — follow(target) / topDown(target, height) (minimap) / orbit(target, radius) / firstPerson(head) / freeFly(). Each Viewport owns its own camera director; WrenVm::tickCameras folds the main Cam view in as a z-behind fullscreen base and composites the Viewports on top (compositeViewports=true). A Viewport that goes out of scope finalizes + stops rendering. New /examples/pip (apps/pip_demo — third-person main + a top-down minimap; render-verified: the minimap shows the four corner landmarks + the centred player). /api/camera gains a Viewport section; landing Camera card + the Scripting class list gain Viewport. test_camera_wren gains a Viewport case (every setter + each camera preset + the GC/finalizer path, 40 frames). 60 Wren classes (was 59 — +Viewport), 83 apps (was 82 — +pip_demo). Suite 493/493, 0 warnings. Completes PLM-190 (animated merge → RTT composite + angled → rect-peel → PiP). Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0062)
Prior: 2026-07-31 (delta — Split-screen: animated rect-PEEL merge (PLM-190, ADR 0061). Additive to the frozen SplitScreen — no new class; +1 app (peel_demo → 82 apps). Over the RTT composite path (ADR 0061): SplitScreen.peel(on) makes the dynamic merge slide each viewport’s rect from its split cell toward fullscreen (z-stacked, the “views expand together” Kronnect look) instead of the default fixed-rect camera-converge blend — the rects overlap mid-slide, which the axis-aligned path can’t do, so peel forces composite while a merge is in flight. Each camera still converges to the group pose, so the overlapping slides read as one coherent merge. New /examples/peel (apps/peel_demo — 2 players; panels slide together to a merged view; render-verified). /api/camera SplitScreen gains peel(on); landing Camera card notes it. test_split_screen gains a peel case (mid-merge the cells are wider than the base half + z-stacked + composite forced). 59 Wren classes, 82 apps (was 81 — +peel_demo). Suite 492/492, 0 warnings. Remaining PLM-190: PiP / raw Viewport primitive. Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0061)
Prior: 2026-07-31 (delta — Split-screen: RTT composite path + angled (diagonal) 2-player split (PLM-190, ADR 0061). The v1 axis-aligned sub-rect path structurally can’t do a diagonal divider (a half-plane isn’t a rectangle), overlap, or picture-in-picture. Add an opt-in RTT composite path: each viewport renders to its own offscreen colour target (an offscreen render pass render-pass-compatible with the mesh pipelines, so they’re reused unchanged), then the renderer composites the targets as arbitrary polygons. Additive to the frozen SplitScreen: compositeMode(on) forces it (the composited disjoint split is pixel-equivalent to the fast path — always safe), and a new SplitLayout.angled — a Kronnect-style 2-player diagonal split whose divider is perpendicular to the on-screen direction between the players (it rotates as they move around each other); each player renders full-screen and the two complementary half-planes are composited (Sutherland–Hodgman clip), auto-enabling compositeMode. New /examples/angled-split (apps/angled_split_demo — 2 players orbiting, the diagonal divider rotating; render-verified sky/horizon/players undistorted). /api/camera SplitScreen + SplitLayout updated; landing Camera card notes angled split. test_split_screen gains an angled-layout case (complementary half-plane polygons, full-screen UV, one divider). 59 Wren classes (no new class — additive methods on SplitScreen + a SplitLayout constant), 81 apps (was 80 — +angled_split_demo). Suite 491/491, 0 warnings. The RTT path also unlocks the still-ahead animated rect-peel + PiP / raw Viewport primitive. Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0061)
Prior: 2026-07-31 (delta — Split-screen: animated merge/split — camera-converge, no RTT (PLM-190, ADR 0060). Additive to the frozen SplitScreen (ADR 0059) — no new class, no new app. The dynamic merge/split is no longer a hysteresis hard-switch: it is an animated blend over a transitionDuration. Two new members on SplitScreen: transitionDuration(s) — seconds the merge↔split blend takes (0 = the old instant switch; default 0.4) — and mergeFraction (Num getter, 0..1: 0 split → 1 merged, the animation progress; isMerged still reports the settled target). During the blend the split cells are held in place while each cell’s camera eases from its player pose toward the shared group pose (smoothstep) and the dividers fade, then the cells collapse to one fullscreen group viewport — and because every cell converges to the same group pose, they show identical pixels at collapse, so there is no overlap artefact and no render-to-texture. /api/camera SplitScreen methods + scope note updated; /examples/split-screen reworked (single sweep grid→centre; new split_transition mid-blend screenshot). The Kronnect rect peel + PiP/overlap + raw Viewport primitive still need RTT (the next PLM-190 unit). Headless test_split_screen gains the animated-blend + transitionDuration(0) back-compat cases. 59 Wren classes, 80 apps (both unchanged — additive methods on an existing class + demo). Suite 490/490, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, PLM-190 + ADR 0060)
Prior: 2026-07-31 (delta — Camera: occlusion collider + split-screen with dynamic merge/split (Camera epic, ADR 0059). Two capabilities on top of the rig system. Occlusion collider — additive setter CameraRig.collider(padding): the camera pulls in front of any wall that would occlude the target (masked physics raycast, ADR 0056), keeping padding clearance; new /examples/camera-collider (render-verified: wall occludes vs. camera pulled in). Split-screen — two new Wren classes: SplitScreen (SplitScreen.new(scene, players) → one third-person camera per player, layout/distance/height/dynamic/mergeDistance/splitDistance/isMerged/playerCount) and SplitLayout (auto/horizontal/vertical/grid). The renderer draws the scene once per viewport into its screen sub-rect (each with its own mesh-UBO instance; an empty viewport list is the byte-for-byte legacy single-camera path) with clear-attachment dividers; dynamic merge/split fuses the views into one group-framed camera (bounding-sphere → fov pull-back) when players cluster and splits them apart past a hysteresis band. New /examples/split-screen (apps/split_screen_demo — render-verified 4-way grid ⇄ merged group view). Landing Camera card updated; /api/camera gains SplitScreen/SplitLayout sections + the collider setter. v1 scope: ≤4 viewports, overlays full-screen, hysteresis merge/split (animated peel = additive v2, PLM-190). Headless test_camera_collider + test_split_screen; render-verified demos. 59 Wren classes (was 57 — +SplitScreen +SplitLayout), 80 apps (was 78 — +camera_collider +split_screen_demo). Suite 488/488, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Camera epic + ADR 0059)
Prior: 2026-07-31 (delta — Camera system: Cinemachine-like rigs + blending + noise + shake (Camera epic, ADR 0058). A streamlined, code-driven camera that replaces the ~185-line FPSCamera copy-pasted across ~9 demos, with no draw() plumbing — the engine auto-ticks a per-scene director that pushes the view/projection itself. Four new Wren classes in the engine module: Cam — a static factory for one-call rigs (thirdPerson(scene, target), firstPerson(scene, head), topDown(scene, target, height), orbit(scene, target, radius), freeFly(scene)) plus setActive(rig) / blendTo(rig, seconds, ease) / active(scene); CameraRig — the returned rig, with fluent setters (distance/damping/offset/fov/near/far/sensitivity/noise/height/radius/moveSpeed/shoulder/setActive/blendTo); Impulse — emit(scene, strength, x, y, z) decaying, distance-attenuated shake; and Ease — blend curves (linear/easeIn/easeOut/easeInOut/cut). Additive — the existing Graphics.setViewMatrix single-camera path and the low-level Camera class are untouched, and coexist last-write-wins. Device-free modules/camera solver core (unit-tested: math/damping/blend/body/aim/noise/impulse) + a native CameraDirector; test_camera_wren drives the whole surface headless; apps/camera_showcase render-verified (third-person follow → blend to orbit → shake). Existing /api/camera page extended with the rig system (low-level Camera kept below); new /examples/camera-showcase. Landing Camera “What’s in place” card added; the Scripting class list gains Cam/CameraRig. Freeze-staging: the Cam factory + named rigs + Ease/Impulse + blend semantics are frozen; the CameraRig setter param names/order are experimental for one release. 57 Wren classes (was 53 — +Cam +CameraRig +Impulse +Ease), 78 apps (was 77 — +camera_showcase). Suite 482/482, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Camera epic + ADR 0058)
Prior: 2026-07-31 (delta — Block-AI Wren surface: navmesh + surround-ring + AI math (engine PLM-187, ADR 0057). The scripted AI capstone for Block AI (enemy AI for Hadal, a top-down ARPG). Three new Wren classes in the engine module: Nav — a Recast/Detour navmesh (Nav.new(), bake(verts, tris) from a flat triangle soup → Bool, findPath(fx,fy,fz, tx,ty,tz) → a List of [x,y,z] waypoints that routes around blocked footprints); SurroundRing — an encounter-spacing solver (assign(id, weight, preferredRange, isMelee, desiredAngleDeg) → [angleDeg, radius, standby] sticky slots so a pack encircles instead of stacking on one point; release(id)); and Ai — decision-core math (Ai.stopDistance(attackRange, bodyRadius), Ai.inFieldOfView(ex,ey,ez, fx,fy,fz, tx,ty,tz, fovDeg)). Genre-general — the navmesh models walkable surfaces (top-down / third-person / FPS), not a top-down- or swim-specific abstraction. Thin bindings over device-free, unit-tested C++ cores; a Wren integration test (test_ai_wren) drives the whole surface headless (bake + cross-mesh path, distinct/stable/non-standby ring slots, the two Ai helpers). New /api/ai page + /examples/ai-hunters (apps/ai_hunters — four hunters detect a target, path around a central obstacle via the navmesh, and take distinct slots around it via the ring solver; render-verified encirclement). Landing AI & Navigation “What’s in place” card added; the Scripting class list gains Nav/SurroundRing/Ai, and the Physics card now names the spatial-query surface (overlapSphere + collision-layer masks from PLM-180). Composes with ADR 0056’s Physics.overlapSphere / Raycast.fromPointMasked. 53 Wren classes (was 50 — +Nav +SurroundRing +Ai; Nav had not been synced yet), 77 apps (was 76 — +ai_hunters). Gate 457/457, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Block AI + ADR 0057)
Prior: 2026-07-31 (delta — Physics: gameplay collision layers + spatial queries (engine PLM-180, ADR 0056). The sensory prerequisite for the new Block AI (enemy AI for Hadal, a top-down ARPG). A gameplay collision-layer bitmask on bodies — distinct from Jolt’s simulation ObjectLayer, it only filters queries, not what physically collides. On the existing Physics class (no new Wren class): setCollisionLayer(node, mask) / getCollisionLayer(node), overlapSphere(scene, x, y, z, radius, mask) → a List of nodes whose body center is within radius and whose layer intersects mask (the “who is near me” AI primitive), and Raycast.fromPointMasked(scene, o…, d…, maxDist, mask) — the layer-filtered line-of-sight cast (makes the previously-stub layerMask real). /api/physics gains a Spatial queries & collision layers section. Verified headless (Jolt unit test) + Wren integration test; gate 448/448, 0 warnings. No new Wren class → 50 classes; no new app → 76 apps (both unchanged). Engine-side Hyperscale/projects/plume3d.md, Block AI + ADR 0056)
Prior: 2026-07-30 (delta — Material textures: full PBR map set on script meshes + the MoltenVK render fix (engine PLM-021 / PLM-177, ADR 0055). The Mesh material-texture API (merged earlier as groundwork) is now ratified + documented + demoed end-to-end. On the existing Mesh class (no new Wren class): setBaseColorTexture/setMetallicTexture/setRoughnessTexture/setNormalTexture/setAoTexture/setEmissiveTexture(texture) + the scalar factors setColor/setMetallic/setRoughness + clearMaterial. The engine binds the six maps as a per-material descriptor set at set 1, bindings 1..6; an unassigned map falls back to a no-op default (white / flat-normal / black). Mesh shaders are per-app, so /api/mesh documents the binding contract. New /examples/material-textures (apps/material_textures — a script UV cube assigns all six maps from Wren, drawn with a reference shader that samples set 1 1..6 with derivative-TBN normal mapping). Landing Rendering “What’s in place” card updated (full material-map set + normal mapping). MoltenVK root cause (PLM-177): a per-app PBR shader that references the set-0 shadow array unconditionally fails to compile under Metal argument buffers unless the app enables shadow mapping (the engine only declares those bindings then); the material reference shader routes around it by keeping to set-1 samplers + lighting. Follow-ups filed: PLM-178 (make the set-0 layout always match the shader ABI) + PLM-179 (linear texture format for non-color maps). No new Wren class → 50 classes; +1 app (material_textures) → 76 apps. Engine-side Hyperscale/projects/plume3d.md, Block R + ADR 0055)
Prior: 2026-07-30 (delta — Effect wet/dry mix — audibility fix (engine PLM-160, ADR 0054). Bug the owner hit testing audio_effects_demo: added effects were inaudible. Diagnosed (loopback render + real-output capture) as correct-but-subtle — an OpenAL aux-slot effect is a parallel wet on top of the full-volume dry, and OpenAL caps the wet at unity, so it’s imperceptible under loud audio. Fix: a per-group wet/dry mix that ducks the dry so the wet stands out. Additive to the existing MixerGroup (no new Wren class): setEffectMix(mix) — 0 (default) = subtle parallel wet (unchanged), →1 = dry fully ducked (insert-style). Folded into the PLM-157 direct filter (one AL_DIRECT_FILTER per source). /examples/audio-effects-demo now sets a prominent mix per effect so each toggle is clearly audible. Also closes a test gap: the first regression test that asserts effects reach the audio output (renders through ALC_SOFT_loopback — CI-safe — and checks a reverb adds a tail the dry lacks + the mix ducks the dry). No new Wren class → 50 classes; no new app → 75 apps (both unchanged). Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0054)
Prior: 2026-07-30 (delta — Mixer effects & filters: contract freeze + capstone (engine PLM-159, ADR 0053). Closes the audio-effects epic. No new API — ADR 0053 freezes the per-group effects + filters Wren surface (the MixerGroup effect/filter methods + MixerEffect) as a public contract (consumer games are Wren-only, so this is what they depend on); a future signature change now needs an ADR, while additive values (a new addEffect type or setParam name) stay free. /api/mixer gains a Stable API note. The GUI /examples/audio-mixer-studio is now the complete Unity-parity console — its Music bus gains a reverb preset cycle + a low-pass toggle beside volume/mute/solo/Master/ducking/Save. No new Wren class → 50 classes; no new app (extended audio_mixer_studio) → 75 apps (both unchanged). Out of scope (a later epic): software-submix v2 — parameterized compressor, arbitrary param-EQ, swept resonant filters, normalize, long delays, send-matrix. Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0053)
Prior: 2026-07-30 (delta — Per-group direct-path filters (engine PLM-157, ADR 0052). The last native OpenAL EFX surface: a mixer group can carry one low/high/band-pass filter on its sources’ dry signal (AL_DIRECT_FILTER) — Unity’s mixer Lowpass/Highpass — separate from and composing with the effect chain. Additive to the existing MixerGroup (no new Wren class): setLowPass(gain, gainHF) (muffle), setHighPass(gain, gainLF) (thin), setBandPass(gain, gainLF, gainHF), clearFilter(). Gains are linear 0..1 shelf gains (OpenAL filters are not a swept resonant cutoff — that stays a software-mix feature). The engine keeps one AL_FILTER per group (rebuilt on change) and re-points each routed source’s direct filter each frame; direct filters are copy-on-attach, so no in-use-slot dance. Needs ALC_EXT_EFX (no-op otherwise). New /api/mixer Filters section; /examples/audio-effects-demo gains muffle/thin/clear buttons. No new Wren class → 50 classes; no new app (extended audio_effects_demo) → 75 apps (unchanged). Device-backed test reconciles low/high/band + swap/clear + a filter coexisting with an effect. Completes the native EFX exposure (aux-slot effects PLM-153/154 + filters PLM-157); only the contract-freeze ADR remains (PLM-159). Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0052)
Prior: 2026-07-30 (delta — Full OpenAL EFX effect suite (engine PLM-154, ADR 0051). Building on the reverb pipeline, every OpenAL EFX aux-slot effect is now exposed through the existing MixerGroup.addEffect(type) + MixerEffect.setParam (no new Wren class). Accepted types (case-insensitive): reverb, chorus, echo, flanger, distortion, equalizer, frequencyshifter, vocalmorpher, pitchshifter, ringmodulator, autowah, compressor (unknown → null). Each starts from OpenAL’s defaults, so addEffect("echo") is already a working echo; tune it with setParam(name, value) (per-effect param tables now on /api/mixer). Calling addEffect more than once on a group builds an ordered serial chain. The backend applicator is table-driven (one {name → AL enum, int/float} registry per type); mixerEffectTypeFromString is the single source of truth for type names. Compressor is on/off only and Equalizer is a fixed 4-band (a parameterized compressor / arbitrary EQ stay a later software-mix feature). Needs ALC_EXT_EFX (no-op otherwise). /api/mixer Effects/MixerEffect sections rewritten for the full suite; new /examples/audio-effects-demo (apps/audio_effects_demo — every effect + a serial chain on real music). Landing Audio card updated. No new Wren class → 50 classes; +1 app (audio_effects_demo) → 75 apps (was 74). Device-backed test reconciles all 12 types + a chain. Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0051)
Prior: 2026-07-30 (delta — Mixer effects: per-group reverb via OpenAL EFX (engine PLM-153, ADR 0050). A mixer group can host audio effects (OpenAL EFX); the first is reverb (EAX). One new Wren class MixerEffect (a non-owning effect handle) + additive MixerGroup methods: group.addReverbPreset(name) (Cave/Hangar/Underwater/ConcertHall/… → a MixerEffect), group.addEffect("reverb") (→ handle, null for unknown types), group.clearEffects(), group.effectCount; effect.setPreset(name) + effect.setParam(name, value) (named reverb params: decaytime, gain, diffusion, …). The engine builds the OpenAL EFX aux-slot chain behind the mixer (reconciled per frame; in-place param updates avoid rebuilds/leaks). Needs a device with ALC_EXT_EFX — safe gain-only no-op otherwise. Adversarially reviewed (an in-use-slot leak + a latched-error heisenbug found + fixed). /api/mixer gains Effects + MixerEffect sections; new /examples/audio-reverb-demo (apps/audio_reverb_demo — looping music with live reverb-preset switching). Landing Audio card + Scripting list updated. The rest of the EFX suite (echo/chorus/flanger/distortion/pitch/EQ/filters/…) follows in PLM-154+. 50 Wren classes (was 49 — +MixerEffect), 74 apps (was 73 — +audio_reverb_demo). Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0050)
Prior: 2026-07-30 (delta — GUI audio-mixer example app (engine PLM-150). A wired GUI mixer console — new example app, no engine API change. apps/audio_mixer_studio routes a looping music bed + a one-shot SFX (the audio_3d clips) to Mixer.group("Music"/"SFX"), with a channel strip per group (volume slider + muted/soloed), a Master fader, a ducking toggle (duckUnder/clearDuck), and a Save mix button (Mixer.saveSettings() → settings.toml). Group defaults come from the app’s game.toml [Mixer]. Showcases the whole mixer stack (groups + gain/mute/solo + ducking + config + persistence) on one screen. New /examples/audio-mixer-studio. No new Wren class → 49 classes; +1 app (audio_mixer_studio) → 73 apps (was 72). Engine-side Hyperscale/projects/plume3d.md, Block AX2)
Prior: 2026-07-30 (delta — Mixer audio config: game.toml [Mixer] defaults + player settings.toml (engine PLM-149, ADR 0049). Default audio-mixer group volumes in game.toml — a [Mixer] section (master + [[Mixer.group]] name/volume/mute) applied to the Mixer at startup (groups pre-created), documented on /configuration. Plus a player-override settings.toml in the app dir (same shape), auto-loaded at startup after the game defaults (player wins); two new additive Mixer methods (no new class): Mixer.saveSettings() (write current volumes/mute) and Mixer.loadSettings() (re-apply now). /api/mixer gains a Defaults and player settings section. Internal --test harness (apps/_mixer_config_test, _-prefixed = not an example) verifies defaults-applied + settings round-trip. No new Wren class → 49 classes; no new example app → 72 apps (counts unchanged). Engine-side Hyperscale/projects/plume3d.md, Block AX2 + ADR 0049)
Prior: 2026-07-30 (delta — Native MIDI → SoundFont route (engine PLM-136, ADR 0048). Play a SoundFont from a USB-MIDI keyboard with one call, additive to the existing Midi class (no new Wren class). Midi.route(instrument) — after Midi.openInput, the engine feeds every incoming note / controller / pitch-bend / program-change event straight to a SoundFontInstrument in C++ (the native low-latency route, mapped on each event’s MIDI channel), no per-note Wren; events still reach Midi.poll for gameplay. Midi.clearRoute() stops it (also auto-cleared if the routed instrument is collected). /api/midi gains a Playing a SoundFont from a keyboard section; new /examples/midi-soundfont-demo (apps/midi_soundfont_demo — routes a keyboard to the piano, with a computer-key fallback). Completes Block M. Live device play is verified by hand with a MIDI keyboard (HUMAN-017); the route/clear wiring is CI-tested. No new Wren class → 49 classes; +1 app (midi_soundfont_demo) → 72 apps (was 71). Engine-side Hyperscale/projects/plume3d.md, Block M + ADR 0048)
Prior: 2026-07-30 (delta — Audio ducking / sidechain (engine PLM-148, ADR 0047). The V1 mixer effect: a group ducks (drops in level) while another group is making sound — the classic music-under-dialogue. Additive to the existing MixerGroup (no new Wren class). group.duckUnder(triggerGroup, amountDb, attack, release) — while any source in triggerGroup’s subtree is playing, group ramps down to amountDb (e.g. −12; 0/positive = no duck) over attack s and back to unity over release s. group.clearDuck() removes it; group.duckGain reads the live envelope (1 = not ducked) for a meter. Activity-driven (fires on a playing source, not keyed to loudness — a level-metered sidechain compressor is the later software-submix v2); the duck composes with group volume, and mute/solo still win. Applied every frame by the engine. /api/mixer gains a Ducking section; new /examples/audio-ducking-demo (apps/audio_ducking_demo — hold Space so a voice cue ducks the music). Completes Block AX. No new Wren class → 49 classes; +1 app (audio_ducking_demo) → 71 apps (was 70). Engine-side Hyperscale/projects/plume3d.md, Block AX + ADR 0047)
Prior: 2026-07-30 (delta — Audio mixer groups: Mixer + MixerGroup (engine PLM-147, ADR 0046). Unity-style audio buses: two new public Wren classes route any sound — SFX, Music, Ambience, and SoundFont instruments — through named groups with per-group volume, mute, and solo. Mixer (static facade): Mixer.master (the root group), Mixer.group(name) (get-or-create under Master), Mixer.groupUnder(name, parent) (nested). MixerGroup: name, volume / volume= (linear gain, clamped ≥ 0), muted / muted=, soloed / soloed=, addSource(source) (route a Source), addInstrument(instrument) (route a SoundFontInstrument). Each sound plays at its own volume × its group’s effective gain (group gain × ancestors’, gated by mute/solo — Unity-like solo plays only the soloed subtree), applied every frame; an unrouted sound plays under Master (unity), so adding the mixer changes nothing until you route + adjust. A source’s own setVolume is preserved (the group multiplier never leaks into getVolume). Hard cutover (PLM-147): SoundFontInstrument.update() was REMOVED — the engine now renders + queues instrument audio automatically each frame; games no longer pump it. New /api/mixer page + /examples/audio-mixer-demo (apps/audio_mixer_demo — Music/SFX/Ambience with live faders, mute, solo); landing Audio card updated; /api/soundfont + its example updated to drop update(). Ducking/sidechain is the next step (PLM-148). 49 Wren classes (was 47 — +Mixer +MixerGroup), 70 apps (was 69 — +audio_mixer_demo). Engine-side Hyperscale/projects/plume3d.md, Block AX + ADR 0046)
Prior: 2026-07-30 (delta — MIDI device input: Midi (engine PLM-135, ADR 0044). One new public Wren class in the engine module — Midi — reads live MIDI input (a USB / virtual MIDI keyboard) as normalized events. Static facade (like Net): Midi.inputDevices → a List of device-name Strings; Midi.openInput(device) (a Num index or a String name substring) → Bool; Midi.closeInput(); Midi.isOpen; Midi.openedDevice → String; Midi.poll() → a List of event Maps drained since the last call, each { "type": String, "channel": Num (0–15), "data1": Num, "data2": Num, "value14": Num } (type ∈ noteOn/noteOff/controlChange/programChange/pitchBend/channelPressure/polyPressure/clock/other; data1/data2 = note+velocity or controller+value; value14 = pitch bend 0–16383). Events are parsed + queued on the device’s callback thread and handed to Wren on the main thread each frame, so poll() is always safe. The note source is arbitrary — feed events to a SoundFontInstrument, gameplay, or anything. Desktop only (no-op on builds without a MIDI backend). Powered by libremidi (pinned v5.4.2). New /api/midi page + /examples/midi-input-demo (apps/midi_input_demo — a MIDI monitor). Landing MIDI input “What’s in place” card added. Live note-in from a physical device is verified with hardware (engine HUMAN-017); the parser + Wren binding + real enumeration are CI-tested with no device. 47 Wren classes (was 46 — +Midi), 69 apps (was 68 — +midi_input_demo). Engine-side Hyperscale/projects/plume3d.md, Block M + ADR 0044)
Prior: 2026-07-30 (delta — SoundFont instruments: SoundFontInstrument (.sf2 synthesis) (engine PLM-134, ADR 0043). One new public Wren class in the engine module — SoundFontInstrument — loads a SoundFont (.sf2) bank and plays instruments from it as real polyphonic audio, rendered through the streaming sink (PLM-133). SoundFontInstrument.new(path, sampleRate, channels) mount-loads a bank (PhysicsFS-sandboxed; channels = 1 mono / 2 stereo) and creates its own streaming source; check loaded (false if the bank was missing/unparseable — the object stays safe to call). Drive it from any note source (keyboard/gamepad/chart/MIDI) with noteOn(note, velocity) (MIDI note 0–127, velocity 0–1; polyphonic), noteOff(note), allNotesOff(), setPreset(index) (0…presetCount−1), controlChange(controller, value) (MIDI CC, e.g. 64 = sustain), pitchBend(value14) (14-bit, 8192 centered), setMaxVoices(n), setGain(db); read state via loaded / presetCount / presetName(i) / activeVoices. update() pumps audio and must be called once per frame (manual pump — automatic servicing is deferred to the audio-mixer epic). SoundFont banks are loaded resources, not committed — large licensed .sf2 files are git-ignored and fetched at runtime (owner decision); the demo/tests skip cleanly when absent. New /api/soundfont page + /examples/soundfont-demo (apps/soundfont_demo — a computer-keyboard piano); the landing Audio “What’s in place” card now lists SoundFont instrument playback. Live MIDI-device input is a separate upcoming capability (PLM-135) — do not claim it. 46 Wren classes (was 45 — +SoundFontInstrument), 68 apps (was 67 — +soundfont_demo). Engine-side Hyperscale/projects/plume3d.md, Block M + ADR 0043)
Prior: 2026-07-29 (delta — Audio: float-PCM streaming sink (engine PLM-133, ADR 0042). A way to stream synthesized / procedural float PCM to the audio device over time, additive to the existing Audio / Source classes (no new Wren class, no new Wren method beyond three additive members). Audio.newStreamingSource(sampleRate, channels) → a Source you feed over time (channels = 1 mono or 2 stereo; returns null on failure) — the counterpart to the one-shot Audio.newSource(path), which is unchanged. source.queueSamples(list) → queues a List of interleaved float samples in [-1, 1] (stereo interleaves L,R,L,R…; bounded to 1M samples/call) and (re)starts playback automatically as buffers are queued — the engine recycles finished buffers; returns Bool; a no-op on a non-streaming source. source.queuedBuffers → the number of buffers currently queued — poll it to pace refills (top up to ~4 each frame). Uses AL_EXT_FLOAT32 (float) with an int16 fallback; main-thread fed (top up each frame). It is the primitive a SoundFont/MIDI synth (Block M — PLM-134/136) renders into, and independently useful for procedural / streamed music. /api/audio gains a Streaming audio section (the three members, the interleaved-float format, the feed-each-frame pattern with a Wren example); new /examples/audio-streaming-demo (apps/audio_streaming_demo — a game streaming a procedural 220 Hz sine tone); the landing Audio “What’s in place” card now lists streaming float PCM. No new Wren class → 45 classes; +1 app (audio_streaming_demo) → 67 apps (was 66). Engine-side Hyperscale/projects/plume3d.md, Block M + ADR 0042)
Prior: 2026-07-29 (delta — Retained UI: text input — editable input field (engine PLM-142, ADR 0041). A new .ui element type, an editable single-line text field, additive to the existing Ui class + .ui format (no new Wren class, no new Wren method — created via Ui.createElement(doc, "input", parent) and read through the existing elem.text accessor). type = "input" — a focusable field: click it to focus (a caret appears), type to edit — insert / backspace / delete and Left/Right/Home/End caret movement are all UTF-8-aware (whole codepoints), and a press elsewhere blurs it. New fields: text (the initial/current value — read via elem.text), placeholder (a dimmed hint shown while empty), on_submit (a named handler fired on Enter → Game.<name>()), on_change (a named handler fired when the buffer changes), and caret_color (the caret bar colour). This is v1 — single-line, focus-by-click; selection, clipboard, multi-line, and IME composition are a documented follow-up (PLM-145) — do not claim them. /api/ui gains an Input element-type entry (the fields, focus-by-click, elem.text, the v1 scope); new /examples/ui-input-demo (apps/ui_input_demo — a Name field with initial text + a City field showing a placeholder); the landing Retained UI “What’s in place” card now lists text input. No new Wren class (Ui already exists) → 45 classes; +1 app (ui_input_demo) → 66 apps (was 65). Engine-side Hyperscale/projects/plume3d.md, Block U + ADR 0041)
Prior: 2026-07-29 (delta — Retained UI: layout groups — horizontal / vertical / grid (engine PLM-143, ADR 0040). A container .ui element can now auto-position its direct children by flow, additive to the existing Ui class + .ui format (no new Wren class, no new app, and no new Wren method — layout is authored in the .ui). New container fields: layout = "horizontal" | "vertical" | "grid" (Horizontal packs children left→right, Vertical top→bottom, Grid wraps every columns items — each row as tall as its tallest child; absent = anchor positioning, unchanged), spacing (px gap between items), padding = [L, R, T, B] (px insets on the content origin — L/T seed the flow cursor, R/B reserved for a future width-wrap), and columns (Grid only, ≥ 1). A layout group overrides only each child’s position; the child keeps its own size (from its offsets), and children of a flow-positioned element still anchor relative to its resolved rect, so layout groups compose with the anchor system (a grandchild centres inside a flow-placed cell). Existing .ui files load identically. /api/ui gains a Layout groups section (the four fields, the three modes, size-preservation + anchor composition); the landing Retained UI “What’s in place” card now lists layout groups. No new Wren class (Ui already exists) → 45 classes; no new app → 65 apps (counts unchanged). Engine-side Hyperscale/projects/plume3d.md, Block U + ADR 0040)
Prior: 2026-07-29 (delta — Retained UI: Progress + Radio widgets (engine PLM-141, ADR 0039). Two new .ui element types on the existing Ui class + .ui format (no new Wren class, no new app). type = "progress" — a display-only progress bar; it reuses the slider’s value/range model (value in [min, max], color track, fill_color fill) and has no handle and no pointer interaction. type = "radio" — a grouped toggle: it reuses the toggle’s checked/check_color/on_change plus a new group (string). Radios sharing the same non-empty group are mutually exclusive — clicking one selects it (checked = true) and deselects its group siblings; a radio never toggles back off. Both are polled through the existing UiElement.value/checked accessors and created via Ui.createElement(doc, "progress"|"radio", parent) — no new Wren methods (the group is authored in the .ui file). /api/ui documents both element types + the mutual-exclusion behaviour; the apps/ui_widgets demo + /examples/ui-widgets gain a progress bar + a Low/High radio group; the landing Retained UI “What’s in place” card now lists progress + radio. Combo/Dropdown, ScrollView, and Pills are explicitly deferred (PLM-144 — they need popup/scissor-clip/rounded-rect infrastructure the retained UI lacks); do not claim them. No new Wren class (Ui already exists) → 45 classes; no new app (ui_widgets already exists) → 65 apps. Engine-side Hyperscale/projects/plume3d.md, Block U + ADR 0039)
Prior: 2026-07-29 (delta — Retained UI: textured 9-slice + image-by-path + .ui.component sidecar (engine PLM-139/140, ADR 0038). Two related retained-UI features on the existing Ui class + .ui format (no new Wren class). (1) Textured 9-slice now renders (PLM-139). A .ui panel/button with nine_slice = true + border = [L,R,T,B] (texture px) and an image now draws as a real 9-slice — corners keep their pixel size, edges stretch on one axis, the center stretches both (the fields previously round-tripped but did nothing). (2) Image-by-path + .ui.component sidecar (PLM-140, ADR 0038 — additive extension to Frozen Contract #1). A .ui element can reference an image BY PATH — image = "ui/panel.png" (a mounted PNG, resolved once per element on first Ui.render, PhysFS-sandboxed) — alongside the legacy bare-integer texture handle. A .ui.component TOML sidecar next to the image (<image path>.ui.component, e.g. ui/panel.png.ui.component) carries slicing metadata (nine_slice + border) authored once with the asset; a .ui referencing that image by path picks it up automatically for values the .ui left at defaults (an explicit .ui border wins). PNG only (PSD/XCF deferred); untrusted-input hardened at the parser boundary. Existing .ui files load identically. /api/ui gains a Textured panels & 9-slice section (the image field, 9-slice behaviour, the sidecar); new /examples/ui-nineslice-demo (apps/ui_nineslice_demo — two different-sized frames share one textures/panel.png + a border=24 sidecar, corners unstretched); landing Retained UI “What’s in place” card updated (9-slice + image-by-path / .ui.component). No new Wren class (Ui already exists), so classes hold at 45; 65 apps (was 64 — +ui_nineslice_demo). Engine-side Hyperscale/projects/plume3d.md, Block U + ADR 0038)
Prior: 2026-07-29 (delta — Coroutines / Async: an engine-ticked cooperative scheduler (engine PLM-031, ADR 0037). One new public Wren class in the engine module — Async, a cooperative coroutine scheduler over Wren’s core Fiber. Async.run { ... } starts a coroutine (a zero-arg block; runs immediately to its first suspend or completion); inside one, Async.wait(seconds) suspends for seconds of scene time (frame delta, not wall-clock) and Async.nextFrame() suspends until the next frame; Async.pending (getter) counts suspended coroutines and Async.clear() cancels them all (e.g. on a scene change). The engine auto-ticks the scheduler every frame (after physics, before Game.update) — the game wires nothing into its own update, and a game that never imports Async pays nothing (guarded). Cooperative + single-threaded (Wren fibers are thread-confined — this is frame-scheduling, NOT parallelism/threads); a coroutine that never yields blocks the frame; timing is deterministic under fixed-step / headless runs. New /api/coroutines page + /examples/coroutine-demo (apps/coroutine_demo — a self-cycling traffic light + a per-frame counter, both driven by coroutines). Landing Coroutines / Async “What’s in place” card added; the Short-Term Scripting roadmap item (“Coroutines or async for ‘wait then…’”) removed (now ships); the Coroutines API-gap bullet resolved. 45 Wren classes (was 44 — +Async), 64 apps (was 63 — +coroutine_demo). Engine-side Hyperscale/projects/plume3d.md + ADR 0037)
Prior: 2026-07-29 (delta — Save/Load: game-authored, versioned SaveState + Save (engine PLM-022, ADR 0036). Two new public Wren classes in the engine module. SaveState — a typed key/value store (bool / int / number / string): setBool/setInt/setNumber/setString, getBool/getInt/getNumber/getString (1-arg and 2-arg-with-fallback), has, remove (→ Bool), clear, count (getter), keys (sorted List). Save — a static file facade over the app’s PhysicsFS write dir: write(name, state) → Bool (nested paths get their parent dirs created), read(name) → SaveState|null, exists(name) → Bool. Serializes to human-readable versioned TOML ([meta].format_version + [data]); a save written by a newer build reads back as null — rejected, never mis-parsed. Game-authored (the game chooses what to persist), NOT an automatic scene dump. Slots enumerate with the existing Resource.list("saves", ".sav") — there is no Save.list. New /api/save page + /examples/save-load-demo (apps/save_load_demo — persists a launch counter across runs). Landing Save & Load “What’s in place” card added; the Short-Term Save/load roadmap item removed (now ships). 44 Wren classes (was 42 — +SaveState +Save), 63 apps (was 62 — +save_load_demo). Engine-side Hyperscale/projects/plume3d.md + ADR 0036)
Prior: 2026-07-25 (delta — Resource.list directory enumeration + node-editor model picker (engine PLM-125, ADR 0035). New public Resource.list(dir, ext) — the sorted filenames of the files in a mounted directory, filtered by a case-insensitive suffix (e.g. ".blend"), sub-directories excluded, PhysicsFS-sandboxed (a ../absolute dir lists nothing, never escapes the project). Backed by new C++ FsMount::enumerateFiles. The /examples/node-editor 3D Preview gains a Model dropdown over Models/*.blend next to the collection dropdown, so a converted marketplace character previews alongside the bundled sprite; both picks are remembered across restarts. Additive Wren API on the existing Resource class (42 Wren classes, still 62 apps; the Scripting landing card already lists Resource). Security-reviewed (untrusted-path surface — sandbox + memory-safety clean). Engine gate 359/359, 0 warnings. Engine-side Hyperscale/projects/plume3d.md + ADR 0035)
Prior: 2026-07-24 (delta — Animation layers: runtime weight API + layers editing (engine PLM-123/124, ADR 0034). Layers were fully implemented in C++ but unreachable from Wren. New ctrl.setLayerWeight(layer, weight) / ctrl.layerWeight(layer) — layer is the authored [[layers]] name or its 1-based index; weight clamps to 0..1; an unknown layer is an inert no-op. Documented in a new Layers section on /api/animation-controller that also states the model plainly for Unity users: a Plume3D layer is a masked pose overlay over ONE graph, not a per-layer state machine (owner-decided direction; ADR 0021/0034). The /examples/node-editor gains a Layers panel (edit name/blend/mask, see the Actions bound to a layer, renaming re-points them) and live per-layer weight sliders in the 3D preview. Also fixed: a blend2d node was previously saved back in the blend1d schema, producing a controller the engine rejects on load — 1D and 2D are now each edited and saved in their own schema (param+at vs param_x/param_y+x/y), and clip sections are preserved. Additive Wren API on an existing class (42 Wren classes, still 62 apps); landing Animation Controller card updated. Engine gate 355/355, 0 warnings. Engine-side Hyperscale/projects/plume3d.md + ADR 0034 + the layers-strategy spec)
Prior: 2026-07-24 (delta — Node editor: preview collection picker + lossless round-trip (engine PLM-121/122). The /examples/node-editor 3D Preview gains a collection dropdown that auto-discovers the model’s collections (the shipped Character.blend exposes Fire + Bramble sprites), previews the picked one live (rebuild + re-attach), and remembers your pick across restarts in a gitignored tool-owned file — no config file to maintain. Separately, the editor’s serializer now round-trips every controller section losslessly, including the ones it doesn’t draw as nodes — [[layers]], [transitions], [actions], [contexts] — which were previously silently dropped on Save. Adversarial review fixed 5 confirmed edge cases (3 emitter shapes: array-of-scalars, empty array, non-bare section name; 2 picker: default-resolution + coverage honesty), verified via device runs + an independent tomllib round-trip. App-side Wren only (no engine/schema change). Still 62 apps. Engine gate 352/352, 0 warnings. Engine-side Hyperscale/projects/plume3d.md + layers-strategy spec)
Prior: 2026-07-24 (delta — In-editor 3D animation visualizer (engine PLM-118). The /examples/node-editor gains a 3D Preview mode: it renders the skinned character playing the controller being edited, driven live by the edited graph (serialize → scratch TOML → attachController, which hot-reloads while preserving state + params). Drive it with WASD/Space/K → params (walk transitions/blends/Any-State without launching the game); orbit + zoom camera with a track/no-track toggle for root-motion; a toggleable floor + gravity toggle + grounded readout for ground-checking. Full-frame mode (the design’s de-risk path; an embedded render-to-texture panel is a follow-up — the record_scene_meshes renderer refactor for it already landed). Bundled controllers/preview.toml pairs with Character.blend so it works out of the box. Docs: a new 3D preview section on the node-editor page. Still 62 apps (enhances the existing example). Screenshot-verified; engine gate 351/351, 0 warnings. Engine-side Hyperscale/projects/plume3d.md + design spec + ADR 0033)
Prior: 2026-07-24 (delta — StateBehaviour Wren class (engine PLM-116). A new StateBehaviour base class in the engine module — Unity’s StateMachineBehaviour as pure sugar over the controller’s per-state hooks: subclass, override onEnter/onUpdate/onExit/onEnd, then attach(controller, state). Documented in a new State behaviours subsection on /api/animation-controller. No engine/schema/AnimController-API change (additive Wren sugar). 42 Wren classes now (was 41). Still 62 apps. Engine gate 351/351, 0 warnings. Engine-side Hyperscale/projects/plume3d.md)
Prior: 2026-07-24 (delta — Node editor UX: pan/zoom, multi-select, dropdowns (engine PLM-114). The /examples/node-editor editor gains an effectively-infinite canvas (wheel-zoom toward the cursor, middle-drag pan, Fit view), multi-select (box-marquee, shift-click, multi-drag, multi-delete), and dropdowns wherever options are drivable — a structured guard editor (param dropdown + operator dropdown + a value typed to the param; or a trigger / END) and to / state-ref / blend-entry-clip / blend-param pick-lists. Pure Wren app (no engine/schema change, no new page). Still 62 apps. Screenshot-verified; engine gate 350/350, 0 warnings. Engine-side Hyperscale/projects/plume3d.md)
Prior: 2026-07-24 (delta — Transition events (engine PLM-115, amends the controller schema + Wren API). A graph transition can raise a named event: an optional event = "Name" on any per-state OR [any_state] transition, delivered to ctrl.onEvent("Name", fn) on commit; plus ctrl.onTransition(fn) — a firehose fn(from, to) on every committed transition. Order: onExit(from) → onEnter(to) → onTransition → onEvent. Additive/backward-compatible (a transition with no event, a controller with no subscribers, behave as before). Recorded in engine ADR 0032. Docs: new Transition events subsection on /api/animation-controller + the event in the TOML example + the Animation Controller landing card. The node editor round-trips the event field. Still 62 apps, no new page. Engine gate 350/350, 0 warnings. Engine-side Hyperscale/projects/plume3d.md)
Prior: 2026-07-24 (delta — Any State: Mecanim-style global transitions (engine PLM-113, amends the controller schema). The AnimController TOML gains an optional top-level [any_state] table — global transitions evaluated every tick from whatever state is current (Mecanim’s Any State), so a die → death edge is reachable from every state without wiring it per-state. Each entry is the usual { to, guard, fade } plus an optional self (Can-Transition-To-Self, default false — won’t restart the state you’re in). Any-state edges are checked first (global override); END is not allowed on a global edge. Additive/backward-compatible — a controller with no [any_state] is unchanged; no new Wren API (TOML-authored). Frozen-schema amendment owner-approved (engine HUMAN-015), recorded in engine ADR 0031. The /examples/node-editor gains an “Any State” source node that round-trips it. Docs: the controller page’s new Any State section + the TOML example + the Animation Controller landing card. Still 62 apps (no new app). Engine gate 345/345, 0 warnings. Engine-side Hyperscale/projects/plume3d.md)
Prior: 2026-07-24 (delta — Node editor: any-direction connectors + blend trees (engine PLM-112, refines U-M4). The /examples/node-editor editor now routes connectors from any node side (not just left→right — edgeAnchor picks the facing edge) and treats blend trees as first-class: a unified state/blend/clip graph with three typed edge kinds (transitions gold + guard label, plays blue, blend-entries teal + @threshold) and a blend inspector (param + entry thresholds). Loads/edits/saves the full controller — states + blend nodes (1D/2D via param/param2 + entries) + clips + transitions + params + root_motion — round-tripping the LOCKED PLM-077 schema (verified 3 states / 1 blend / 5 clips → reload → 3/1/5, thresholds and all). Adversarial review FIX-FIRST → fixed dangling-ref-on-delete (a delete now prunes every reference), malformed-load type-guards, quoted non-bare loaded keys, and a stale link-in-progress; plus a refKind node/clip toggle. Still 62 apps (enhances the existing example — no new app, no new Wren API). Gate 339/339, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Block U)
Prior: 2026-07-24 (delta — Node animation-state editor — Block U capstone (engine PLM-087, U-M4). New /examples/node-editor (apps/node_editor) — a visual editor for AnimController graphs: states are nodes, transitions are directed bezier wires, drawn on the retained-UI Canvas; chrome (toolbar/list/inspector) via immediate-mode Gui. Loads a controller .toml (Config.load, the LOCKED PLM-077 schema — never a new format), extracts the state graph (states + node/clip refs + guarded transitions + default), supports select/drag/add/link/delete/rename/edit-guards/set-default, and saves back with a schema-aware serializer that round-trips the whole controller ([controller]/[params]/[clips.*]/[nodes.*]/[states.*]+inline transitions/[root_motion]) — verified. 62 apps now. Screenshot-verified. No new Wren API (built on the U-M3 Ui.canvas* + Config/Gui). This completes Block U (retained-mode GUI): U-M1 runtime+editor → U-M2 widgets → U-M3 Canvas → U-M4 node editor. Deferred: live-preview on a skinned character, node-position persistence, pan/zoom. Engine-side Hyperscale/projects/plume3d.md, Block U) Prior: 2026-07-24 (delta — Retained UI: Canvas / custom-draw capability (engine PLM-111, Block U/U-M3). A type = "canvas" element is a region the game draws custom primitives into with a new Ui.canvas* API — canvasRect/canvasLine/canvasBezier/canvasText, in canvas-local design px, colours 0..1 — composited in the retained-UI pass and letterboxed with the rest of the document. New /examples/ui-canvas (apps/ui_canvas — a node graph of boxes + bezier wires + labels; 61 apps now) + a top delta; /api/ui documents the canvas element type + the four draw methods; the /examples/ui-editor can add a canvas. No new Wren class (new element type + Ui statics). The node-graph enabler for the upcoming node editor. Screenshot-verified; gate 339/339, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Block U) Prior: 2026-07-24 (delta — Retained UI: widget breadth — Toggle + Slider (engine PLM-110, Block U/U-M2). Two interactive widgets added to the retained UI: type = "toggle" (a checkbox — a click flips checked) and type = "slider" (drag to set value in [min, max]). Retained-state-polling model: the game reads element.checked / element.value each frame (an optional on_change also dispatches to Game.<name>()). New /examples/ui-widgets (apps/ui_widgets — a toggle + slider recolour a swatch by polling; 60 apps now) + a top delta; /api/ui documents the toggle/slider element fields + the checked/value accessors; the /examples/ui-editor can add them. No new Wren class (new element types + UiElement accessors). Screenshot-verified; gate 337/337, 0 warnings. Engine-side Hyperscale/projects/plume3d.md, Block U) Prior: 2026-07-24 (delta — Retained UI: WYSIWYG editor + authoring API — U-M1 complete (engine PLM-109 + PLM-004/ADR 0020, Block U). The retained UI now has a visual editor: new /examples/ui-editor (apps/ui_editor) — an element tree, an inspector, and a live canvas rendered by Ui.renderEdit (the identical runtime ⇒ WYSIWYG), click-to-select via Ui.hitTest, and a selection gizmo drawn through a transparent Gui.beginOverlay. Built on a new editor-authoring tier on /api/ui: Ui.newDocument/createElement/deleteElement/reparent/save/hitTest/renderEdit, UiDocument.elementAt, and UiElement type/parentId/id=/rect/color/fontSize/onClick — every structural edit keeps the tree valid and invalidates handles (re-fetch by id). The editor runs standalone or plume3d --tool apps/ui_editor <target> to edit a target project’s .ui; the --tool CLI (the data/script-root split) is now documented on the UI Editor example page (engine ADR 0020). /api/gui gains beginOverlay/endOverlay. Landing “Retained UI” card + Scripting unchanged (no new Wren class — new methods on existing Ui/UiElement/Gui). Screenshot-verified; gate 328/328, 0 warnings. Engine-side tracked in Hyperscale/projects/plume3d.md, Block U) Prior: 2026-07-24 (delta — Retained-mode UI shipped — Wren Ui API + .ui format (engine PLM-108, ADR 0019, Block U/U-M1). The first user-facing slice of the retained UI: new /api/ui documents the Ui / UiDocument / UiElement Wren classes and the .ui TOML document format (a [document] + [[element]] tree, parent-by-id, uGUI-style anchors/offsets, panel/text/button widgets). Ui.load(path) parses + validates a .ui (malformed input returns null, never a crash); Ui.render(doc, x, y, w, h) letterboxes the design into a screen rect (resolution-independent, aspect preserved), runs pointer interaction, and submits the draws (panels/images as quads, labels via the SDF Text pass); a button released inside fires Game.<on_click>() from native context. UiElement is a non-owning handle (valid/id/visible/text/onClick/resolved x/y/width/height). New app apps/ui_demo + /examples/ui-demo: a .ui card (title, subtitle, interactive button) rendered full-window, screenshot-verified. Landing “What’s in place” card added (Retained UI); the Scripting card now lists Ui. Built on the U-M1 stack (engine PLM-104 core → 105 codec → 106 Vulkan backend → 107 pointer phase → 108 this API). Engine-side tracked in Hyperscale/projects/plume3d.md, Block U) Prior: 2026-07-23 (delta — Animation-controller demo — CT-M1 complete (engine PLM-086). New app apps/character_anim + /examples/character-anim: the epic’s end-to-end proof — a skinned Character.blend/Fire_Sprite driven through the AnimController via node.attachController("controllers/biped.toml") (a blend-node locomotion State, typed speed param from input, root-motion designation, a tagged cancel section driving input-buffering/cancel-windows, an onEnter hook from the auto-tick). Screenshot-verified (the character skins + animates via the controller, no validation errors). Named character_anim to sit alongside — not replace — the existing character_controller Jolt physics demo. Fixture-limited to one action so the blend is single-entry + no distinct masked-Action clip yet (a richer multi-action rig is HUMAN-014); the engine supports the full blend/Action (unit-tested). Engine-side tracked in Hyperscale/projects/plume3d.md, Block CT) Prior: 2026-07-23 (delta — State / Action / Context runtime (engine PLM-085, ADR 0018/0030). The controller’s behaviour model is now live: Actions — transient overlays played via ctrl.action(name) on a masked layer over the current State (without exiting it), completing on the clip’s latch-and-refire END, interrupting any action on the same layer; Contexts — modal param gates (when/allows) that permit/block States and Actions (a gated State transition holds and preserves its trigger; a blocked action(...) is a no-op). [actions]/[contexts] in the TOML schema now have runtime (previously validate-only). [api/animation-controller](/api/animation-controller/) gains an “Actions & Contexts” section + the action(name) method + the two schema tables. Landing card updated. Engine-side tracked in Hyperscale/projects/plume3d.md, Block CT) Prior: 2026-07-23 (delta — TOML controller-asset schema now documented + loaded (engine PLM-084, ADR 0017/0029). node.attachController(path) now parses the LOCKED controller-asset schema into a live graph: [controller] (name/default_state), typed [params], [clips] (+ tagged sections), [nodes] (blend1d/blend2d), [transitions] value-objects, [states] with guarded edges (param OP value / bare-trigger / END), masked [[layers]] (override/additive), and [root_motion]. controller.play(name) now accepts a named transition (value-object) or a bare clip. Controllers hot-reload when the TOML file changes (a bad reload keeps the previous graph). The parser validates + bounds all untrusted input at the boundary (dangling/duplicate/malformed refs rejected with diagnostics — a /security-review found no exploitable issues). [api/animation-controller](/api/animation-controller/) gains an “Authoring the graph” section with the full schema; the landing card is updated (TOML authoring now shipped). [actions]/[contexts] are validated but their runtime is PLM-085. Engine-side tracked in Hyperscale/projects/plume3d.md, Block CT) Prior: 2026-07-23 (delta — Animation controller Wren surface (engine PLM-083, ADR 0017). New /api/animation-controller: a per-character AnimController attached via node.attachController(path), layered over AnimationState. Ships the runtime surface — typed params (setFloat/Int/Bool/setTrigger + getters), a direct-play escape hatch (play(name)), clip phase (currentState, normalizedTime, inSection over the highest-weighted “primary” clip), behaviour hooks (onEnter/onUpdate/onExit/onEnd, the last a latch-and-refire END — fires once when a non-looping clip finishes and again on each loop wrap), and read-only rootMotionDelta (character-local; the engine strips the motion from the pose and the game applies the delta to the body). The engine auto-ticks attached controllers each frame (after event dispatch, before Game.update); tick(dt) is also callable for manual stepping. Landing “What’s in place” card added. TOML graph authoring (states/blend nodes/transitions/layers) lands in the next sync (PLM-084). Engine-side tracked in Hyperscale/projects/plume3d.md, Block CT) Prior: 2026-07-16 (delta — Contact callbacks now deliver a PhysicsContact object (ADR 0016, PLM-064). onContactBegin/onContactEnd/onContactStay previously took eight positional arguments (nameA, nameB, px, py, pz, nx, ny, nz); they now take one argument, a PhysicsContact, with accessors getNodeA()/getNodeB() (name), getNodeAId()/getNodeBId() (numeric node id, unique, 0 = no node), getPointX/Y/Z(), getNormalX/Y/Z(), and hasManifold() (false for onContactEnd, whose point/normal are zeroed — so a script can tell “no manifold” from a real contact at the origin). This closes the gap that PhysicsContact was a fully-bound Wren class the engine never handed to a script — it existed only as a constructible empty object. It is a breaking signature change: the arg count is validated at registration and a wrong-arity callback is rejected with a logged error rather than fed garbage (the VM’s own arity check compiles out of release builds). /api/physics contact section rewritten; the previous “String name, not a Node” correction is superseded. Engine-side tracked in Hyperscale/projects/plume3d.md, Block J) Prior: 2026-07-16 (delta — Engine↔docs gap analysis — a six-lens audit of the engine’s public surface against this site. An entire ADR had never reached the docs: ADR 0009 (accepted 2026-07-10) shipped Http.post and Engine.entropy; neither appeared anywhere on the site, and this very log jumps 07-08 → 07-12 straight past it. Closed: new /api/http; Engine.entropy() on /api/engine; Config.save on /api/config (which was marked Current while documenting half its class); the API Overview index was missing six live pages (Http, Net, PlayFlow, Sprite, Texture, Text) and now lists them. Seven documented claims were false and are corrected — worst first: collision_matrix was documented backwards, and the page’s own recommended example dropped every dynamic body through the floor; contact callbacks pass a String name, not a Node, so every documented example was uncallable; Engine.Version() returns 202601.001, not 2026.1.1; getBodyCount counts all bodies, not active ones; raycast directions must be normalised; gamepadButton takes an index, not a name; addNode(name) is required, not optional. [Window] had no keys documented despite this file claiming it was covered. The landing page had silently skipped four capabilities’ cards (Sprites, SDF Text, Networking, HTTP) over four months and overstated touch. Four “API gaps” bullets were wrong or understated — notably “Coroutines — Not in Wren runtime”, when the vendored VM ships Fiber. Two shipped apps (sdf_text_demo, sprite_demo) were invisible here while API pages point readers at them by name → PWEB-013. Engine-side tracked in Hyperscale/projects/plume3d.md) Prior: 2026-07-16 (delta — Ongoing contacts + bounded contact queues (PLM-061): new Physics.onContactStay(scene, callback), fired once per step for each pair still touching — the ongoing-contact counterpart to onContactBegin/onContactEnd, for grounded checks and sustained pushes. It is a firehose (every step, every touching pair), so the queue behind it only runs while a callback is registered. The same change fixes a pre-existing leak: the contact queues were drained only inside the “is a callback registered?” check, so an app with contacts but no contact callback grew them for the lifetime of the world. Queues are now capped (4096 events each) and drained every frame regardless of listeners — the engine holds the bound itself instead of trusting a consumer, so a C++-only app is safe too. Interacts with manual stepping: a long stepN run has no frame in between to drain, so it can now drop the excess rather than grow without bound. /api/physics updated. Engine-side tracked in Hyperscale/projects/plume3d.md, Block J) Prior: 2026-07-16 (delta — Manual physics stepping (ADR 0015, PLM-058): new Physics.step(scene, dt) / Physics.stepN(scene, dt, n) / Physics.fixedDeltaTime, advancing a scene’s physics on demand, decoupled from the game loop. Makes headless physics possible: a plume3d --test run never runs the loop, so this is the only way to advance a sim there — a test can now step to rest and assert the outcome deterministically (same build + machine) instead of capturing a windowed screenshot. Requires the scene’s auto-step to be paused (setSimulationPaused), enforced, so the loop and a manual stepper can never both advance a scene. /api/physics updated. Engine-side tracked in Hyperscale/projects/plume3d.md, Block J) Prior: 2026-07-15 (delta — OverPar physics Wren surface (ADRs 0011–0014, PLM-026/055/056/057): new Physics body builders — sphere/capsule/cylinder/convexHull (static+dynamic) — plus a static triangle-mesh collider (addStaticMesh) and a heightfield collider (addStaticHeightfield, backed by a new C++ ShapeType::HeightField), and runtime material setters (setRestitution/setFriction/setDamping). /api/physics updated. Engine-side tracked in Hyperscale/projects/plume3d.md, Block J) Prior: 2026-07-12 (delta — Blender addon 0.2.0 overhaul (PLM-044…054): in-Blender Validate + Write Config to .blend buttons, class-name picker, multi-select batch layers, addon preferences (fallback layer names), Blender 4.2+ extension manifest, collision-layer default fixed to the engine’s Default layer, save-sync no longer stamps unconfigured objects. /blender-addon updated. Engine-side tracked in Hyperscale/projects/plume3d.md, Block T) Prior: 2026-07-08 (delta — Billboarded sprites (ADR 0008, PLM-009): new Sprite + Texture Wren API, new /api/sprite + /api/texture pages. Same-day SDF text (ADR 0007, PLM-010): Text API + /api/text. Engine-side tracked in Hyperscale/projects/plume3d.md, Block B steps 11–12) Prior: 2026-07-03 (full networking stack: Net transport (ADR 0003) + replication (ADR 0005) + PlayFlow client (ADR 0004). New /api/net, /api/playflow; /dedicated-server + /configuration updated)


New since last review (2026-06-29) — resolved 2026-06-29

Section titled “New since last review (2026-06-29) — resolved 2026-06-29”

Engine features that landed after the 2026-03-25 review, now documented (PLM-032):

FeatureEngine sourceDoc status
Random — seeded, deterministic RNG (PCG32): Random.new(seed), int(n), intRange(min,max), float(), shuffle(list). Identical seed ⇒ identical sequence cross-platform (ADR 0002).wren_engine_module.cpp + wren_api.cpp✅ Done — new /api/random page; API index + landing updated.
Headless test mode — plume3d --test <appdir> (alias --headless) runs <appdir>/tests/run.wren → Tests.main() on the headless VM (no window/Vulkan), exits with Engine.exit code (ADR 0001).engine/src/app_host.cpp✅ Done — new Testing (Headless) guide + sidebar entry.
Engine.exit(code) — clamps 0–255; ends the run / sets the process exit status.wren_api.cpp (Engine)✅ Done — added to /api/engine.
apps/toml_editor exampleapps/toml_editor✅ Done — new /examples/toml-editor page + examples index (Tools).
--tool <toolDir> <dataDir> editor mode — run a tool’s main.wren against another project’s data dir.engine/src/app_host.cpp⏳ Partial — usage shown on the TOML Editor page; full dedicated doc pending the engine ADR (engine ticket PLM-004).

2026-08-26 — server authority (engine PLM-334, ADR 0124, protocol v2): /api/net gains a Server authority — typed input section (sendInput/inputFor/setEntityOwner/clientId/serverAckTick), the entity Maps gain "owner", the protocol notes cover v2 (18-byte envelope, 0x03 input channel, per-connection acks), and the arena worked example is REWRITTEN — WASD steers typed intent, the server integrates, your own shape is found by owner == clientId(). The movement hot path contains no string parsing end to end; strings remain for lifecycle only.

2026-08-26 — layered states + the aim representation (engine PLM-333, ADR 0123): /api/animation-controller gains a Layered states section — the [states] layer = "..." key (an authored blend tree living on a masked layer), playOnLayer(name[, fade]) / stopLayer(layer), the loader rules, and the aim-pose authoring pattern. Additive-layer correctness is now MEASURED on the real Character.blend rig (weight 0 = base exactly; a known delta applies once). ADR 0123 also freezes the aim wire representation (absolute yaw+pitch, quantized u16 each) for the future input protocol.

2026-08-26 — wire envelope + handshake (engine PLM-332, ADR 0122): /api/net gains startServer(port, maxClients) (the 1-arg form now capped by [Network] max_clients, 32 default), rtt(conn), serverTick() / serverTime(), and a Protocol notes section (version-gated handshake — “connected” = protocol agreed; the tick-keyed snapshot ordering; receive budgets). /configuration [Network] section updated: max_clients now also caps the Wren listen path (the one capped-by-default case). Networking landing card extended.

2026-08-26 — destruction surface (engine PLM-331, ADR 0121, follow-up): /api/character-controller gains destroy() (despawn a character’s physics presence; the wrapper safely no-ops afterwards) and /api/physics gains a Body destruction section — Physics.destroyBody(node) (returns whether a body existed; idempotent). Needed because hittable, solid characters made “a despawned enemy keeps its body forever” user-visible.

2026-08-26 — characters are hittable (engine PLM-331, ADR 0121): /api/character-controller gains a Physics-query identity section — the inner kinematic body (raycasts/sweeps/overlaps hit characters; dynamic bodies collide with them), the dedicated character collision layer (bit 1), the new bodyId getter, and the note that the camera occlusion collider masks characters out by default. /api/raycast gains RaycastHit.getBodyId() (hit-attribution for node-less physics hits; Physics.overlapSphere resolves characters to their bound node). /configuration documents the [Physics] character_inner_body kill switch (default ON).

2026-08-25 — /api/camera Updated: a note on the damping section recording that pivotDamping/aimDamping/lookahead default to 0 by measurement, not by omission (engine PLM-330, ADR 0120 addendum). Publishes the measured trade table and states plainly when to reach for them — interpolation disabled, or a genuinely jittery follow target. Otherwise zero damping is both smoother AND more responsive, because render interpolation removes the staircase these were compensating for.

2026-08-25 — /configuration Updated: the [Window] presentation-pacing keys — present_mode, frame_cap (engine PLM-328) and render_interpolation (engine PLM-329 / ADR 0120). Adds a “Smooth motion” section explaining the fixed-60 Hz vs display-rate staircase, why it is invisible walking and obvious sprinting, the measured before/after (worst jitter 0.1000 -> 0.0119), the guarantee that gameplay reads are unaffected because the interpolated pose exists only for the render half of the frame, and a tip for forcing a render/simulation rate mismatch to reproduce motion problems on purpose. Note: present_mode/frame_cap shipped in PLM-328 WITHOUT a docs sync; caught and closed here.

2026-08-24 — /api/camera Updated: engine PLM-323 / ADR 0118 — the camera damping repair and zoom. New CameraRig surface: pivotDamping / aimDamping / dampingAxes / lookahead, zoom / zoomRange / zoomStep / lensDamping, and 13 config getters (there were none, so a game could not read a value back to modify it — which is what a zoom step is). Documents that damping now covers three separate quantities (eye, anchor, look target); that a NEGATIVE dampingAxes value means inherit while 0 means snap; and that aim modes are rigs, not a mode — blendTo now carries look state, so an over-the-shoulder ADS is a second rig you blend to, with no aim concept in the engine. Also records the one behaviour change (shoulder(x, y) is camera-relative; identical at yaw 0) and adds the cameras-TOML keys — including invertX/invertY/pitchClamp, which ADR 0116 shipped as Wren setters and never gave TOML keys.

2026-08-24 — /api/net Updated (2): new Net.parseNum(text) — a numeric parse that returns null instead of RAISING. Engine PLM-326 / ADR 0119: Num.fromString aborts on an out-of-range literal, and the shipped apps/net_arena_server called it on wire text, so the 10-byte payload join:1e400 ended the dedicated-server process and every connected player’s session (reproduced end-to-end). Documented with a strictness table and a danger callout.

2026-08-24 — /api/net Updated: new Simulating an adverse link section documenting the [Network].sim_* config keys (sim_packet_loss_send/recv, sim_lag_send_ms/recv_ms, sim_packet_reorder_send, sim_reorder_time_ms) plus a worked example row for apps/net_conditions_demo. Engine PLM-324: GameNetworkingSockets already shipped these knobs and the engine set none of them, so a predictor or interpolator could not be tested anywhere but a perfect link.

PageStatusNotes
/api/aiUpdated 2026-07-31AI. Scriptable AI (engine PLM-189, ADR 0065): author a behaviour tree in Wren, engine-ticked. BehaviourTree/Bt/BtNode/Blackboard/BtStatus (closure leaves `{
/api/behaviour-tree-authoringNew 2026-08-01Data-driven BT authoring for the GUI editor (engine PLM-211…217, ADR 0068): named leaves so a GUI/asset can reference a leaf a closure can’t be. Bt.registerLeaf(name, paramSpec) {|bb,dt,params|} (arity-3, typed params) · Bt.leafCatalog (editor palette) · Bt.load(path) → an engine-ticked BehaviourTree from a hardened .bt.toml (bounded/cycle-checked; unknown-leaf/bad-param → Bt.loadError, tree intact) · the .bt.toml schema (sequence/selector/parallel/decorators/leaf/phase_switch). PhaseEvaluator.evaluate(currentPhase, health01, elapsed, thresholds) — forward-only boss phase the game feeds health01 (engine never reads HP) → a blackboard int a phase_switch reads; mini-boss = 2 entries, boss = 4+, same data. Additive to ADR 0065 (Bt.leaf closure unchanged). /security-review PASSED.
/api/light-sound-visionNew 2026-08-01Light/Sound/Vision as mechanics (engine PLM-199/203/206, ADR 0067): the engine samples, the game owns the policy. Illumination.illuminanceAt(scene,x,y,z,mask) → continuous 0..1 (per-light falloff matching the render + masked LOS; no binary helper; bidirectional). FogOfWar — world-XZ visibility field: configure (bake occluder grid) / beginFrame / reveal(x,z,radius) (radius scaled by illuminance = darkness shrinks view) / state→0/1/2 (persistent Explored) / mark+tick+isRevealed (wall-penetrating sonar); team + per-player. NoiseBus.emitOccluded(scene,…,mask) — a wall blocks what the AI hears. SoundEmitter — unifies audible + audible-to-AI. Frozen (ADR 0067). PLM-210: the fog now RENDERS on-screen — a depth-based post-process darkens the world by the field (any camera angle, no smear), with a Graphics.fogSoftness(v) crisp↔smooth dial; the per-scene fog state moved to WrenVm ownership (was a leaking static map).
/api/combatRewritten 2026-08-01 (ADR 0069)Action combat on Jolt sensor bodies (engine PLM-196 + PLM-198) — the engine detects hits, the game owns health. Shape (capsule/box/sphere). Hitbox/Hurtbox — bone-attached sensor boxes + .layer/.tag/.owner/.payload/.enabled; hitbox never hits its own owner, once-per-swing, bindWindow gates to a Blender-marker hit-window, bindController links a Blender-authored box to a controller; .independent/.reHitSeconds = flurry/DoT; disabled hurtbox = i-frames. Hit (read-only attacker/victim/hitboxTag/point/normal/payload). Combat.onHit/onStay/onExit(scene, fn) — lifecycle events, engine-ticked at the tail of update() (no game-called tick — it would re-enter the VM). Blender authoring: meshes (Passive+Animated rigid body, bone-parented) in Plume_Hitboxes/Plume_Hurtboxes collections (name = tag) surface via node.hitboxes/node.hurtboxes maps; node.attachToBone/detachFromBone is the engine-driven socket. No HP/damage/death type in the engine — the opaque payload is the only health-boundary wire. Fast swings don’t tunnel — after the discrete sensor pass the engine sweeps any hitbox that moved farther than its own thickness in one frame (PLM-221, shared once-per-swing dedup), so a very fast attack still lands; Physics.castShape stays the swept one-shot for hitscan outside the box system, and combat sensors never surface in Physics.* world queries.
/api/engineUpdated 2026-07-16Engine.exit(code) (ADR 0001, clamps 0–255, record-and-honor; exact codes in --test mode). 2026-07-16: added Engine.entropy() (ADR 0009 — non-deterministic 53-bit seed, std::random_device + high-res clock; explicitly not /api/random’s frozen determinism, and not cryptographic), and corrected the version format — it is YYYYMM.PPP (e.g. 202601.001), not Year.Month.Patch / 2026.1.1. That false claim was echoed here too.
/api/randomNew 2026-06-29Seeded, deterministic RNG (PCG32, ADR 0002): new(seed), int(n) [0,n), intRange(min,max) inclusive, float() [0,1), in-place shuffle(list). Frozen-sequence contract noted.
/api/coroutinesNew 2026-07-29Coroutines / Async (engine PLM-031, ADR 0037): one new Wren class Async — a cooperative coroutine scheduler over core Fiber. Async.run { ... } (start a zero-arg-block coroutine, runs to first suspend/return), Async.wait(seconds) (suspend for seconds of scene time), Async.nextFrame() (suspend to next frame), Async.pending (getter — count of suspended coroutines), Async.clear() (cancel all). The engine auto-ticks the scheduler each frame (after physics, before Game.update) — no per-game wiring; guarded so an app that never imports Async pays nothing. Cooperative + single-threaded (frame-scheduling, NOT parallelism/threads); a coroutine that never yields blocks the frame; deterministic under fixed-step/headless.
/api/httpNew 2026-07-16Blocking HTTP POST (ADR 0009, shipped 2026-07-10, undocumented until now): Http.post(url, body) → Map {ok, status, body}. Fixed limits: 1 MiB request cap (response uncapped), 5s connect / 10s read, redirects followed, JSON-only, POST-only, HTTPS via OpenSSL. No auth-header parameter — tokens ride in the URL query. The C++ error field is dropped by the binding, so script cannot tell why a send failed. Graceful ok=false on non-networking (console) builds.
/api/loggerCurrent—
/api/shaderCurrentMinimal; shader has only a name property
/api/meshUpdated 2026-08-03Covers all Mesh methods. Added render state (PLM-103): blendMode (“opaque”/“alpha”), cullMode (“none”/“back”/“front”), doubleSided, per-mesh alpha blending + culling for procedural meshes. 2026-08-03 (PLM-234, ADR 0073): added a Custom material (set 5) section — setCustomTexture(slot, texture) (slot 0..7), setCustomParamFloat/setCustomColor/setCustomParams (256-byte params blob), clearCustom — plus the set-5 binding contract and the plume3d.slang include, so a pack shader binds its own textures + params with no engine change.
/api/inputUpdated 2026-07-24Keyboard, mouse, gamepad (4 players). 2026-07-24 (engine PLM-120): gamepadButton/gamepadAxis now actually populate — the engine recorded a controller on connect but never called SDL_OpenGamepad, and SDL3 only delivers button/axis events for an opened pad, so every read silently returned false/0.0 engine-wide; the device is now opened on connect and closed on disconnect (page body already documented the intended behavior, so no API-surface change). 2026-07-16: corrected gamepadButton/gamepadAxis — the button/axis argument is a Num index only; the page promised “Num or String … name” but the binding reads wrenGetSlotDouble with no string path, so a name silently yields always-false / always-0.0. Added a Touch section stating plainly that touch is C++-only (InputState, 8 fingers) with zero Wren binding.
/api/windowCurrentCovers size, fullscreen, design size, scale modes
/api/graphicsUpdated 2026-08-07Updated 2026-03-25: added setLights, setShadowMappingEnabled, setShadowBias, setShadowMapSize, setDebugView. 2026-07-31: added perViewportOverlays(on) — replay the retained-UI HUD per split-screen cell (CAM-18, ADR 0064). 2026-08-03 (PLM-236, ADR 0072): added a Global frame uniforms section — setWind(x, y, z, strength) sets the app-owned wind of the frame block bound at set 0 binding 4 ({time, delta, frameIndex, wind}), the prerequisite for animated shaders; read via plume3d.slang (plume3d_time/plume3d_windDir/plume3d_windStrength). 2026-08-03: Post-process effects (A#5/A#2) + Opaque colour capture (A#3) sections (ADR 0074/0075). 2026-08-05 (PLM-241, ADR 0076, Block RFX A#6): added a Sky, fog & ambient (A#6) section — setAmbient(r,g,b) (raw-linear ambient fill, LightingData UBO tail, replaces the old 0.03 constant), setFog(r,g,b,density,start,end) (atmospheric distance fog — exp2 if density>0 else linear [start,end], a lit-shader term), setSky(shaderName, mode, params) (full-screen sky at z=far; mode 0 gradient / 1 cubemap; sky ABI plume3d_sky.slang) and setEnvironmentMap(handle) (mode-1 cube from Texture.loadCubemap). Calls out that this fog is DISTINCT from fog-of-war (fogSoftness/FogOfWar.*) + the v1 limits (single-viewport sky pass; env-cube IBL deferred, MoltenVK §5). 2026-08-05 (PLM-242, ADR 0077, Block RFX A#7): added a Render-to-texture capture (A#7) section — newCaptureTarget(w, h) / newDataCaptureTarget(w, h) (allocate a persistent capture target once → a Texture; sRGB colour vs UNORM/linear data) and submitCapture(target, view, proj) (per-frame in draw(), render the scene from a view/proj List of 16 Nums into the target). A capture IS a Texture → sample it via the set-5 path (Mesh.setCustomTexture), no new binding. Notes: submit in draw() not update(); single-camera; captures the whole scene incl. screen-space overlays (drawSubset is a follow-up). 2026-08-05 (PLM-239, ADR 0078, Block RFX A#4): added a Planar reflection (A#4) section — setPlanarReflection(enabled, planeY) (render the scene mirrored across the horizontal plane y=planeY into a half-res target; sticky, single-camera, cost = a second opaque draw) and setPlanarReflectionScale(scale) (target size as a fraction of the swapchain; default 0.5 = half-res). The reflection is exposed globally at mesh set 0 binding 7, sampled screen-space by a water/floor shader via plume3d_reflection(screenUv) (#include "plume3d.slang"), no per-draw binding. Notes: the reflector must be TRANSPARENT (blendMode = "alpha") so it’s excluded from its own opaque-only reflection; content should be above the plane; single-camera; oblique below-plane clipping is a follow-up. 2026-08-05 (ADR 0079, Block RFX A#6 follow-up): added setFogHeight(baseY, falloff) beside setFog — exponential HEIGHT fog (aerial perspective) on top of the distance fog; geometry below world-height baseY hazes into the sky at the horizon (falloff = 0 = off), a per-fragment lit-shader term riding the fog UBO tail’s former pad (no layout change); set the fog colour = the sky horizon for a seamless blend. Same follow-up fixed setAmbient/setFog being silently inert in the first A#6 build (the lighting UBO was built from defaults) — both now apply (a note added to their entries); apps that never called them are byte-identical. 2026-08-07 (engine Block WTR2 / Water Fidelity, ADR 0094): added a Water & camera submersion (WTR2) section — setWaterPlane(surfaceY, fogR, fogG, fogB, fogDensity) (register an infinite flat water surface + its underwater fog; sticky renderer state) and cameraWaterState() → List [submerged, amount, waterLevel, fogR, fogG, fogB, fogDensity] (the camera position recovered engine-side from the view matrix; amount is the smooth 0→1 submersion to cross-fade the underwater post/waterline on). Two frozen additive methods; the underwater effect itself is a game-driven addPostEffect. 2026-08-07 (engine WTR2 #3/#4): added a Graphics.setWaterVolume(minX,minZ,maxX,maxZ,minY,surfaceY,fogR,fogG,fogB,fogDensity) section — a BOUNDED water box beside the infinite setWaterPlane (cameraWaterState reports submerged only inside it); it is exposed to Wren (engine 63b6ab8) and is auto-called by the plume3d_water_volume loader (ADR 0097). This supersedes the earlier “setWaterVolume NOT yet exposed / infinite plane only” scope note — both a plane and a bounded volume are now drivable; only the screen-space underwater approximation remains a documented follow-up (a world-space pass is future work). See /examples/water-playground + /examples/blend-water-demo. 2026-08-19 (engine PLM-319): added Graphics.setSkeletonOverlay(mode) to the Debug views section — draw each skinned node’s bone hierarchy as screen-space lines from the engine’s own runtime bone matrices (0 off / 1 deform bones / 2 deform + a dim rest-pose reference / 3 every bone incl. IK+control). It is the only view of a skinned character that bypasses the skinning shader, so it separates a wrong POSE from wrong SKINNING; colour encodes hierarchy depth. Also documented setDebugView modes 10/11/12 (skinning weight heat map / colour per bone index / influence count, implemented per-shader) and corrected the availability note: debug views were described as “no-ops in release builds” and in fact were — they were gated on a Debug-only macro, so the Release binary every game ships silently ignored them. They are now gated on the shipping variant and warn once when compiled out. Added Graphics.setSkinnedMeshHidden(on) alongside it — hide skinned meshes while still posing them, so the bone lines can be read on their own (the armature is still evaluated, so the overlay shows the pose the hidden mesh would have used; static geometry still draws as a size reference).
/api/guiCurrentUpdated 2026-03-25: expanded from 7 to full API (~100 methods). Bug fixed 2026-03-25: all color parameters corrected to 0–255 (not 0–1); separator last param documented as Bool (not rounding radius); colorPicker return corrected to 0–255; setWindowAlpha stays 0–1.
/api/audioUpdated 2026-07-29 (PLM-133)Covers global audio, Source 3D. 2026-07-29 (PLM-133, ADR 0042): new Streaming audio section — Audio.newStreamingSource(sampleRate, channels) (a Source fed over time; mono/stereo; null on failure), source.queueSamples(list) (queue interleaved floats in [-1,1]; (re)starts playback; buffers recycled; 1M/call cap; no-op on a file-backed source), source.queuedBuffers (poll to pace refills). Additive to Audio/Source (no new class); the one-shot newSource(path) is unchanged. Uses AL_EXT_FLOAT32 with an int16 fallback; main-thread fed.
/api/mixerNew 2026-07-30 (PLM-147)Mixer + MixerGroup — Unity-style audio mixer groups (buses) routing Sources + SoundFontInstruments (ADR 0046). Mixer.master, Mixer.group(name) (get-or-create), Mixer.groupUnder(name, parent); MixerGroup.name/volume/volume=/muted/muted=/soloed/soloed=/addSource(_)/addInstrument(_). Effective gain (group × ancestors, mute/solo) applied to every routed source each frame; user setVolume preserved. Hard cutover: SoundFontInstrument.update() removed (engine auto-services instruments).
/api/midiNew 2026-07-30 (PLM-135)Midi — live MIDI device input (USB/virtual keyboard) as normalized events (ADR 0044). Static facade: Midi.inputDevices (List of names), openInput(index|name) → Bool, closeInput(), isOpen, openedDevice, poll() → List of {type,channel,data1,data2,value14} maps. Parsed + queued on the device thread, delivered to Wren on the main thread each frame. Desktop only (no-op otherwise). Note source is arbitrary (feed a SoundFontInstrument). Live note-in needs hardware (HUMAN-017); parser + binding + enumeration are CI-tested.
/api/soundfontNew 2026-07-30 (PLM-134)SoundFontInstrument — loads a SoundFont (.sf2) bank and plays it as real polyphonic audio through the streaming sink (ADR 0043). SoundFontInstrument.new(path, sampleRate, channels) (loaded reports success), noteOn(note, velocity) / noteOff(note) / allNotesOff, setPreset(index), controlChange(controller, value), pitchBend(value14), setMaxVoices, setGain, presetCount / presetName(i) / activeVoices. The engine renders + queues its audio automatically each frame (no update() — removed in PLM-147). Banks are loaded resources (git-ignored, fetched at runtime). Note source is arbitrary; routes to a mixer group via MixerGroup.addInstrument.
/api/sceneUpdated 2026-08-06findNodeById, createCharacterController, createParticleEmitter, createInstancedMesh, findInstancedMesh. 2026-08-06 (Block BLD): loadBlendScene(path) (Loading a Blender scene section) — load a .blend and instantiate its whole environment (terrain + scatter + static props) in one call, returning the BlendResult so scripted units stay explicit; documents the composition and the instantiate-by-node_id double-draw caveat (PLM-253, ADR 0088). 2026-08-06 (PLM-267, ADR 0090): loadBlendScene/instantiateUnits note each static prop also gains a configurable static Jolt collider from its plume3d_collider tag. 2026-08-06 (PLM-274, ADR 0092): loadBlendScene now also builds plume3d_water-tagged meshes as auto-drawn water surfaces (composes the new instantiateWater after terrain, before units). 2026-07-16: corrected addNode(name) — name is required, not optional. Wren makes arity part of the signature, so addNode() aborts the fiber; pass "" for an unnamed node.
/api/nodeUpdated 2026-08-01Added setQuat(x,y,z,w) — exact, gimbal-free quaternion rotation (ADR 0063). Combat (PLM-198, ADR 0069): hitboxes/hurtboxes (Blender-authored tag→box maps on an instantiated character), attachToBone(character, controller, bone)/detachFromBone() (engine-driven weapon/VFX bone socket). Documented on /api/combat.
/api/cameraUpdated 2026-08-09The camera rig system (ADR 0058): Cam factory (thirdPerson/firstPerson/topDown/orbit/freeFly + setActive/blendTo/active), CameraRig fluent setters (incl. collider(padding) — wall pull-in, ADR 0059), Impulse.emit, Ease. Plus camera assets from TOML (ADR 0104): Cam.loadAssets(scene, path) builds a scene’s rigs (+ an optional [splitscreen] layout) from a cameras TOML in one call — each [[rig]] picks a preset by type, applies overrides, resolves target by SceneNode id, and marks the active rig live — and Cam.rig(scene, name) looks a rig up by its TOML name (e.g. to blendTo it); the method names + TOML keys are a frozen contract, the keys mirroring the CameraRig setters. Plus split-screen (ADR 0059): SplitScreen (new(scene, players) + layout/distance/height/dynamic/mergeDistance/splitDistance/transitionDuration/isMerged/mergeFraction/playerCount) and SplitLayout (auto/horizontal/vertical/grid/angled) with dynamic merge/split — an animated blend (ADR 0060) — plus the RTT composite path (compositeMode), the angled 2-player diagonal split, and the animated rect-peel (peel) (ADR 0061). Plus the raw Viewport primitive for picture-in-picture / minimap (ADR 0062). Plus .blend cameras drive the rig system (ADR 0106): Cam.fromNode(scene, node) builds a static rig pinned to a node’s authored world pose and adopts its Scene.Camera lens (blend focal length → FOV, near, far) — the way to drive a Blender-authored camera through the director — and the cameras-TOML type = "static" is now a real fixed rig (was a v1 stub aliased to thirdPerson) that pins to its target node’s pose and adopts a .blend camera’s lens. The low-level Camera (view/projection, world/screen) class is kept below the rig sections.
/api/lightCurrentUpdated 2026-03-25: added castsShadows / castsShadows=
/api/raycastCurrentCovers all Raycast methods and RaycastHit
/api/configUpdated 2026-07-162026-07-16: added the previously undocumented Config.save(path, tomlText) (the page covered only half the class while marked Current) — TOML text persistence to the write dir, not scene serialisation. Also corrected Config.load’s documented return: the root is always a Map (the List/primitive mapping applies to nested values only).
/api/saveNew 2026-07-29Save/Load (engine PLM-022, ADR 0036): SaveState — typed key/value store (bool/int/number/string): setBool/setInt/setNumber/setString, getBool/getInt/getNumber/getString (1-arg + 2-arg-with-fallback), has, remove, clear, count, keys. Save static facade — write(name,state)→Bool, read(name)→SaveState|null, exists(name)→Bool, over the app’s PhysicsFS write dir (parent dirs auto-created). Versioned human-readable TOML ([meta].format_version + [data]); a newer-format save reads as null (rejected, not corrupted). Game-authored, not an automatic scene dump. Slots via Resource.list (no Save.list).
/api/resourceUpdated 2026-08-06Covers Resource, BlendResult. 2026-08-06 (Block BLD): BlendResult gains instantiateScatter(scene) (emit the blend’s baked foliage/scatter as one GPU-instanced mesh per prototype — PLM-252), instantiateTerrain(scene) (a plume3d_terrain-tagged mesh → renderable ground + an automatic static triangle-mesh collider, creating the physics world if absent — PLM-254), and instantiateUnits(scene) (every remaining static mesh prop as a render node — skips terrain, the scatter prototype by node_id, hide_render objects, and class_name-scripted subtrees so nothing double-draws — PLM-253, ADR 0088). Reached in one call via Scene.loadBlendScene. 2026-08-06 (PLM-267, ADR 0090): new Blender-authored colliders (plume3d_collider) subsection under instantiateUnits — a plume3d_collider tag (none/box/sphere/capsule/convex/mesh) + plume3d_collider_layer (collision-layer bitmask) + plume3d_collider_static (1 static / 0 kinematic; mesh always static) makes each imported object build a static Jolt collider on import (primitives sized from bounds with scale baked in; convex/mesh from geometry), generalizing the terrain auto-collider to any object. 2026-08-06 (PLM-274, ADR 0092): BlendResult gains instantiateWater(scene) — every mesh tagged plume3d_water (a string naming the water shader: toon→water_toon, pbr→water_pbr, else verbatim as a shader basename; shader stays pack content) becomes a transparent, auto-drawn stylized water surface, with optional float-array look overrides plume3d_water_shallow/_deep/_foam/_waves/_horizon (set-5 slots p0/p1/p2/p3/p10, NaN-guarded). Under the hood a SceneNode now carries an optional CustomMaterial (the general “scene node drawn with a pack shader + set-5 params” capability, additive); the engine fills the camera world position into slot p9 each frame. Reached in one call via Scene.loadBlendScene; instantiateUnits skips plume3d_water meshes so no double-draw.
/api/animationCurrentUpdated 2026-03-25: added blend layers, crossfade, stopLayer, full AnimationPlayOptions (loop, speed, timeOffset, blendWeight, layer, crossfadeDuration)
/api/animation-controllerUpdated 2026-07-24 (PLM-123)AnimController (engine PLM-083/084/085/123, ADR 0017/0034): per-character controller via node.attachController(path). Typed params (setFloat/Int/Bool/setTrigger + getters), direct-play escape hatch (play(clip|namedTransition)), Actions (action(name) — masked overlays over the current State, completing on END) + Contexts (modal param gates), Layers (setLayerWeight/layerWeight by authored name or index — masked pose overlays over the single graph, NOT per-layer state machines; documented for Unity users), clip phase (currentState, normalizedTime, inSection), behaviour hooks (onEnter/onUpdate/onExit/onEnd — latch-and-refire END), read-only rootMotionDelta, tick. Engine auto-ticks + hot-reloads attached controllers. TOML controller-asset schema fully loaded — [controller]/[params]/[clips]/[nodes] (blend1d/blend2d)/[transitions]/[states]/[[layers]]/[actions]/[contexts]/[root_motion]; malformed assets rejected at the parser boundary.
/api/mathCurrent—
/api/physicsUpdated 2026-08-07Physics class: forces, impulses, velocities, gravity, pause, raycast, contact events (onContactBegin/onContactEnd/onContactStay, plus the 4096-event queue cap and its manual-stepping interaction); box/sphere/capsule/cylinder/convexHull bodies (static+dynamic), static triangle-mesh + heightfield colliders, material setters (restitution/friction/damping); manual stepping (step/stepN/fixedDeltaTime) for headless/deterministic sim. Combat shape queries (PLM-196, ADR 0066): castShape (swept, all bodies crossed), overlapShape (at-pose narrow-phase), raycastAll (piercing) → a List of RaycastHit, layer-masked — the swept/overlap/pierce primitives shared with the combat queries. PLM-198/ADR 0069: combat hit/hurt sensor volumes are excluded from these world queries (a bullet/LOS ray must not stop on an invisible hitbox). Contact callbacks (ADR 0016, PLM-064): a single PhysicsContact argument — getNodeA()/getNodeB() (name, "" if unbound), getNodeAId()/getNodeBId() (numeric id, 0 if unbound), getPoint*/getNormal*, hasManifold(). PhysicsContact documented inline as a sub-section. Wrong-arity callbacks are rejected at registration. 2026-07-16 corrections still current: getBodyCount returns all bodies (incl. static and sleeping), not active ones; raycast directions should be normalised (magnitude scales both ray length and the reported distance). (The earlier “callbacks pass a String name, not a Node” correction is superseded by the PhysicsContact object above.) 2026-08-05 (PLM-247, ADR 0083, Block SPL): added a Surface & ground queries section — raycastDown(scene, x, y, z, maxDist) (cast straight down), groundPoint(scene, x, z) (project a column onto the nearest ground below), randomPointOnGround(scene, minX, minZ, maxX, maxZ, seed) (a seed-deterministic pick in a rect, projected down), each returning a flat [hx,hy,hz, nx,ny,nz] List (world point + surface normal) or null. They only hit registered colliders (addStaticMesh/addStaticHeightfield); start just above the surface for precision. The placement primitive behind scatter + terrain-conforming spline extrusion. 2026-08-06 (PLM-271, ADR 0091, Block WATER): added a Buoyancy section — applyBuoyancy(node, water, time, offsets, strength, density), multi-point Jolt buoyancy on the stylized water. Reads surface height from a Water (so physics matches the render); offsets = a flat List of local [x,y,z] probe points (a raft’s 4 corners → rocks/tilts; empty [] → a single centre bob); force per probe = strength · submersion / density applied upward at that world point via addForceAtPoint; density (rel. to water, default 1.0, clamped ≥ 0.05) is the responsiveness knob — low floats high + pops out snappy, high sits low + sluggish. Call every frame. v1 samples in the body’s level frame (full-orientation probes = a refinement). 2026-08-07 (engine WTR2 #4, ADR 0098): the water param note now points out that water rendered with the Realistic variant should call Water.setRealistic(true) so the sampled height mirrors the drawn 5-octave swell. See the Buoyancy Demo.
/api/character-controllerNew 2026-03-25CharacterController + CharacterState constants
/api/instanced-meshUpdated 2026-08-05InstancedMesh: addInstance, setInstancePosition, setInstanceTransform, GPU instancing (SSBO). 2026-08-05 (engine PLM-248, ADR 0081, Block SPL enhanced instancing): added a Per-instance colour, params & culling section — addInstanceColored(x,y,z,scale, r,g,b) (add an instance carrying a per-instance base colour → index), setInstanceParams(index, scaleMul, windPhase, lodBias) (per-instance shader params: scaleMul height/scale, windPhase wind-sway offset, lodBias reserved/inert), setCullingEnabled(enabled) (opt-in per-instance frustum cull — draws only instances whose world AABB is in view) + the cullingEnabled / visibleCount (instances drawn last frame — diagnostic) properties. Rides the widened 96-byte instance SSBO record [transform, baseColor, params] (Plume3D_Instance in plume3d.slang) — no extra buffer/descriptor, transform stays at offset 0.
/api/particlesUpdated 2026-08-08ParticleEmitter: play/stop/burst, setPosition/setNormal, setMeshFromNode. 2026-08-08 (engine main, ADR 0102): sprite render modes — setRenderMode (0 PhysicsMesh (default) / 1 SpriteGround / 2 SpriteBillboard), setScaleCurve(startHalfSize, endHalfSize), setAlphaCurve(startAlpha, endAlpha), setColor(r,g,b), setTexture(texture) — non-physics pinned kinematic sprite decals (surface-pinned ripple rings + camera-facing sparkle) that back water splashes/ripples, reusing the existing sprite pipeline with no Vulkan renderer change.
/api/netNew 2026-07-03Networking transport (GameNetworkingSockets, ADR 0003): Net.startServer, connect, send(conn,bytes,reliable), poll() → List of {type,conn,payload} Maps, close, isServer, connectionCount. + Replication (ADR 0005): setReplicatedEntity, removeReplicatedEntity, clearReplicatedEntities, replicatedEntities(), replicatedEntitiesLerp(alpha). Static facade; desktop-only; NUL-safe byte payloads.
/api/playflowNew 2026-07-03PlayFlow cloud hosting client (ADR 0004): PlayFlow.isConfigured, requestServer(region,customData) → instanceId, serverInfo(id) → {ok,status,host,port,error}. Static facade; key from PLAYFLOW_CLIENT_KEY env. Matchmaking flow + Docker packaging documented.
/api/spriteNew 2026-07-08Billboarded sprites (ADR 0008, PLM-009): Sprite.new(texture); size/color/billboard (0 spherical / 1 cylindrical / 2 flat); drawAt(x,y,z). Camera-facing, alpha-blended, depth-sorted quads.
/api/textureUpdated 2026-08-05Texture.load(path) → GPU texture (PNG/JPG/WebP from the mounted project); width/height/valid. Used by Sprite (ADR 0008, PLM-009). 2026-08-05 (PLM-241, ADR 0076, Block RFX A#6): added a Cubemaps section — Texture.loadCubemap(paths) (static): 6 face images in order +X,-X,+Y,-Y,+Z,-Z (all the same square size) → a cube handle Num (0 on failure) for Graphics.setEnvironmentMap + the mode-1 cubemap sky.
/api/splineNew 2026-08-05A 3D spline / curve primitive (engine PLM-243, ADR 0080, Block SPL keystone) — one new Wren class Spline. Build: Spline.new(), addPoint(x,y,z) / addPointFull(x,y,z,width), setType("catmullrom"|"bezier"), setClosed(Bool), setRoll(radians). Read: pointCount / length. Evaluate by parameter t∈[0,1] (position(t) / tangent(t) → [x,y,z]) or by arc-length distance s (constant-speed: positionAtDistance(s) → [x,y,z], tForDistance(s) → Num). Orientation frames for mesh extrusion: frameAtDistance(s) → a rotation-minimizing flat 13-number frame [px,py,pz, tx,ty,tz, nx,ny,nz, bx,by,bz, width], sampleFrames(count) → a List of frames evenly spaced by arc length. Documents the vector = [x,y,z] List / frame = flat 13-number List formats and the load-bearing t is NOT constant-speed note.
/api/mesh-genUpdated 2026-08-05Procedural mesh factories (engine PLM-244, ADR 0082, Block SPL) — one new Wren class MeshGen of static factories, each returning a standard Mesh: MeshGen.plane(width,depth,subX,subZ) (flat XZ plane, subX×subZ quads, faces +Y), grid(nx,nz,cellW,cellD) (exactly nx×nz verts — terrain base), box(sx,sy,sz) (AABB ±(s)/2, per-face normals), cylinder(radius,height,segments,caps) (about Y; caps adds fans), cone(radius,height,segments,cap) (apex +h/2, base −h/2), heightmap(heights,nx,nz,cellW,cellD,yScale) (flat nx·nz List, row-major X-fastest → displaced grid + area-weighted normals; wrong count → empty). Documents the conventions — centered at origin, Y up, unit per-vertex normals, UVs [0,1], CCW-outward, params clamped — and the load-bearing tangents deferred (v1 = normals only) note. Device-free modules/geometry core; the binding assembles the Mesh. 2026-08-05 (PLM-245, ADR 0084, Block SPL spline extrusion): added a Spline extrusion section — MeshGen.ribbon(spline, width, samples, conformScene) (a flat ribbon swept along a Spline; pass a Scene as conformScene → each station snaps onto the ground via the surface queries so the ribbon hugs terrain, null = flat) and MeshGen.tube(spline, radius, sides, samples) (a capped closed tube). Both sample the spline’s rotation-minimizing frames and return a Mesh; device-free modules/geometry/extrude linking only modules/spline, conform is an injected physics callback. Conform only sees registered colliders. 2026-08-13 (PLM-273, ADR 0113, per-bend river flow): ribbon gains an optional 5th flow argument (a new additive signature; the 4-arg form is unchanged) — flow=true bakes each station’s downstream tangent into the ribbon’s vertex colour so a river water shader scrolls foam along the local bend (leave off for lit ribbons that tint by vertex colour).
/api/scatterNew 2026-08-05Scatter / placement (engine PLM-246, ADR 0082/0085, Block SPL — last of the trio) — one new frozen Wren class Scatter of static factories that fill an InstancedMesh in C++ (never a 10k-item Wren List, no per-instance callback) and return the count placed: Scatter.onSurface(mesh, scene, minX, minZ, maxX, maxZ, count, seed) (project count candidates onto the ground; natural scale/slope-align defaults; misses drop), Scatter.onSurfaceFull(…, minScale, maxScale, slopeMaxDegrees, alignToNormal, seed) (full knobs — scale range, reject slopes steeper than slopeMaxDegrees, alignToNormal 0=upright…1=on the slope), Scatter.alongSpline(mesh, spline, count, widthJitter, minScale, maxScale, seed) (line instances along a Spline, jittered ±widthJitter). Load-bearing: scale is baked into the transform (mirrored in params.x — a shader must not re-apply it as scale); baseColor left default (prototype colour shows); seed-deterministic (PCG32/SplitMix64, the Random family); surface placement needs a registered collider (addStaticMesh/addStaticHeightfield); one GPU-instanced mesh per frame → fill one mesh from several factories. Device-free modules/geometry/scatter core with an injected ray-down.
/api/waterUpdated 2026-08-07A CPU mirror of the stylized water surface’s Gerstner waves (engine PLM-270, ADR 0091, Block WATER) — one new frozen Wren class Water. Holds the wave params so one object drives both the rendered surface and the height query (they share water_wave.h ↔ water_common.slangh — one formula, can’t drift). Configure: Water.new(), setWaves(gain, wavelength, steepness, speed), setWind(x, z) (match Graphics.setWind’s x,z — wind is global), setBaseY(y) (still-water surface Y). applyToMesh(mesh) writes those waves into the mesh’s set-5 p3 (byte 48) so render matches query; heightAt(x, z, time) → the surface Y at world (x,z) and an explicit time (the game’s accumulated dt, tracking the shader clock — pass the same time to Physics.applyBuoyancy). Load-bearing: heightAt is a gentle-wave approximation (evaluates height at the query XZ, ignoring the Gerstner horizontal displacement — exact in phase/period, accurate for steepness ≲ 0.4). 2026-08-07 (engine WTR2 #4, ADR 0098, commit 122c46d): added a setRealistic(v) section — an additive method that switches the height query to the 5-octave mirror of the water_realistic surface (default false = the legacy 3-wave, unchanged for every existing consumer). A game that renders the Realistic variant calls setRealistic(true) alongside setWaves so buoyancy tracks the drawn swell (the mirror is unit-tested against the shader’s 5-octave vertex so the two can’t drift); apps/water_playground is the reference. Documented with the Buoyancy Demo + River Demo. Adds setSeascape(v) — the Alekseev seascape octave mirror (ADR 0099) — alongside setRealistic, for buoyancy on the water_seascape surface.
/api/textNew 2026-07-08SDF text (ADR 0007, PLM-010): Text.new(content); text/size/color/outlineColor/outlineWidth/softness/align/wrapWidth/lineSpacing/richText setters; width/height/lineCount; draw(model) (world-space) + drawScreen(x,y) (HUD). Rich-text <color>/<size> markup. Single-channel SDF, runtime-baked from Inter; frozen-contract layout (golden tests). <b>/<i> deferred (PLM-043).
/api/uiUpdated 2026-07-29 (PLM-139/140/141/142/143)Retained-mode UI (ADR 0019, PLM-104–108, Block U): Ui.load(path) → UiDocument; Ui.render(doc, x, y, w, h) (letterboxed, resolution-independent; runs pointer interaction; a button released inside fires Game.<on_click>()); Ui.get(doc, id) → UiElement (valid/id/visible/text/onClick/resolved x/y/width/height). Documents the .ui TOML format: [document] + [[element]] tree, parent-by-id, uGUI anchors/offsets, panel/text/button widgets, 9-slice borders. Panels/images draw as quads; labels reuse the SDF Text pass. Codec is untrusted-input-hardened (a /security-review found no exploitable issues). U-M1 slice is Panel/Text/Button; more widgets + a visual editor are U-M2/M3/M4. 2026-07-29 (PLM-139/140, ADR 0038): new Textured panels & 9-slice section — textured 9-slice now actually renders (fixed corners, edges stretch one axis, center stretches both; the nine_slice/border fields previously round-tripped but were inert), the element can reference an image by path (image = "ui/panel.png", a mounted PNG resolved once on render, PhysFS-sandboxed) alongside the legacy integer texture handle, and slicing can be authored once in a .ui.component TOML sidecar (<image path>.ui.component, nine_slice+border) that a path-referenced image picks up automatically. Additive to the frozen .ui codec (existing files load identically); PNG only. 2026-07-29 (PLM-141, ADR 0039): two new element types — type = "progress" (a display-only progress bar reusing the slider’s value/range model: value/color/fill_color, no handle or interaction) and type = "radio" (a grouped toggle: checked/check_color/on_change + a group string; radios sharing a non-empty group are mutually exclusive — exactly one selected, never toggles back off). Both are polled via the existing value/checked accessors and created via `Ui.createElement(doc, “progress"
  • Touch input — InputState tracks up to 8 touch fingers (SDL_FINGER_DOWN/UP/MOTION, input_state.cpp) but there is no Wren binding at all — grep -riE 'touch|finger' engine/integrations/wren/ returns zero matches. (Corrected 2026-07-16: this bullet used to say there was no section “beyond the touch mention”, implying partial exposure; there was never any. /api/input now states the gap explicitly, and the landing Input card no longer advertises touch as scriptable.)
  • BlendResult.classes / BlendResult.configs — Not fully documented (just noted on /api/resource). These return the list of class names and config paths found in the blend; may want example usage added.
  • Material textures via Wren — no way to assign a texture to a mesh’s PBR material from Wren: the handles exist in the C++ PBRMaterial struct and no binding sets them. (Corrected 2026-07-16: this bullet used to say “No loadTexture API exists”, which is now false — Texture.load(path) ships (see /api/texture), but it feeds sprites, not PBR materials. Textures also reach materials via BlendResult.instantiate from .blend data. The gap is the Wren PBR-material setter specifically.)
  • SkeletalAnimation via Wren — The C++ engine has full bone skinning (ArmatureState, BoneState, GPU bone matrix SSBO), but the Wren API exposes animation only through AnimationState. Direct bone matrix access is not yet exposed. Document if/when bone override API is added.
  • Physics constraints — no Wren builder: C++ supports Fixed/Point/Hinge/Slider/Cone/SixDOF (ConstraintSettings) and there is no Physics.addHingeConstraint etc. (Clarified 2026-07-16: constraints are nonetheless reachable from Wren via BlendResult.instantiate, which builds them from .blend rigid-body-constraint data — see apps/physics_constraint_hinge. The gap is authoring one from script, not using one.)
  • Physics shapes beyond box — resolved 2026-07-15 (ADRs 0011/0013/0014): sphere/capsule/cylinder/convexHull (static+dynamic), addStaticMesh (triangle mesh), and addStaticHeightfield (new C++ ShapeType::HeightField) are Wren-exposed. Remaining by design: dynamic mesh/heightfield bodies are impossible (Jolt mesh/heightfield shapes are static-only).
  • Particle Tier 1 (GPU compute) — The ParticleTier enum has GPU and Hybrid tiers in C++ but only Tier 2 (Jolt physics) is currently wired to Wren. Document when implemented.
  • Audio reverb/EFX — resolved 2026-07-30 (engine PLM-153/154, ADRs 0050/0051): the OpenAL EFX extension is exposed per mixer group — MixerGroup.addEffect(type) / addReverbPreset(name) / clearEffects + MixerEffect.setParam cover the whole aux-slot suite (reverb, echo, chorus, flanger, distortion, EQ, pitch, ring-mod, autowah, frequency-shifter, vocal-morpher, compressor), chainable. See /api/mixer and /examples/audio-effects-demo. Direct-path low/high/band-pass filters are now exposed too (engine PLM-157, ADR 0052): MixerGroup.setLowPass/setHighPass/setBandPass/clearFilter on the group’s dry signal — see /api/mixer Filters. The native OpenAL EFX surface is now fully exposed.
  • Coroutines — resolved 2026-07-29 (engine PLM-031, ADR 0037): the engine-integration gap is closed. New public Wren class Async is a cooperative scheduler over the VM’s core Fiber (vendor/Wren/src/vm/wren_core.wren) that the engine auto-ticks every frame — Async.wait(seconds) (scene-time delay) and Async.nextFrame() (yield to next frame), plus Async.run/pending/clear. See /api/coroutines and /examples/coroutine-demo. (The 2026-07-16 correction stands: the VM always shipped Fiber; what was missing was the engine hook resuming a fiber across frames on the game clock — that is what this adds.) Deferred by design (ADR 0037): async/await keywords and promise-based async I/O over Http/Net can layer on the same scheduler later.
  • Save/load — resolved 2026-07-29 (engine PLM-022, ADR 0036): game-authored, versioned SaveState + Save now ship — see /api/save and /examples/save-load-demo. By design this is a game-authored key/value store, deliberately not automatic scene serialisation (rejected in the ADR as brittle across engine versions) — the game persists what it chooses. Remaining by design: only scalar values in v1 (bool/int/number/string; structure via key naming), and saves live under the app write dir, not a per-user OS data directory (a future, additive extension). Cloud sync is the sibling engine PLM-138 (blocked on HUMAN-019).

PageAppStatusNotes
/examples/ai-huntersapps/ai_huntersUpdated 2026-07-31AI Foundation full loop (engine PLM-189, ADR 0065): four hunters run a Wren-authored behaviour tree — perceive by sight (Ai.nearestVisibleTarget = range + FoV + masked-raycast LOS against a static obstacle body) and sound (NoiseBus) → navmesh-chase to a SurroundRing slot + pick a Tactic → when the target flees + goes silent, lose it (LostTargetTracker) → search the last-known point → give up and return to spawn. Seeded, navmesh-snapped scatter; hunters coloured by FSM state (engage/search/give-up); render-verified. Also documents the world-space render pattern (apply the scene camera each frame).
/examples/bt-editorapps/bt_editorUpdated 2026-08-01BT GUI editor (engine PLM-217/218, ADR 0068): author a tree as data — a palette from the live Bt.leafCatalog (+ composites/decorators + a PhaseSwitch node), a canvas (boxes + parent→child wires, click to select, drag to move), a schema-driven inspector (typed param sliders for the selected leaf), Delete subtree, Save + validate → trees/enemy.bt.toml round-tripped through Bt.load, and Run 1 tick (a live preview). --test-verified surface; windowed run clean.
/examples/bt-boss-demoapps/bt_boss_demoNew 2026-08-01The payoff (engine PLM-219, ADR 0068): a phaseless enemy, a 2-phase mini-boss, and a 4-phase boss authored from one .bt.toml schema/loader/editor, differing only in data. HP is game-owned → fed to PhaseEvaluator → phase_switch; the HUD meters HP + phase + action + a phase-enter banner. Render-verified (a boss crossing a threshold + switching subtree while the same-HP mini-boss has not). --test-verified surface + test_bt_load_wren mechanics.
/examples/lsv-demoapps/lsv_demoNew 2026-08-01Light/Sound/Vision (engine PLM-199/203, ADR 0067): a player crosses light and shadow while two live HUD bars meter EXPOSURE (Illumination.illuminanceAt, respecting the wall’s shadow) and NOISE (game-computed), with FogOfWar reveal whose radius shrinks in the dark. The engine samples; the game owns the policy (thresholds/HP). --test-verified surface + test_illumination_wren/test_fog_wren/test_noise_occlusion_wren mechanics.
/examples/fog-demoapps/fog_demoNew 2026-08-02 (PLM-210)On-screen fog of war: a top-down 3D scene where the player wanders, the game calls FogOfWar.reveal each frame, and the ENGINE darkens the world by the field (unknown→black / explored→dim+desaturate / visible→full) — a depth-based post-process, no game render code, pixel-exact at any camera angle. Shows Graphics.fogSoftness (crisp↔smooth). Render-verified top-down + oblique (no smear); --test-verified surface.
/examples/combat-demoapps/combat_demoUpdated 2026-08-01 (ADR 0069)Action combat with hand-built boxes (engine PLM-196): a sword Hitbox (a Jolt sensor) lunges into overlap with a body Hurtbox; Combat.onHit reads the hitbox’s opaque payload as damage off a plain Wren _hp field; on death the game disables the hurtbox (a corpse stops taking hits). No engine HP type — health is 100% game-side. --test-verified surface + test_combat_wren mechanics. (Blender-authored path → combat-blender-demo.)
/examples/combat-blender-demoapps/combat_blender_demoNew 2026-08-01 (PLM-198, ADR 0069)Blender-authored combat: CombatVolumes.blend carries a Fighter with a sword hitbox + body hurtbox authored in Blender (bone-parented meshes in Plume_Hitboxes/Plume_Hurtboxes). Instantiated twice; owners set game-side off the node.hitboxes/hurtboxes tag maps; a lunge lands via Combat.onHit (engine turns each volume into a bone-attached Jolt sensor). No hitbox geometry in code. Render-verified 2 strikes; --test asserts the volumes surface.
/examples/camera-showcaseapps/camera_showcaseNew 2026-07-31Camera system end-to-end (Camera epic, ADR 0058): Cam.thirdPerson follows a moving player, Cam.blendTo eases to an orbit rig (with noise sway), Impulse.emit shakes — all with draw() = just scene.draw() (no camera plumbing). Render-verified.
/examples/camera-toml-demoapps/camera_toml_demoNew 2026-08-09Camera rigs authored as data (engine ADR 0104): the whole camera setup is one call — Cam.loadAssets(scene, "cameras.toml") builds a third-person follow rig (active) + an overview orbit rig on the scene’s director, resolving each target by SceneNode id; mid-run it blends to the rig looked up by its TOML name via Cam.rig(scene, "overview"). The data-authored twin of camera-showcase — the loader drives the same presets + field mapping as the fluent setters, so the TOML follow rig is identical to its coded equivalent. Render-verified.
/examples/blend-camera-demoapps/blend_camera_demoNew 2026-08-09A camera authored in a .blend driving the rig system (engine ADR 0106): loads a .blend carrying a camera + geometry, instantiates both, then Cam.fromNode(scene, cam.node) builds a static director rig pinned to the camera’s authored world pose and adopts its lens (blend focal length → FOV, near, far); scene.draw() renders from exactly the framing the artist set — through the same CameraDirector every code-created rig uses. The type = "static" cameras-TOML value is the data-authored twin. Render-verified (auto-screenshots the model from the authored 3/4 pose).
/examples/camera-colliderapps/camera_colliderNew 2026-07-31Occlusion collider (Camera epic, ADR 0059): a wall between the camera and target — the plain rig’s view is filled by the wall, the .collider(0.3) rig pulls the camera in front of it so the player stays visible. Static physics body + masked raycast. Render-verified.
/examples/split-screenapps/split_screen_demoUpdated 2026-07-31Local co-op split-screen (Camera epic, ADR 0059; animated merge ADR 0060): SplitScreen.new(scene, players) — four players in a 2×2 grid that dynamically merges into one group-framed view when they cluster and splits apart when they spread, now an animated blend (transitionDuration/mergeFraction). The renderer draws the scene once per viewport with dividers. Render-verified (grid / mid-transition / merged).
/examples/angled-splitapps/angled_split_demoNew 2026-07-31Kronnect-style angled 2-player split (ADR 0061): SplitLayout.angled — a diagonal divider perpendicular to the on-screen direction between the players, so it rotates as they move around each other. Each player renders full-screen to an offscreen target; the renderer composites the two complementary half-planes (the RTT composite path). Render-verified (divider rotating; sky/horizon/players undistorted).
/examples/peelapps/peel_demoNew 2026-07-31Animated rect-peel merge (ADR 0061): SplitScreen.peel(true) — the split viewports slide together to fullscreen (z-stacked, over the RTT composite path), the Kronnect “views expand together” look, instead of the default camera-converge blend. Render-verified (2 players; panels slide seamlessly into one).
/examples/pipapps/pip_demoNew 2026-07-31Picture-in-picture with the raw Viewport primitive (CAM-19, ADR 0062): a top-down minimap Viewport composited over a third-person Cam main view (rect/z/opacity/border + own camera). Render-verified (minimap shows the corner landmarks + centred player).
/examples/split-hudapps/split_hud_demoNew 2026-07-31Per-viewport HUD (CAM-18, ADR 0064): Graphics.perViewportOverlays(true) replays a retained-UI HUD into each split-screen cell (scissored), so a per-player HUD stays inside its region. Render-verified (a 4-way grid; the HUD appears in every cell, clipped — no spill across dividers).
/examples/split-text-demoapps/split_text_demoNew 2026-08-09Per-viewport SDF text (ADR 0105): the same Graphics.perViewportOverlays(true) flag that replays the HUD (CAM-18) now also replays SDF text into each split-screen cell — world-space nameplates (“ONE”/“TWO”) re-projected by that cell’s own camera (each player’s name sits above their own cube; the other’s is off-screen), and a screen-space banner scaled into both cells. A 2-player horizontal split; text is submitted once, replayed per cell (model-space glyphs identical across cells, only the baked MVP differs). Without the flag, text draws once full-screen against the main camera (byte-identical legacy path). Render-verified.
/examples/plume-triangleapps/plume_triangleCurrent—
/examples/aspect-ratioapps/aspect_ratio_demoCurrent—
/examples/window-demoapps/window_demoCurrent—
/examples/audio-basicapps/audio_basicCurrent—
/examples/audio-3dapps/audio_3dCurrent—
/examples/audio-playerapps/audio_playerCurrent—
/examples/audio-player-3dapps/audio_player_3dCurrent—
/examples/blend-loadapps/blend_loadCurrent—
/examples/physics-simpleapps/physics_simpleNew 2026-03-25—
/examples/physics-stackapps/physics_stackNew 2026-03-25—
/examples/physics-frictionapps/physics_frictionNew 2026-03-25—
/examples/physics-constraint-hingeapps/physics_constraint_hingeNew 2026-03-25—
/examples/physics-particles-debrisapps/physics_particles_debrisNew 2026-03-25—
/examples/particle-fx-demoapps/particle_fx_demoNew 2026-08-08Non-physics sprite particle render modes (engine ADR 0102): two new ParticleEmitter modes reuse the sprite pipeline with no Vulkan renderer change — SpriteGround (mode 1) lays a soft ring flat on the XZ plane (burst one per beat → concentric, expanding, fading ripple rings, a raindrop hitting water) and SpriteBillboard (mode 2) a camera-facing sparkle; PhysicsMesh (mode 0) stays the default (splash spray). Sprite particles are pinned kinematic decals (no Jolt body) animated only by setScaleCurve + setAlphaCurve (+ setColor / setTexture). Companion to physics-particles-debris (the physics-body path).
/examples/physics-golfapps/physics_golfNew 2026-07-16Restitution side by side: three spheres (0.0/0.5/0.9) dropped from the same height. Correction: the old row called this a “restitution/friction/damping” demo — friction (0.4) and damping (0.05, 0.3) are identical on all three, so restitution is the only variable. Procedural geometry, no .blend, no input; auto-screenshots at t=2.2s then exits, doubling as a headless smoke test (engine PLM-026/055). Despite the name, there is no golf.
/examples/physics-mesh-terrainapps/physics_mesh_terrainNew 2026-07-16Ball rolls into a parabolic trough (y = 0.14x²) built as a 17×9 grid in Wren and used as both the render mesh and the addStaticMesh collider, so visuals and collision agree by construction. Winding verified upward (Jolt mesh shapes are one-sided). Procedural, no .blend, no input; auto-screenshots at 2.4s then exits (engine PLM-056).
/examples/physics-heightfieldapps/physics_heightfieldNew 2026-07-16Ball settles into a bowl from a 16×16 row-major height grid (cellSize 0.8) feeding both addStaticHeightfield and the render mesh. Documents the main gotcha: the grid’s (0,0) sample sits at the node origin and extends +X/+Z, so the node is offset to centre the bowl. Procedural, no .blend, no input; auto-screenshots at 2.8s then exits (engine PLM-057).
/examples/sprite-demoapps/sprite_demoNew 2026-07-16Billboarded sprites (engine PLM-009 / ADR 0008): one Texture.load PNG shared by four tinted Sprites. Depth sorting is engine-side (per-frame view-space z), not draw order. Only exercises billboard = 0 (spherical) — cylindrical/flat ship but are unexercised by any app. Procedural scene, no .blend, no input.
/examples/sdf-text-demoapps/sdf_text_demoNew 2026-07-16SDF text (engine PLM-010 / ADR 0007): world-space draw(model), screen-space drawScreen(x,y), outline, wrap/align and rich-text markup — all confirmed present. Unusual: no camera and no scene; identity view/projection puts the model matrix straight in NDC. size is world units for draw, pixels for drawScreen.
/examples/character-controllerapps/character_controllerNew 2026-03-25—
/examples/character-animapps/character_animNew 2026-07-23The Animation Controller epic end-to-end demo (engine PLM-086, CT-M1): a skinned Character.blend/Fire_Sprite driven by a TOML controller via node.attachController — a blend-node locomotion State, typed speed param from input, root-motion designation, a tagged cancel section driving input-buffering/cancel-windows, and an onEnter hook from the auto-tick. Screenshot-verified (character skins + animates via the controller, no validation errors). Fixture-limited to one action (Idle_ArmSwing) → single-entry blend + no masked-Action clip yet (richer rig: HUMAN-014).
/examples/ui-demoapps/ui_demoNew 2026-07-24Retained-mode UI end-to-end (engine PLM-108, ADR 0019, Block U/U-M1): Ui.load("ui/main.ui") + Ui.render(doc, 0, 0, Window.getWidth(), Window.getHeight()). A .ui document (dark backdrop, centered card, title/subtitle, a Play button whose on_click = "onPlay" dispatches to Game.onPlay()) letterboxed full-window. Panels/button draw as quads, labels via the SDF text pass. Screenshot-verified (ui_demo.png at ~1.5s).
/examples/ui-widgetsapps/ui_widgetsUpdated 2026-07-29 (PLM-141)Retained-UI widget breadth (engine PLM-110/141, Block U/U-M2): a type="toggle" checkbox + a type="slider" (drag to set value) whose state the game polls each frame (element.checked/element.value) to recolour a swatch panel. Widgets own their state; the game reads it (optional on_change handler). 2026-07-29 (PLM-141, ADR 0039): extended with a display-only type="progress" bar and a Low/High type="radio" group (mutually exclusive via a shared group, polled through the same checked accessor). Screenshot-verified.
/examples/ui-canvasapps/ui_canvasNew 2026-07-24Retained-UI Canvas custom-draw (engine PLM-111, Block U/U-M3): a type="canvas" region the game draws into with Ui.canvasRect/canvasLine/canvasBezier/canvasText (canvas-local design px, colours 0..1), composited + letterboxed. Demo draws a node graph (boxes + gold bezier wires + labels) — the node-editor building blocks. Screenshot-verified.
/examples/node-editorapps/node_editorUpdated 2026-07-24 (PLM-123/124)The Block-U capstone (engine PLM-087, U-M4; enhanced by PLM-112/113/114/118/121/122/123/124): a visual node editor for AnimController graphs — a unified state/blend/clip graph + a Mecanim Any State on the retained-UI Canvas, chrome via immediate-mode Gui. Typed any-direction wires; pan/zoom (wheel + middle-drag + Fit); multi-select (box/shift/multi-drag); dropdowns (structured guard editor + to/ref/clip/param pick-lists); and a 3D Preview mode that renders the skinned character playing the edited controller live (WASD/Space/K → params; orbit+zoom+track camera; floor/gravity toggles + ground-check; a collection dropdown that auto-discovers the model’s collections and remembers your pick, no config file). Plus a Layers panel (edit a pose-overlay layer’s name/blend/mask, see the Actions bound to it — renaming re-points them) and live per-layer weight sliders in the preview. Loads/edits/saves a controller .toml (PLM-077 schema + [any_state] + transition event), round-tripping it losslessly — including [[layers]]/[transitions]/[actions]/[contexts] (preserved verbatim instead of dropped), with blend1d/blend2d each edited and saved in its own schema (a 2D node used to be written back malformed and then rejected by the engine) and clip sections preserved. --tool-capable. Screenshot-verified.
/examples/ui-editorapps/ui_editorNew 2026-07-24WYSIWYG .ui editor (engine PLM-109, ADR 0019/0020, Block U — closes U-M1): a Nuklear-chrome editor (left: add/delete/save + element tree; right: inspector) over a live canvas rendered by Ui.renderEdit (the identical runtime ⇒ WYSIWYG), click-to-select via Ui.hitTest, a selection gizmo via transparent Gui.beginOverlay. Built on the editor-authoring tier (Ui.newDocument/createElement/deleteElement/reparent/save, UiElement rect/color/fontSize/…). Runs standalone or plume3d --tool apps/ui_editor <target> (the data/script-root split, ADR 0020). Screenshot-verified.
/examples/gui-overviewapps/gui_overviewCurrent—
/examples/gui-inputapps/gui_inputCurrent—
/examples/gui-calculatorapps/gui_calculatorCurrent—
/examples/gui-configuratorapps/gui_configuratorCurrent—
/examples/gui-terminalapps/gui_terminalCurrent—
/examples/mesh-linesapps/mesh_linesCurrent—
/examples/mesh-pointsapps/mesh_pointsCurrent—
/examples/mesh-indexedapps/mesh_indexedCurrent—
/examples/mesh-draw-rangeapps/mesh_draw_rangeCurrent—
/examples/mesh-dynamicapps/mesh_dynamicCurrent—
/examples/mesh-attributesapps/mesh_attributesCurrent—
/examples/mesh-transparencyapps/mesh_transparencyNew 2026-07-23Per-mesh render state on procedural meshes (engine PLM-103, follow-up to PLM-025/ADR 0026): an opaque red quad behind two translucent panes set with mesh.blendMode = "alpha", plus doubleSided/cullMode. Engine partitions opaque-then-transparent and blends back-to-front (depth-write off); the shader emits vertex alpha. Procedural, no .blend, no input; auto-screenshots at t=2s. .blend-imported materials set the same state automatically.
/examples/instanced-mesh-gpuapps/instanced_mesh_gpuNew 2026-03-25—
/examples/grass-fieldapps/grass_fieldNew 2026-08-05Enhanced instancing (engine PLM-248, ADR 0081, Block SPL): one InstancedMesh draws 4096 grass blades in a single GPU-instanced draw, each with its own base colour (addInstanceColored) and height + wind-phase params (setInstanceParams, scaleMul/windPhase), swaying against the global frame clock + Graphics.setWind, with per-instance frustum culling (setCullingEnabled(true)) dropping the ~1700/4096 blades outside the view (visibleCount reads back the drawn count). The blade count exceeds the instance SSBO’s initial 1024 so it also exercises the per-frame SSBO grow (1024→4096). A jittered 64×64 grid, a crossed-quad blade prototype, procedural colours, no .blend, no input; screenshots at frame 80 then exits. Screenshot-verified.
/examples/material-texturesapps/material_texturesNew 2026-07-30Script-assigned PBR material textures (engine PLM-021/PLM-177, ADR 0055): a UV cube built with Graphics.newMesh gets its full six-map set (base/metallic/roughness/normal/AO/emissive) assigned from Wren via mesh.set*Texture, drawn with a reference shader that samples set 1 bindings 1..6 with derivative-TBN normal mapping. The reference shader deliberately omits the set-0 shadow array so it compiles on macOS/MoltenVK without shadow mapping (root-caused in PLM-177). Screenshot-verified.
/examples/custom-material-packapps/custom_material_packNew 2026-08-03Extensible custom material at set 5 (engine PLM-234, ADR 0073): a pack shader binds its OWN textures (a gradient ramp + a scrolling flow map) and params (a tint + a scroll speed) via mesh.setCustomTexture/setCustomColor/setCustomParamFloat, read through #include "plume3d.slang" — no engine change to the descriptor ABI. Screenshot-verified (a neutral grayscale ramp renders warm-tinted, proving both custom textures AND params reach the shader).
/examples/wind-swayapps/wind_sway_demoNew 2026-08-03Global frame-uniform block (engine PLM-236, ADR 0072): Graphics.setWind plus the host frame clock at set 0 binding 4 drive a shader that sways + colour-pulses with plume3d_time(). Two screenshots at different frames differ, proving the frame clock reaches the GPU — the prerequisite for animated shaders (wind/water/clouds).
/examples/post-depthapps/post_depth_demoNew 2026-08-03Full-screen post-process + scene depth (engine PLM-240/237, ADR 0074): quads drawn at different depths are read back as distinct grays by a post effect that samples plume3d_sceneDepth (via #include "plume3d_post.slang"), added each frame with Graphics.addPostEffect. Proves the scene renders offscreen with a sampleable depth target and the post pass runs into the swapchain. Screenshot-verified (near dark → far light, background white).
/examples/post-stack-demoapps/post_stack_demoNew 2026-08-08Stacked N-effect post chain (engine ADR 0101): four full-screen post effects added with Graphics.addPostEffect — a warm tint, a vignette, a desaturate, then CRT scanlines — run as a chain (each effect’s output feeds the next; the last writes the swapchain), so the screenshot shows all three composed. Before ADR 0101 only the FIRST effect ran and the rest were silently dropped — the dark vignette corners + muting + scanlines prove stages 2-4 executed (4 effects also exercise the ping-pong scratch reuse / write-after-read path). Intermediate stages ping-pong through two scratch colour targets (each with an explicit PLM-224 barrier); N==1 is byte-identical to the old single-effect path. A new engine capability (the addPostEffect semantics change 1→N, no Wren signature change) — ADR 0101, no .blend/parser surface (no /security-review). Screenshot-verified (MoltenVK).
/examples/refractionapps/refraction_demoNew 2026-08-03Opaque colour capture for transparent refraction (engine PLM-238, ADR 0075, Block RFX A#3): Graphics.opaqueCaptureEnabled(true) splits the offscreen render into opaque → copy → transparent, and a transparent quad samples the captured opaque scene via plume3d_opaqueColor (mesh ABI, set 0 binding 6, #include "plume3d.slang") at a wobbled screen UV. Four opaque colour bands behind one transparent quad — band boundaries straight outside the quad, wavy inside it. Needs a post effect (a passthrough) for the offscreen path. Screenshot-verified.
/examples/sky-demoapps/sky_demoNew 2026-08-05Atmosphere (engine PLM-241, ADR 0076, Block RFX A#6): one scene proving all four knobs — a procedural gradient sky then an environment-cubemap sky (Graphics.setSky mode 0/1 + Texture.loadCubemap([6 faces +X,-X,+Y,-Y,+Z,-Z])/setEnvironmentMap), a raw-linear ambient fill (setAmbient) and atmospheric distance fog (setFog, linear) that dissolves a diagonal row of receding lit cubes into the sky colour. Two screenshots (gradient sky at frame 60, cubemap sky at frame 150). Screenshot-verified. Retuned 2026-08-05 (ADR 0079, A#6 follow-up): now also calls Graphics.setFogHeight(0.5, 0.18) so the ground hazes seamlessly into the horizon (no hard floor↔sky seam), and its environment cubemap is a real sky-like skybox (zenith→horizon→ground gradient + sun) instead of six flat test colours; the A#6 ambient/fog delivery fix means its lighting now actually applies.
/examples/rtt-monitorapps/rtt_monitor_demoNew 2026-08-05Script render-to-texture capture (engine PLM-242, ADR 0077, Block RFX A#7): a top-down ring of coloured cubes with a picture-in-picture monitor panel showing the same ring from a second low-front camera. Graphics.newCaptureTarget(400,400) allocates a persistent capture texture once; each frame Graphics.submitCapture(target, view, proj) (in draw()) renders the scene from the second camera into it; the capture — an ordinary Texture — is sampled on the panel via mesh.setCustomTexture(0, capture) (set-5 path, no new binding). One screenshot at frame 90. Screenshot-verified.
/examples/splineapps/spline_demoNew 2026-08-05The Spline curve primitive (engine PLM-243, ADR 0080, Block SPL keystone): a 5-point Catmull-Rom spline drawn as a line strip sampled at constant arc-length spacing (positionAtDistance, not position(t) — t isn’t constant-speed), plus 16 rotation-minimizing orientation frames from sampleFrames(16) drawn as gizmos (tangent red / normal green / binormal blue) — the 13-number [px,py,pz, tx,ty,tz, nx,ny,nz, bx,by,bz, width] frames a mesh extruder consumes. One screenshot at frame 90.
/examples/mesh-primitivesapps/mesh_primitivesNew 2026-08-05Procedural mesh library (engine PLM-244, ADR 0082, Block SPL): one of each MeshGen factory — box / cylinder / cone / plane / grid — laid out in a row, plus a sine-wave heightmap terrain (MeshGen.heightmap over a flat 32×32 row-major sample list), each built by the device-free modules/geometry core, tinted with setColor, and drawn immediate-mode with a lit N·L shader so the generated normals read correctly (box hard-edged / per-face; cylinder/cone/heightmap smooth). Centered at origin, positioned purely by the model matrix. Procedural, no .blend, no input; one screenshot at frame 60 then exits at 80. Screenshot-verified.
/examples/spline-mesh-demoapps/spline_mesh_demoNew 2026-08-05Spline mesh extrusion (engine PLM-245, ADR 0084, Block SPL): a river ribbon swept along a Catmull-Rom Spline and terrain-conformed onto a heightfield — MeshGen.ribbon(path, 2.4, 64, _scene) snaps each station onto the ground (via PLM-247 rayDown) so the water hugs the hills — plus an arching tube (MeshGen.tube) over the valley. The terrain is one MeshGen.heightmap visual + a matching Jolt static heightfield from the same heights (so the collider aligns with the drawn surface). Passing null instead of the scene leaves the ribbon flat. Procedural, no .blend, no input; one screenshot at frame 60 then exits at 80.
/examples/scatter-demoapps/scatter_demoNew 2026-08-05Scatter / placement (engine PLM-246, ADR 0085, Block SPL — last of the trio): ~10,600 foliage instances placed two ways into one InstancedMesh (a single GPU-instanced draw) — Scatter.onSurface projects 8,000 blades onto a heightfield terrain (via PLM-247 rayDown), and Scatter.alongSpline lines 2,600 more along a path whose control points are snapped to the terrain with Physics.groundPoint. GPU instancing + per-instance frustum culling (visibleCount) + wind sway (Graphics.setWind); the blade prototype’s own vertex colour shows (scatter leaves baseColor default). Procedural, no .blend, no input; one screenshot at frame 60 then exits at 80. Screenshot-verified.
/examples/blend-scatter-demoapps/blend_scatter_demoNew 2026-08-06Blender foliage import (engine PLM-252, Block BLD): GrassField.blend bakes a hidden grass-blade prototype + a Plume_Scatter Empty carrying 196 blade transforms + colours (ADR 0087 encoding), and BlendResult.instantiateScatter turns each baked prototype into one InstancedMesh — a single GPU-instanced draw per prototype with per-instance colour (from the baked table), per-instance frustum culling and wind sway, coordinate-converted (Blender Z-up → Y-up) onto the ground. The clean visual proof of the baked geometry-nodes / particles scatter pipeline through the loader (vs grass_field hand-placed / scatter_demo procedural). Grass shader loaded first (active shader for the instanced draw) + a lit ground plane; one screenshot at frame 60 then exits at 80. Screenshot-verified.
/examples/foliage-terrain-demoapps/foliage_terrain_demoUpdated 2026-08-06Block BLD Blender import end-to-end (engine PLM-249/251/252/253/254): FoliageTerrain.blend carries a sculpted terrain tagged plume3d_terrain + a hidden grass-blade prototype + a Plume_Scatter Empty baking 256 blades on its surface + two static rock props. The whole level comes in with one call — scene.loadBlendScene(path) — which composes instantiateTerrain (renderable ground node AND an automatic static concave triangle-mesh Jolt collider from the same geometry, creating the scene’s physics world if absent) + instantiateScatter (baked grass as one GPU-instanced draw) + instantiateUnits (the two rock props — the instantiateUnits witness), returning the BlendResult. The terrain surface, grass, rocks, and probe cubes all render (PLM-259: the renderer resolves the terrain’s plume3d_shader_name by basename). Three physics probe spheres dropped from above settle on the hills at varying rest heights (printed) — proving the collider — and the grass follows the terrain contour. Screenshot at frame 180 then exits at 200. (Was a two-call instantiateTerrain+instantiateScatter demo with a terrain-surface-render WIP caveat; PLM-253 gave it loadBlendScene + the rock props and PLM-259 fixed the surface render, so the caveat is removed.) 2026-08-06 (PLM-267): the two rock props now carry a plume3d_collider tag (rock_a = sphere, rock_b = box) and the demo drops a green probe onto each that rests on the rock — the instantiateUnits-built-collider witness.
/examples/toon-pack-demoapps/toon_pack_demoNew 2026-08-06The Toon / Stylized-Shading content pack (engine PLM-261): a complete stylized look — a cel/toon surface shader (shaders/toon.slang: N·L quantized into hard bands, darkest band tinted toward a cool shadow colour, plus a view-space Fresnel rim hardened into a toon rim light; #include "plume3d.slang", tuned via set-5 custom params) plus a depth-edge ink outline post effect (shaders/outline.slang: a 4-tap plume3d_sceneDepth cross → central-difference gradient → dark ink over plume3d_sceneColor on silhouettes + depth steps; #include "plume3d_post.slang", added each frame with Graphics.addPostEffect("shaders/outline", [inkR,inkG,inkB, thicknessPx, depthThreshold, edgeGain])). Built with ZERO engine change — the reference proof of the engine-vs-pack boundary: the engine ships neutral capabilities, the pack ships the look. The outline needs no new engine pass because RFX A#5 (the post stack) + A#2 (sampleable scene depth) already expose everything a depth-edge outline reads. Three primitives (box / cylinder / cone) rotate under the toon surface while the outline inks their silhouettes; screenshot at frame 90 then exits at 110. v1 limit (documented): the depth-based outline catches silhouettes + depth steps but not interior creases on a continuous surface (those need a scene normal buffer — a future engine enhancement, not a pack change); the toon surface uses a single fixed key-light direction (wiring a scene light in is left to the consuming game). Screenshot-verified.
/examples/sky-weather-demoapps/sky_weather_demoNew 2026-08-06The Sky / Weather content pack (engine PLM-262) — the policy half of the engine-vs-pack boundary (the Toon pack proved the shader half). A pure-Wren time-of-day driver (Scripts/TimeOfDay.wren — TimeOfDay.new(scene, "shaders/sky"), then tod.apply(t) each frame, t in [0,1): 0.00 midnight, 0.28 sunrise, 0.50 noon, 0.72 sunset) paints a full day/night cycle by driving the engine’s neutral Block RFX A#6 atmosphere knobs with ZERO engine change: Graphics.setSky (gradient sky, params[0..2] horizon / params[4..6] zenith), setAmbient, setFog (matched to the horizon), and a directional sun Light (type = 1) it rotates and colours across a dawn→noon→dusk arc. apply(t) linear-interpolates keyframe tables for horizon/zenith/sun/ambient colour + sun energy; re-theme the whole sky by editing those tables, still no engine change. A small scene (ground + a receding row of cubes) lit by the changing sky; steps sunrise → noon → sunset → night and screenshots each. Extension (documented): a weather state machine (overcast/rain/storm) layers on top the same way — more Wren policy over the same knobs. v1 note: reuses the stock A#6 sky/scene shaders verbatim (one pre-existing non-fatal MoltenVK pipeline-variant warning, same as sky_demo); renders correctly.
/examples/weather-demoapps/weather_demoNew 2026-08-06The Weather content pack (engine PLM-265) — the third content pack, combining the Toon pack’s shader half with the Sky pack’s policy half: rain, snow, thunder, wind, dynamic clouds, height fog and rain-splash ripples, driven by a pure-Wren Weather state machine (clear/rain/snow/storm) over the engine’s neutral knobs, with ZERO engine change. Four content shaders: precip.slang (rain and snow in ONE instanced shader — per-instance mode params.z 0 rain / 1 snow, GPU-animated fall from the frame time + per-instance seed, so ~2,200 rain + 2,200 snow particles cost one instanced draw each; hand-declares its minimal descriptor set — no #include "plume3d.slang"), clouds.slang (a dynamic cloud sky on Graphics.setSky — a horizon→zenith gradient + an animated fbm cloud layer, coverage a param, the driver advancing a scroll each frame; #include "plume3d_sky.slang"), wet_ground.slang (the lit ground + expanding rain-splash ripples, swapped in while raining), and the reused scene.slang lit ground. Weather.wren drives the cloud sky + coverage, setAmbient, distance + height fog (setFog/setFogHeight), global setWind, and toggles the two precip InstancedMeshes; storm auto-fires a decaying lightning ambient-flash. Cycles clear → rain → snow → storm and screenshots each. Engine notes baked in: an instanced shader must hand-declare the minimal set-0 + set-2 SSBO (the full mesh ABI corrupts the set-2 binding under MoltenVK), and rain + snow share ONE active precip shader because a per-mesh newMesh(.., shader) on an instanced prototype breaks the instance SSBO (engine PLM-264; the per-instance mode param is the workaround). Deferred: rain bloops on water ride with the future Water pack (the wet_ground ripple is the reusable pattern); thunder audio is a documented Source.queueSamples extension (v1 ships the visual flash). Render-verified.
/examples/foliage-pack-demoapps/foliage_pack_demoNew 2026-08-06The Grass / Foliage / Trees content pack (engine PLM-266) — content pack #4 of the Environment & Stylization program. Stylized, instanced grass, foliage and trees with wind sway + back-light translucency (leaves glow when the sun is behind them), built with ZERO engine change: the whole look is one instanced shader shaders/src/foliage.slang (wind sway from the A#1 frame time + global wind + a per-instance phase, a base→tip height gradient, two-sided leaf lighting, and back-light translucency) riding enhanced instancing + the A#6 atmosphere knobs. It hand-declares its minimal descriptor set (set-0 view/proj + frame, set-2 instance SSBO) — no #include "plume3d.slang" — and uses the active-shader path (a per-mesh shader on an instanced prototype breaks the instance SSBO, engine PLM-264). The demo is the shader/engineering proof — a procedural grass field + a few procedural trees (a lit trunk + a canopy of instanced leaf cards), all foliage in one InstancedMesh filled via addInstanceColored/setInstanceParams; screenshot at frame 70 then exits at 90. The production path is Blender-authored (owner emphasis — grass/foliage/trees are primarily authored in Blender): author in Blender → Bake Scatter (the plume3d_wren add-on, PLM-250) → import with Scene.loadBlendScene / BlendResult.instantiateScatter (Block BLD) → render with foliage.slang active. Roadmap, not shipped: multi-part authored-tree prototypes + LOD/billboards (PLM-266 follow-ups, needs PLM-024), spawn masking (PLM-268), audio (PLM-272), real art (HUMAN-024). 2026-08-06 (PLM-267): authored-object colliders now ship — a tree/prop tagged plume3d_collider blocks movement via instantiateUnits; only per-scattered-instance colliders remain a follow-up (the page’s roadmap table + notes were corrected accordingly). 2026-08-06 (PLM-268): spawn masking now ships — the page’s four “spawn masking = roadmap” references (workflow step, tree-authoring-needs table, program-ties list, Notes roadmap) corrected to shipped and cross-linked to the new Bake Scatter › Scatter mask add-on section (a geometry-nodes density mask captured by the bake, or the add-on’s top-down grayscale mask image). Render-verified.
/examples/scene-depth-demoapps/scene_depth_demoNew 2026-08-06Scene depth in the mesh ABI (engine PLM-263, ADR 0089, Block RFX — the Water-pack enabler): a mesh (water) shader reads the opaque scene depth behind/under it at set 0 binding 5, the primitive for shoreline blend / intersection foam / depth-tinted colour. #include "plume3d.slang" → plume3d_sceneDepthLoad(int2(SV_Position.xy)) (raw device depth [0,1]; plume3d_linearizeDepth(raw, nearZ, farZ) for eye-space distance, the caller passing its own camera planes). Gated by Graphics.opaqueCaptureEnabled(true) — the same gate as opaque colour (A#3): after the opaque phase the engine copies sceneDepth_ → a separate opaqueDepth_ image and a transparent (blendMode = "alpha") draw samples the copy, so a water surface reads the scene behind it without touching the live depth attachment it is being tested against (feedback). Binding 5 is a Texture2D<float> read via .Load() (a texel fetch — a D32 sampled through a SamplerState makes an undeclared split sampler in MSL on MoltenVK), always declared (PLM-178 superset idiom, inert for shaders that ignore it); the 1×1 default depth when capture is off (no cost, no crash). Mirrors refraction (opaque colour, b6/A#3) and planar reflection (b7/A#4) — one more global mesh-ABI feed, carrying depth. The post ABI has had sampleable scene depth since A#2; PLM-263 brings the read to mesh shaders. The demo: four opaque bands at four different depths + one transparent quad reading binding 5 → a shoreline (foam-white where a band is close behind, blue where deep) that tracks the bands’ depth — the varying gradient proves the per-pixel read (a constant/default read would be uniform). Screenshot scene_depth.png shortly after startup, then exits. No new Wren surface (the gate opaqueCaptureEnabled already existed, no engine_api.json change); no api/* page — a render-foundation mesh-ABI capability, not a scripting change. Screenshot-verified.
/examples/water-mask-demoapps/water_mask_demoUpdated 2026-08-13Water masking on a BUOYANT boat (engine PLM-273) — the owner ask “keep water from rendering inside boats and things”, now on a floating hull: one Jolt body + Physics.applyBuoyancy (four hull-corner probes → it rocks), the mask footprint moved to a free set-5 slot p6 so it coexists with the Toon colour params, and rewritten from the boat body’s world XZ each frame so the discard tracks the floating hull. The shader discards fragments inside that footprint, so the open-hull boat sits DRY (deck + crate visible) instead of flooded. The footprint is game-driven, not hardcoded — passed via Block MAT set-5 custom params (customParam(0).xy centre + .zw half-extent, customParam(1).x rotation), and the shader (#include "plume3d.slang") discards where abs(local) < halfExt in the rotated footprint space. Arbitrary exclusion shapes (many boats, docks, terrain cut-outs) drive the same discard from a sampled mask texture (a top-down footprint rendered via an A#7 capture target bound at a set-5 slot) — identical mechanism, only the mask source changes. The water also paints a depth-based shoreline foam around the rocks/hull via the PLM-263 scene depth (set 0 binding 5): it linearizes both the sampled scene depth and the water’s own SV_Position.z with the same camera near/far (carried in customParam(1).yz) and foams where the eye-space gap → 0. Gated by Graphics.opaqueCaptureEnabled(true) + a passthrough post effect (offscreen path); the water is blendMode = "alpha" → transparent phase after the opaque depth copy. Uses NO new engine API (MeshGen, Graphics.newMesh/drawMesh/setViewMatrix/setProjectionMatrix/setViewProjectionEnabled/opaqueCaptureEnabled, Mesh.setCustomColor/blendMode, Scene.addCamera) → no new Wren class, no api/* page, no landing card — masking is composed from the already-documented set-5 custom material + PLM-263 depth. Two authoring gotchas the demo pins down: an immediate-mode demo drives view/proj explicitly each frame (setViewMatrix/setProjectionMatrix) — Cam.orbit is the rig system needing Scene.draw(); and setCustomColor’s first arg is a BYTE OFFSET (0 → customParam(0), 16 → customParam(1)), not a slot index. Screenshots water_mask.png at frame 60, exits at 80. Render-verified (blue depth-graded water outside the footprint, dry seabed inside). Cleared the “no doc page yet” debt row carried since the PLM-263 sync.
/examples/water-demoapps/water_demoNew 2026-08-06The stylized Water pack (engine PLM-273) — the surface half of Plume3D’s water content, content pack #5 of the Environment & Stylization program. Stylized water in two variants side by side sharing ONE core — PBR (realistic) on the left, Toon (anime / Inasa-Fujio) on the right, over the same waves — built with ZERO engine change (no engine source, descriptor layout, or frozen contract), the same engine-vs-pack posture as the Toon and Foliage packs. The shared core water_common.slangh (mesh ABI, #include "plume3d.slang") is the whole surface: 3 Gerstner waves (hand-unrolled, MoltenVK-safe) steered by Graphics.setWind with an analytic normal + fine procedural scrolling ripple normals (no texture); depth colour + Beer–Lambert underwater absorption via PLM-263 scene depth (set 0 binding 5, 1 − exp2(−K·depthBelow)); A#3 refraction of the opaque scene behind (binding 6) at a normal-distorted screen UV, foreground halo-guarded; A#4 planar reflection (binding 7) biased to the sky/horizon colour (the reflection pass renders no sky) + Schlick fresnel; and an infini-water horizon fade. A world-space plane can’t derive NDC from SV_Position, so the core carries a clip-space ScreenPos varying → a resolution-free screen-UV grab-pass. The two variants are thin and differ only in the style remap: water_pbr.slang = continuous depth colour + soft Blinn spec + soft (smoothstep) foam; water_toon.slang = floor()-quantized colour bands + a hard stepped foam line (tiny-AA) + a stepped specular glint + sparkle + a bright contact rim. Tuned entirely through set-5 custom params (Mesh.setCustomColor at byte offsets — 0→customParam(0), 16→customParam(1)… — not slot indices): p0–p5 shared geometry/depth/refraction/foam, p6/p7 the Toon-only knobs, p8 camera planes, p9 cam pos, p10 horizon + fade. Gated by Graphics.opaqueCaptureEnabled(true) (the one gate feeding both the opaque colour and depth copies) + a passthrough post effect; the water planes are subdivided MeshGen.plane (verts for the displacement), blendMode = "alpha" → transparent phase; a beach-sloped seabed gives the shallow→deep gradient and rocks poke through as foam/refraction witnesses. Both halves share waves and align at the x=0 seam; screenshot at frame 70 then exits at 90. A shared include must be .slangh (not .slang) so the apps/*/shaders/src/*.slang build glob doesn’t compile it standalone. 2026-08-06 update (engine 10ba108): the demo now also dips the camera below the surface (frame 90) and applies an underwater post effect (shaders/underwater — tint→deep + fog + wobble + caustic + vignette, a Graphics.addPostEffect swap gated on the camera’s world Y), screenshotting water_demo_underwater.png at frame 150, exits at 170 — added a Below the surface section + updated the follow-ups. 2026-08-06 water visual parity (engine PLM-273, commit a7c54d8): the procedural foam is now faithful to the tutorials (gameidea.org stylized water + roystan.net toon water) — Roystan depth-scaled shoreline foam (a foamDepth01 = depthBelow/foamMax scales the noise cutoff → a crisp foam band hugging shorelines/objects, clean in deep water) + his _SurfaceDistortion done procedurally (a second, slower noise field warps the foam-noise UV so foam edges churn like whitewater). Added a Procedural foam, caustics & rain section + a p11 param row (rain intensity / caustic strength). Rain bloops + procedural caustics documented as SHIPPED (both live in the shared core — the earlier row wrongly listed rain bloops as “not shipped”; they landed with the pack — the rain bloops are the water counterpart of the Weather wet-ground ripple). Follow-ups SHIPPED (documented as such): spline rivers (→ /examples/river-demo), the underwater post (above), Jolt buoyancy (→ /examples/buoyancy-demo, the Water class + Physics.applyBuoyancy, PLM-270/271 ADR 0091), and now rain bloops + caustics. Still NOT shipped: a masked buoyant open hull and per-bend river flow (custom water textures partly landed — see the PLM-275 note next). Pure content → no ADR, no /security-review, no CVE trigger, no new Wren class. 2026-08-06 real Perlin foam texture (engine PLM-275, commit 7513e4c): the stylized foam now optionally samples a real, tileable Perlin _SurfaceNoise texture warped by a _SurfaceDistortion texture (Roystan’s actual setup — crisp, detailed cel foam) instead of the procedural value-noise. New set-5 param p11.x = useFoamTex (1 → sample; 0 → procedural fbm fallback); the two maps go in custom-texture slots 0 (water_noise.png) + 1 (water_distortion.png) via Mesh.setCustomTexture, generated by tools/testdata/make_water_foam_textures.py (pure-Python Pillow, seamlessly tileable periodic Perlin). water_demo / river_demo / buoyancy_demo bind them + set p11.x=1; loader-created plume3d_water keeps the procedural fallback. Doc: added a Foam textures subsection + a p11.x param row + the two custom-texture slots, and corrected the foam bullets + the custom-textures follow-up. Pure shader/content — no engine C++ / frozen-contract / .blend-parser change (no ADR / /security-review / CVE). Render-verified (PBR left / Toon right, same waves, aligned at the seam).
/examples/water-playgroundapps/water_playgroundNew 2026-08-07The interactive water capstone (engine Block WTR2 — Water Fidelity, ADR 0094). Free-fly and dive below the surface, toggle the Realistic ↔ Toon water variant and the weather at runtime, and spawn buoyant crates/barrels that bob + rock on the same waves the surface renders. Composes the whole water program: the two surface variants, camera-submersion detection via the new Graphics.setWaterPlane + Graphics.cameraWaterState (returns [submerged, amount, waterLevel, fogR,fogG,fogB, fogDensity]), an underwater post cross-faded on the submersion amount, a camera-facing waterline meniscus quad, and Water + Physics.applyBuoyancy so floaters ride the rendered waves. Runs a scripted TOUR (spawn floaters → both variants → storm → dive → screenshots) so a headless run self-verifies; any control key hands over to the player. New engine surface used = the two additive frozen Graphics submersion methods (ADR 0094); the surface/underwater/meniscus are otherwise pure set-5 content. Bounded water volumes are now drivable too (engine WTR2 #3/#4, ADR 0097/0098) — Graphics.setWaterVolume and the plume3d_water_volume loader register a bounded box; only the underwater look stays a screen-space approximation. Render-verified.
/examples/river-demoapps/river_demoUpdated 2026-08-13A spline water mesh with PER-BEND flow (engine PLM-273, ADR 0113) — the owner ask “spline water meshes for rivers and water flow.” A Catmull-Rom Spline swept into a MeshGen.ribbon(path, w, samples, null, true) with flow=true, so each station’s downstream tangent is baked into the ribbon and the shared Toon water scrolls its whitewater foam along the local bend (not one global wind). Rocks in the channel make foam; the green banks give the PLM-263 depth-based shoreline; gated by opaqueCaptureEnabled + a passthrough post effect, blendMode="alpha". Content on a small additive seam — the one engine addition is the optional ribbon flow param (ADR 0113). Screenshots river_demo.png at frame 70, exits at 90.
/examples/buoyancy-demoapps/buoyancy_demoNew 2026-08-06Jolt buoyancy on the stylized water (engine PLM-270/271, ADR 0091, commit 065735d). One Water drives both the rendered surface (applyToMesh → set-5 p3) and the CPU height query (heightAt), so floats bob on the exact waves rendered. Physics.applyBuoyancy samples multiple probe points per body: two rafts sample their 4 corners → they rock/tilt; three cubes at density 0.35/0.7/1.25 sample a single centre point → they bob at different heights (low floats high + snappy, high sits low + sluggish). Rafts drawn at their full body transform (getWorldMatrix) so the rocking is visible; buoyancy applied every frame; seabed opaque (depth for the shoreline), water transparent last under the opaqueCaptureEnabled gate. Uses the new Water Wren class + Physics.applyBuoyancy method (the engine feature; additive, LowTide/Lexicon-safe). Settles then screenshots buoyancy_demo.png (~frame 220). Rain bloops / masked buoyant hull / custom textures remain follow-ups.
/examples/water-raymarchapps/water_raymarchUpdated 2026-08-08The Seascape interactive water playground — the owner’s gameidea.org ray-marched stylized-water look ported onto a REAL subdivided mesh (water_seascape.slang: Alekseev `
/examples/toon-water-demoapps/toon_water_demoUpdated 2026-08-08The owner’s ray-marched TOON water ported onto a real mesh (water_toon.slang): a banded depth palette, crisp cel foam at shorelines/crests, INK graphic marks + a horizon stroke, banded sun light, and a toonSky rim reflection + sun-gold specular — over the SAME Alekseev octave waves as water-raymarch’s seascape look, so buoyant floaters ride the visible swell (full-orientation buoyancy, ADR 0100). Sees the real scene (seabed + floaters) via the grab-pass + scene depth. Press 1 to compare the realistic Seascape look. A demo of existing capabilities (set-5 custom material + grab-pass + scene depth), not a new engine feature. The toon palette (INK/FOAM/SUN_GOLD) + thresholds are tunable knobs at the top of the shader. 2026-08-08 (task 71): splashes/ripples/precip now run through the engine particle + instancing systems via Scene.draw() — same as its Seascape sibling: SpriteGround foam-ring ripples + a SpriteBillboard splash flash (ADR 0102), GPU-instanced InstancedMesh rain/snow (retiring the screen-space overlay) + a cloud sky.
/examples/blend-water-demoapps/blend_water_demoUpdated 2026-08-07The Blender water tag (engine PLM-274 / PLM-275 / WTR2 #3, ADR 0092 + ADR 0093 + ADR 0097, commits 8fe8c19 / f781551 / 3bb0846) — the artist-authoring on-ramp for water, parallel to plume3d_terrain (PLM-254), now driven through to underwater. A mesh (or box) tagged plume3d_water (a string naming the water shader — toon→water_toon, realistic/pbr→water_realistic [WTR2 #3 fixed this — was wrongly water_pbr], else the value verbatim as a shader basename; the shader stays pack content) is auto-drawn as a stylized water surface by Scene.loadBlendScene — no Wren wiring. The box/VOLUME tag (PLM-275): a box tagged plume3d_water + plume3d_water_volume makes the engine GENERATE a level surface at the box’s world top face (optional plume3d_water_subdiv int/[subX,subZ] clamped 1..256 + plume3d_water_level absolute waterline) — no water mesh authored; folded into the same instantiateWater, no new Wren signature. WTR2 #3 (ADR 0097) adds three loader behaviours (all internal — no public C++/Wren signature change): (1) the Realistic variant is reachable via the tag; (2) a variant-selected set-5 default block + new Realistic-only clamped props plume3d_water_pbr (p4) / _refract (p5) / _caustic (p6) / _crest (p7) / _caustic_strength (p11.y) / _fog (underwater fog) — each finite-guarded AND range-clamped; and (3) auto-registration — instantiateWater registers one primary WaterSurfaceState (a plume3d_water_volume box as a bounded volume via setWaterVolume, else the largest flat plane via setWaterPlane), so Graphics.cameraWaterState + the underwater post fire on authored water with no manual setWaterPlane (buoyancy stays an app-side Water.setWaves path — WTR2 #4 follow-up). This demo now loads Models/WaterRealistic.blend (a plume3d_water="realistic" box volume) and dives the camera below the surface to fire the underwater post via cameraWaterState; the box is never drawn as a solid prop (units skips plume3d_water nodes). The one-call load composes instantiateTerrain (the sloped seabed + collider) + instantiateWater (the tagged surface + registered volume) + instantiateUnits (two rock props — which skips the plume3d_water mesh, so no double-draw). The surface draws with the shared water_common.slangh core (multi-octave Gerstner + PBR + world-anchored caustics + crest foam for Realistic), the engine filling the camera-position param into set-5 p9 (byte 144) each frame while the artist’s float-array props set the look. Fixtures: tools/testdata/make_water_authored_fixture.py → Models/WaterRealistic.blend (the Realistic box volume this demo now loads); the Toon make_water_volume_fixture.py → WaterVolume.blend and the mesh-tag make_water_scene_fixture.py → WaterScene.blend are kept for the loader tests. Security: untrusted-.blend reviewed — instantiateWater reads only already-parsed strings + float arrays (no new byte parser), every set-5 write a hard-coded slot, the colour override loop capped at 4 + finite-guarded, meshIndex bounds-checked; the box-volume path finite-guards every box vert / transform / world corner / plume3d_water_level and hard-clamps the subdivision to 1..256; the WTR2 #3 Realistic numeric knobs are finite-guarded and range-clamped in a pure, unit-tested guard header (water_prop_clamp.h, test_water_prop_clamp.cpp) since finite alone is unsafe for a pow() exponent / normalizer / UV multiplier (blocking /security-review run — ADR 0092 + ADR 0093 + ADR 0097 §Security). Render-verified (MoltenVK): the box’s top face draws as Realistic authored water from above, then the underwater post fires when the camera dives into the registered box; log instantiateWater(...): N water surface(s) built (…) [registered VOLUME for underwater]. Screenshots above at frame 30 + underwater at frame 128, exits at 150.
/examples/water-blenderapps/water_blenderNew 2026-08-08Blender-authored water on a HAND-authored .blend — the surface companion to blend-water-demo (which authors a box VOLUME). A scene modelled in Blender (Island + Seabed + a Water plane) renders its water purely from tags on the mesh: the Water plane tagged plume3d_water="realistic" (+ a _shallow/_deep/_foam/_waves/_horizon/_caustic/_crest param block) is drawn by Scene.loadBlendScene → instantiateWater as engine water around the island (waves, depth colour, a shoreline foam ring, the seabed showing through via scene depth + refraction); the Seabed tagged plume3d_terrain (+auto collider); the Island an ordinary lit mesh — no water wiring in the Wren script. Authoring gotcha the demo pins down: a mesh tagged plume3d_water must be subdivided for the realistic shader’s vertex wave displacement (this Water is a ~140×140 grid; a single quad renders flat) — a plume3d_water_volume box instead auto-generates a subdivided surface. A demo of the existing authored-water loader (PLM-274 / ADR 0092), not a new engine feature (no ADR / new Wren class / /security-review). Render-verified (MoltenVK): log instantiateWater(...): 1 water surface(s) built … registered PLANE for underwater; the camera orbits the island above the surface and screenshots three angles.
/examples/scatter-multiapps/scatter_multiUpdated 2026-08-13Multi-InstancedMesh verification of PLM-257 + PLM-264: two GPU-instanced meshes render independently in one frame, each with its own per-mesh instancing shader — an orange field (blade_a) on the left, a cyan field (blade_b) on the right — over a non-instancing plain active shader. PLM-257: the per-draw instance-SSBO sub-allocation lets both meshes render (before it, both wrote the shared buffer at offset 0 so only the last-submitted rendered). PLM-264: the instanced draw now honours each prototype’s per-mesh shader; before the fix it dropped it and fell back to the active (non-instancing) shader → identity transform → every blade collapsed to the origin. Correct render = orange left + cyan right at their transforms; a pile at the origin or one colour across both means a regression. Two crossed-quad prototypes filled with jittered grids via addInstanceColored/setInstanceParams, per-instance culling on. Screenshot at frame 60, exits at 80.
/examples/planar-reflectionapps/planar_reflection_demoNew 2026-08-05Planar reflection (engine PLM-239, ADR 0078, Block RFX A#4): a row of five coloured cubes floats above a transparent water plane at y=0; the cubes appear mirrored in the water. Graphics.setPlanarReflection(true, 0.0) renders the scene once more from the main camera mirrored across the plane into a half-res reflection target (mesh set 0 binding 7); the water shader #include "plume3d.slang" and samples it screen-space via plume3d_reflection(screenUv). The reflector is blendMode = "alpha" so it’s excluded from its own opaque-only reflection. One screenshot at frame 90. Screenshot-verified.
/examples/gui-buttonsapps/gui_buttonsNew 2026-03-25—
/examples/gui-chartapps/gui_chartNew 2026-03-25—
/examples/gui-comboapps/gui_comboNew 2026-03-25—
/examples/gui-drawingapps/gui_drawingNew 2026-03-25—
/examples/gui-labelsapps/gui_labelsNew 2026-03-25—
/examples/gui-layoutapps/gui_layoutNew 2026-03-25—
/examples/gui-menuapps/gui_menuNew 2026-03-25—
/examples/gui-popupapps/gui_popupNew 2026-03-25—
/examples/gui-propertyapps/gui_propertyNew 2026-03-25—
/examples/gui-showcaseapps/gui_showcaseNew 2026-03-25—
/examples/gui-slidersapps/gui_slidersNew 2026-03-25—
/examples/gui-themeapps/gui_themeNew 2026-03-25—
/examples/gui-togglesapps/gui_togglesNew 2026-03-25—
/examples/gui-treeapps/gui_treeNew 2026-03-25—
/examples/gui-windowapps/gui_windowNew 2026-03-25—
/examples/toml-editorapps/toml_editorNew 2026-06-29GUI game.toml editor; documents standalone + --tool usage. Added to examples index under “Tools”.
/examples/save-load-demoapps/save_load_demoNew 2026-07-29Save/Load end-to-end (engine PLM-022, ADR 0036): reads a slot, increments a persisted launch counter, writes it back — re-run to watch it climb. Game-authored SaveState (int/string/bool/number) via Save.write/Save.read; null read = no save yet or a newer-build save. Press S to re-save; a Gui window shows the count. Added to examples index under “Data & persistence”.
/examples/coroutine-demoapps/coroutine_demoNew 2026-07-29Coroutines end-to-end (engine PLM-031, ADR 0037): two Async.run coroutines self-sequence with no per-frame timer code — one cycles a traffic light with Async.wait (GREEN 3s → YELLOW 1s → RED 3s, looping), the other counts frames with Async.nextFrame (one resume/frame). The engine auto-ticks both; a Gui window shows the light, frame count, and Async.pending (2). Added to examples index under “Scripting”.
/examples/ui-input-demoapps/ui_input_demoNew 2026-07-29Retained-UI editable text input end-to-end (engine PLM-142, ADR 0041): two type = "input" fields — a Name field that opens with initial text (Ada Lovelace) and a City field that shows a dimmed placeholder until you type. Click to focus (a caret appears; a press elsewhere blurs); typing edits the buffer (UTF-8-aware insert/backspace/delete + arrow/Home/End). The game reads the value via the existing elem.text accessor and reacts to on_change/on_submit (Enter) — no new Wren method. v1 = single-line (selection/clipboard/multi-line/IME deferred, PLM-145). Auto-screenshots ui_input_demo.png at ~1.5s. Added to examples index under “Mesh & graphics”.
/examples/ui-nineslice-demoapps/ui_nineslice_demoNew 2026-07-29Retained-UI textured 9-slice end-to-end (engine PLM-139/140, ADR 0038): two panels of very different sizes (160×120 and 500×400) share one image = "textures/panel.png" referenced by path, and a textures/panel.png.ui.component sidecar marks it nine_slice with a border = [24,24,24,24] — so both frames keep the same fixed-size corners while their edges/center stretch. No bare texture handle, no per-.ui border (the sidecar supplies it). Auto-screenshots ui_nineslice_demo.png at ~1.5s (host render-verified: corners unstretched). Added to examples index under “Mesh & graphics”.
/examples/audio-streaming-demoapps/audio_streaming_demoNew 2026-07-29Audio float-PCM streaming end-to-end (engine PLM-133, ADR 0042): creates a mono 48 kHz streaming source with Audio.newStreamingSource(48000, 1) and, each frame while queuedBuffers < 4, synthesizes ~1/60 s of a 220 Hz sine and queues it via queueSamples (interleaved floats in [-1,1]); the engine recycles buffers + auto-(re)starts playback so the tone is gapless. A Gui window shows the frequency + live queued-buffer count. Added to examples index under “Audio”.
/examples/audio-ducking-demoapps/audio_ducking_demoNew 2026-07-30Audio ducking / sidechain (engine PLM-148, ADR 0047): a Music drone and a Voice tone each routed to a Mixer.group; music.duckUnder(voice, -14, 0.05, 0.4) ducks Music 14 dB while Voice plays. Music is fed every frame; Voice is fed only while Space is held, so holding Space fires the duck and a Gui.progress bar shows the Music group’s live duckGain. Activity-driven (fires on a playing source in the trigger subtree).
/examples/audio-reverb-demoapps/audio_reverb_demoNew 2026-07-30Per-group reverb via OpenAL EFX (engine PLM-153, ADR 0050): a looping music track (TheFieldOfDreams.mp3) routed to Mixer.group("Music"), with a button cycling the group’s reverb preset (Off/Cave/Hangar/Underwater/Concert Hall) via music.clearEffects() + music.addReverbPreset(name). The engine builds the EFX aux slot behind the mixer. Needs a device with ALC_EXT_EFX; host-run verified.
/examples/audio-effects-demoapps/audio_effects_demoNew 2026-07-30The full OpenAL EFX effect suite (engine PLM-154/157, ADRs 0051/0052): the same looping music on Mixer.group("Music"), with a button per effect (reverb/echo/chorus/flanger/distortion/pitch/ring-mod) that swaps the chain via clearEffects() + addEffect(type) + setParam(...), a button building a serial chain (distortion → echo), and a row of direct-path filter buttons (muffle/thin/clear via setLowPass/setHighPass/clearFilter). Needs a device with ALC_EXT_EFX; host-run verified.
/examples/audio-mixer-studioapps/audio_mixer_studioNew 2026-07-30 (effects added PLM-159)A wired GUI audio-mixer console (engine PLM-150/159): a looping music bed + one-shot SFX (the audio_player music clip + audio_3d SFX) routed to Mixer.group buses, with per-group volume slider + mute/solo channel strips, a Master fader, a ducking toggle, a Music-bus reverb preset cycle + low-pass toggle (the EFX stack), and a Save-mix button (Mixer.saveSettings() → settings.toml). Group defaults from game.toml [Mixer]. The complete Unity-parity console — groups + gain/mute/solo + ducking + effects + filter + config + persistence together. Host-run verified.
/examples/audio-mixer-demoapps/audio_mixer_demoNew 2026-07-30Audio mixer groups (engine PLM-147, ADR 0046): three procedural tones (Music/SFX/Ambience) each an ordinary streaming Source routed to a Mixer.group(...) via addSource; per-group sliderFloat volume + checkbox mute/solo, plus Mixer.master. The engine applies each group’s effective gain to its routed sources every frame, so faders/mute/solo take effect live. Shows regular audio (not just instruments) routing through the mixer.
/examples/midi-soundfont-demoapps/midi_soundfont_demoNew 2026-07-30Play a SoundFont from a USB-MIDI keyboard (engine PLM-136, ADR 0048): loads a .sf2, opens the first MIDI input, and calls Midi.route(inst) so the engine feeds device events straight to the instrument (native low-latency route). A computer-keyboard fallback (A–K) plays it without MIDI hardware; the window shows the routed device + active voices. Bank is a git-ignored loaded resource (see README). Live device play is a manual/HUMAN-017 check.
/examples/midi-input-demoapps/midi_input_demoNew 2026-07-30MIDI monitor (engine PLM-135, ADR 0044): lists MIDI input devices with Midi.inputDevices, opens device 0 with Midi.openInput(0), and each frame drains Midi.poll() into a scrolling Gui log of note/controller/pitch-bend events (main-thread-safe; drained off the device callback thread by the engine). Shows a hint when no device is connected. Desktop only; live note-in needs a USB-MIDI keyboard (HUMAN-017).
/examples/soundfont-demoapps/soundfont_demoNew 2026-07-30SoundFont instrument played from the computer keyboard (engine PLM-134, ADR 0043): loads a .sf2 bank with SoundFontInstrument.new("soundfonts/YDP-GrandPiano.sf2", 48000, 2), edge-detects the A–L / W–O rows into one piano octave (press → noteOn, release → noteOff), maps Space to the sustain pedal via controlChange(64, …), and Z/X shift octave (the engine renders the instrument’s audio automatically). The bank is a git-ignored loaded resource (fetch per apps/soundfont_demo/README.md); the demo shows a hint if it is absent. A Gui window shows the bank name, octave, and live active-voice count.
AppCategoryStatus
apps/skeletal_animationSkeletal animation (demo)Loads a rigged .blend (armature + skinned mesh), attaches the mesh to its armature, and plays a skeletal clip; Scene.draw() GPU-skins any armature-bound node. Ships with the Bramble_Sprite character (Models/Character.blend, 13-bone rig, Idle_ArmSwing). The producer-side path is complete as of 2026-07-22 (engine Block AF — skin-weight parse PLM-070 + armature attach PLM-073, on top of the runtime skinning PLM-071/072/074/076; ADR 0022). No new public Wren/CLI/config surface — the demo drives the existing node.animations[name].play() API. Owner to author the full /examples/skeletal-animation page (and a landing “What’s in place” card if skinning is surfaced as a headline capability) once the character is finalized.
apps/low_tideGame (pre-production)No game.toml or Wren scripts yet — only a .blend file and a PDF design doc. Document when the game is runnable.
apps/net_echo_server + apps/net_echo_clientNetworkingClient/server pair (server via --server, client windowed). Covered inline on /api/net + /dedicated-server.
apps/net_loopback_demoNetworkingSelf-contained headless Net smoke test (connects to itself; run via --server); covered inline on /api/net.
apps/net_replication_demoNetworkingHeadless replication smoke test; covered inline on /api/net.
apps/net_arena_server + apps/net_arena_client + apps/net_arena_probeNetworkingVisual multiplayer demo: clients pick a shape (keys 1-4); the server orbits + replicates one shape per client. Covered inline on /api/net. A standalone /examples/net-arena page (with a screenshot) would be a good addition.
apps/net_conditions_demoNetworkingNew 2026-08-24 — demonstrates the [Network].sim_* simulated-link keys (packet loss / lag / reorder). Server and its own client in one process; reports arrived-vs-broadcast so the adverse-vs-clean contrast is visible. Covered inline on /api/net.

Task-focused how-to pages (a Guides sidebar group, Starlight autogenerate over src/content/docs/guides/), translated from the engine’s docs/runbooks/ runbooks.

PageStatusNotes
/guides/frame-uniform-and-windNew 2026-08-03How-to for the global frame block (engine PLM-236, ADR 0072): #include "plume3d.slang" → read plume3d_time/plume3d_windDir/plume3d_windStrength in a shader → set Graphics.setWind from Wren → build → verify (two screenshots differ). Adapted from the engine runbook docs/runbooks/frame-uniform-and-wind.md; cross-links /api/graphics + /examples/wind-sway.
/guides/custom-material-packsNew 2026-08-03How-to for the set-5 custom material (engine PLM-234, ADR 0073): the set-5 contract table → write a pack .slang (plume3d_customTex/plume3d_customParam) → drive it from Wren (mesh.setCustom*) → ship as a content pack → verify (a grayscale ramp renders warm-tinted). Adapted from the engine runbook docs/runbooks/custom-material-packs.md; cross-links /api/mesh + /examples/custom-material-pack.
/guides/post-effects-and-scene-depthNew 2026-08-03How-to for full-screen post effects + scene depth (engine PLM-240/237, ADR 0074): the post ABI (#include "plume3d_post.slang", plume3d_sceneColor/plume3d_sceneDepth/plume3d_time/plume3d_postParam) → write a post .slang → Graphics.loadShader → add each frame with Graphics.addPostEffect → verify. Adapted from the engine runbook docs/runbooks/post-effects-and-scene-depth.md; cross-links /api/graphics + /examples/post-depth.
/guides/opaque-color-refractionNew 2026-08-03How-to for opaque colour capture + transparent refraction (engine PLM-238, ADR 0075, Block RFX A#3): the mesh-ABI capture (#include "plume3d.slang", plume3d_opaqueColor(screenUv) at set 0 binding 6) → write a transparent .slang sampling a screen UV → enable Graphics.opaqueCaptureEnabled(true) + a passthrough Graphics.addPostEffect + blendMode = "alpha" → verify. Adapted from the engine runbook docs/runbooks/opaque-color-refraction.md; cross-links /api/graphics + /examples/refraction.
/guides/sky-fog-ambientNew 2026-08-05How-to for the atmosphere knobs (engine PLM-241, ADR 0076, Block RFX A#6): ambient fill (Graphics.setAmbient) → atmospheric distance fog (Graphics.setFog, exp2/linear — with the fog-of-war disambiguation) → procedural gradient sky (Graphics.setSky mode 0) → environment-cubemap sky (Texture.loadCubemap + setEnvironmentMap + mode 1) → read ambient/fog in a lit shader (LightingData UBO tail @1040/1056/1072) → write a sky shader (#include "plume3d_sky.slang") → verify. Adapted from the engine runbook docs/runbooks/sky-fog-ambient.md; cross-links /api/graphics + /api/texture + /examples/sky-demo.

PageStatusNotes
/ (landing page)Updated 2026-08-242026-08-24: added a one-line link from the Roadmap intro to the new /where-plume3d-fits maturity ledger. Deliberately not a “What’s in place” card — that grid enumerates engine capabilities, and this page documents no new engine capability; adding a card would misreport the comparison itself as a feature. Recorded here rather than skipped silently, because PWEB-012 exists precisely because three consecutive syncs skipped the landing-page step without saying so. 2026-07-29 (roadmap reconcile): removed roadmap items that already ship (they were listed as pending and present in “What’s in place”): dropped Frustum culling from Short-Term (the Rendering card already lists “per-frame frustum culling”); narrowed Short-Term Rendering robustness to just the Depth pre-pass (back-face culling + alpha/double-sided already ship); narrowed Long-Term Animation+ to IK / retargeting (blend trees ship via the Animation Controller). Save/load and Scripting (coroutines/async) stay in Short-Term — genuinely not yet implemented (engine PLM-022/PLM-031, both backlog). 2026-07-16: added the “What’s in place” cards that four months of engine syncs had silently skipped — Sprites & Textures, SDF Text, Networking, HTTP — plus Engine.entropy() on the Randomness card and crash reporting (macOS/Linux; a no-op on Windows) on Tooling & Testing. Removed the overstated “touch” claim from the Input card (C++-only, no Wren binding). Added the missing classes to the three Wren class enumerations. Prior: 2026-03-25 roadmap restructure; Vaultis added to Tools.
/getting-startedCurrent—
/testingNew 2026-06-29Headless testing guide: plume3d --test, tests/run.wren → Tests.main() convention, Engine.exit, full exit-code table, CI example. Sidebar entry added after Configuration.
/cliNew 2026-08-09CLI reference (engine PLM-284, ADR 0107): running a game, init [<dir>] project scaffold (-i/--interactive, --force), --tools (lists ui_editor/node_editor/bt_editor/toml_editor + the --tool <tool-app> <target-app-dir> launch form), and the headless modes (--test/--server/--check). Cross-linked from /getting-started. Sidebar entry added after Testing (Headless). Not covered by check_docs_coverage.py (apps/Wren-class gate only) — manual sync.
/packagingUpdated 2026-08-10Packaging & distribution guide (engine PLM-286/294, ADR 0110/0111): the .p3d app package (zip of the cleaned app dir + a root package.toml manifest), the plume3d-pack host tool (<appDir> --target macos|windows|linux --runtime <bin> + options), the frozen package.toml schema, the desktop shells (macOS .app, Windows .zip, Linux .tar.gz), and package-integrity signing (--dev-key/--dev-cert → a developer-certificate chain the runtime verifies every launch, refusing a modified payload; the paid licensing tier). Cross-links /cli (build variants), /configuration, /signing. Sidebar entry after Command line. Not covered by check_docs_coverage.py (apps/Wren-class gate only) — manual sync.
/signingNew 2026-08-11Code-sign your game for distribution (engine PLM-286/294, HUMAN-027): the plume3d-pack --sign flags per platform — macOS Developer ID + --notarize --notary-profile (proven end-to-end, spctl = Notarized Developer ID), Windows Authenticode via Azure Artifact Signing (--azure-endpoint/-account/-cert-profile, the cross-platform dotnet sign tool), Linux --gpg-key (SHA256SUMS + .asc) — with the “provenance + tamper-evidence, not encryption” framing (distinct from the .p3d --dev-key integrity). Sidebar entry after Packaging. Not covered by check_docs_coverage.py (apps/Wren-class gate only) — manual sync.
/dedicated-serverUpdated 2026-07-03Dedicated-server run mode (plume3d --server): headless Game.update tick loop, [Network] config, shutdown/exit codes. Networking transport (GNS), replication, and the PlayFlow client are live (links to /api/net, /api/playflow); only a live PlayFlow account is pending (HUMAN-007). Engine runbook: RUNBOOK.md.
/downloadsUpdated 2026-03-25Added Vaultis companion tool section
/blender-addonUpdated 2026-08-06Addon 0.2.0 (PLM-044…054): Validate + Write Config buttons, class picker, multi-select batch layers, preferences fallback layer names, extension manifest (Blender 4.2+), collision default = engine “Default” layer, project-rooted forward-slash paths, no-overwrite file creation. 2026-08-06 (PLM-267, ADR 0090): the Physics collider panel field (a Collider shape enum + Static + Collision Layer, on any mesh object, independent of the Wren-script config) — writes the plume3d_collider / plume3d_collider_static / plume3d_collider_layer custom properties the engine’s instantiateUnits reads. 2026-08-06 (PLM-268): a new Bake Scatter: grass / foliage / trees section — the page’s first coverage of the Bake Scatter panel/operator — with a Scatter mask: where things spawn subsection: a geometry-nodes density mask captured automatically by the bake, or the add-on’s top-down grayscale Scatter mask image (white = spawn / black = clear / gray = sparse, auto-fit to the scatter’s world-XY bounds, seed-deterministic), a bake-time Blender input resolved into the baked instance table the engine never sees. 2026-08-06 (PLM-274, ADR 0092): a Tag-based authoring conventions note under the addon UI — the plume3d_terrain and new plume3d_water custom-property tags that turn a plain mesh into engine behaviour on import (the string names the water shader; plume3d_water_* float-array props tune the look), linking to instantiateWater.
/configurationUpdated 2026-07-16Covers [Game], [Window], [Physics], [Network]. 2026-07-16 — the page was billed as the full game.toml reference while [Window] had no keys at all and collision_matrix was documented backwards: matrix indices are Jolt motion layers (0 = static, 1 = moving), not indices into collision_layers; the page’s own former example [[0,0],[1,1]] disabled static-vs-dynamic contacts and dropped every dynamic body through the floor. Now documented truthfully with a danger callout. Added all ten [Window] keys with real defaults, [Game] (incl. undocumented terminal), the mandatory-[Game] parser rule, PLUME_SERVER_PORT, and fixed [Network].max_clients (a hard cap, not advisory; default 32 applies only when [Network] is present — otherwise 0 = unlimited).
/where-plume3d-fitsNew 2026-08-24Engine maturity ledger — a cited capability matrix comparing Plume3D against Godot 4.7.2, Unity 6.5 and Unreal 5.8 across 57 capabilities in 10 areas, plus a capability-arrival timeline built from the engine’s own ADR dates. GENERATED — never hand-edit the page. Source data is src/data/maturity/capabilities.json (the stable catalogue) + src/data/maturity/rev-YYYY-MM-DD.json (one append-only file per reading); npm run maturity:build regenerates it and npm run maturity:check (wired into CI) goes red if the page drifts from the data. Deliberately carries no numeric score for any engine: a 0–10 composite reads as a measurement and cannot be verified by anyone under EU comparative-advertising rules, whereas a per-cell state with a vendor-doc citation can. Every competitor claim is sourced from public vendor documentation only, never hands-on evaluation (Unity’s ToS restricts competitive analysis by name). Text-only trademarks, no competitor logos. Revision 1 baseline: Plume3D ships 19/57, Godot 42, Unity 49, Unreal 54. Next review due 2027-02-24. Not covered by check_docs_coverage.py (apps/Wren-class gate only) — manual sync.
/licensingCurrent—
/engine-licenseCurrentThird-party table accurate as of 2026-03-25

Engine features not yet reflected anywhere in the docs

Section titled “Engine features not yet reflected anywhere in the docs”
FeatureLocation in engineDoc needed
Wren #!command terminal attributewren_engine_module.cpp (EngineHelper)Covered in GUI Terminal only; could use a dedicated guide
Hot-reloadmodules/hotreload/Mentioned in landing page and getting-started; no dedicated page
PhysicsFS virtual filesystemengine/src/fs_mount.cppNot documented; .p3d archive format not described
Blender 5.0 Kaitai spectools/validate_plume3d_blend_kaitai5/Tools page in engine repo covers this; no website page
CI/CD (Gitea Actions, Docker).gitea/workflows/, docker/Dev-only; no website page needed
Wren syntax highlighting bundletools/Wren.tmbundle/Could note in Getting Started as an optional setup step
PLUME3D_BLEND_DUMP=file.txt env varEngine runtimeMentioned in Blend Load example; could be a debug guide
CMakePresets.json (24 presets)Root CMakePresets.jsonEngine BUILDING.md covers this; no website page needed

Vaultis is now referenced in:

  • / (landing page) → Tools table
  • /downloads → Companion Tools section

Not yet done:

  • No dedicated /vaultis page (may be appropriate as the integration matures)
  • Plume3D-specific EngineType.plume3d export preset does not yet exist in Vaultis source; it uses EngineType.other + custom import root. Update this note when a dedicated preset is added.