Skip to content

Welcome to Plume3D

A code-driven, scriptable 3D game engine that brings the simplicity and immediacy of classic frameworks into a modern workflow — powered by SDL3, Vulkan, Wren, OpenAL, and Jolt

Plume3D is a scriptable 3D game engine for building games and tools with a small, immediate-style API. You write game logic in Wren; the engine handles windowing, rendering, audio, and (optionally) physics via modular integrations. Design resolution and scale modes (stretch, letterbox, integer) give you control over aspect ratio and pixel-perfect scaling.

The engine is built from a small core plus integrations that each handle a major subsystem. Scripts see a unified API; the host wires these together.

IntegrationPurpose
WrenScripting. Runs your game: loads the main script, exposes the engine API as foreign classes (Engine, Logger, Mesh, Input, Window, Graphics, Audio, Scene, Node, Camera, Light, Raycast, Physics, Config, Resource, AnimationState, InstancedMesh, ParticleEmitter, CharacterController, Texture, Sprite, Text, Net, PlayFlow, Http, Random, etc.), and drives the lifecycle — init(), update(dt), draw().
VulkanRendering. Draws meshes and UI: loads SPIR-V shaders, manages vertex/index buffers, PBR materials with shadow mapping, GPU instancing via SSBO, skeletal skinning via bone matrix SSBO, records draw calls from Wren, and provides a Nuklear Vulkan backend so the full GUI is rendered with Vulkan.
OpenALAudio. 3D positional audio: creates sources from mounted files (WAV, MP3, OGG, FLAC), play/stop/pause, per-source volume/pitch/position/cone/attenuation, and global listener position/orientation/Doppler for spatial sound.
JoltPhysics. Full physics integration: rigid bodies (static/kinematic/dynamic) with multiple shapes, constraints (fixed/hinge/slider/cone/6DOF), character controller, contact event callbacks, raycasts, and complete Wren API for forces, impulses, velocities, and simulation control.

Platform and I/O are abstracted: SDL3 is used on desktop for window and input; a console port would swap in another implementation behind the same abstractions.

Current capabilities and planned milestones: what’s in place, what’s still coming, and longer-term direction.

For how these capabilities stack up against Godot, Unity and Unreal — including the places they plainly do not — see Where Plume3D fits, a cited comparison we re-read on a schedule.

  • RenderingVulkan, PBR (Cook–Torrance), full material-map set — base color, metallic, roughness, normal, AO, emissive — assignable from Wren (per-map setters on Mesh), derivative-TBN normal mapping, triplanar fallback, shadow mapping (up to 4 maps, configurable resolution/bias), debug view modes, per-frame frustum culling, per-material alpha blending + back-face culling / double-sided (from .blend materials or set in Wren via mesh.blendMode / cullMode)
  • Extensible materials & animated shadersA content pack’s Slang shader can bind its OWN textures and parameters with no engine change — the descriptor ABI is no longer hardcoded. mesh.setCustomTexture(slot, texture) fills eight custom texture slots and setCustomColor / setCustomParamFloat / setCustomParams write a 256-byte params blob, which the engine binds at set 5 (customTextures[8] + one shared sampler + a CustomParams UBO); the shader reads them via plume3d_customTex / plume3d_customParam, all bounds-checked engine-side. Alongside it, a global frame-uniform block at set 0 binding 4 — { time, delta, frameIndex, wind } — gives every shader a host-owned clock plus an app-set wind (Graphics.setWind), the prerequisite for animated shaders: wind sway, water waves, moving clouds, colour pulses. Both are read through one shared include, #include “plume3d.slang”, the canonical Plume3D shader ABI. See the custom_material_pack and wind_sway_demo examples.
  • Transparent refraction (opaque capture)A transparent surface — water, glass, heat haze — can sample the OPAQUE scene behind it and distort it. Graphics.opaqueCaptureEnabled(true) splits the offscreen scene render into opaque → copy → transparent, so transparent meshes (blendMode = “alpha”) sample the captured opaque colour: a mesh shader reads plume3d_opaqueColor(screenUv) at set 0 binding 6 (the mesh ABI, #include “plume3d.slang”) with a screen-space UV, offsetting it to refract. Requires the offscreen path — pair it with a post effect (a passthrough is fine), single camera; without one the capture reads the 1×1 white default (no refraction). See the refraction_demo example.
  • Sky, fog & ambientNeutral, raw-linear atmosphere knobs — the engine ships the pass, your pack ships the look and the time-of-day / weather curve. Graphics.setSky(shaderName, mode, params) draws a full-screen sky behind the scene at z=far (mode 0 = a procedural horizon→zenith gradient, mode 1 = an environment cubemap loaded with Texture.loadCubemap([6 faces +X,-X,+Y,-Y,+Z,-Z]) + Graphics.setEnvironmentMap); the sky shader uses its own ABI, #include “plume3d_sky.slang”. Graphics.setAmbient(r,g,b) is a raw-linear ambient fill that lifts shadowed faces off black (it replaces the old hardcoded 0.03 constant, and defaults to it, so unset scenes are unchanged). Graphics.setFog(r,g,b,density,start,end) is ATMOSPHERIC distance fog (exponential when density > 0, else linear over [start,end]) applied as a lit-shader term, so it composes with every path — distinct from the gameplay fog-of-war (Graphics.fogSoftness / FogOfWar). Graphics.setFogHeight(baseY, falloff) adds exponential HEIGHT fog (aerial perspective) on top of the distance fog — geometry below world-height baseY hazes into the sky at the horizon, so the ground blends seamlessly into the skybox with no hard floor↔sky seam (set the fog colour = the sky horizon; falloff = 0 turns it off). The cubemap sky renders; sampling the env cube from lit shaders for reflections (IBL) is a follow-up. See the sky_demo example.
  • Render-to-texture captureA Wren script can render the scene from a SECOND camera into a persistent, sampleable texture — in-world monitors, security cameras, mirrors, picture-in-picture, and (later) grass-bending interaction maps and water displacement. Graphics.newCaptureTarget(w, h) allocates a persistent sRGB colour target once (newDataCaptureTarget gives a linear/UNORM one for data maps), and each frame Graphics.submitCapture(target, view, proj) renders the whole scene from a view/projection (e.g. camera.getViewMatrix() / getProjectionMatrix(aspect)) into it. The result IS a Texture, so you sample it exactly like a loaded image — mesh.setCustomTexture(slot, capture) on the existing set-5 custom-material path, no new descriptor binding. Submit in draw() (not update()); v1 is single-camera and captures the whole scene including any screen-space overlay (a drawSubset filter is a follow-up). See the rtt_monitor_demo example.
  • Planar reflectionStylized water and floors reflect the scene above them. Graphics.setPlanarReflection(true, planeY) renders the scene once more from the main camera MIRRORED across the horizontal plane y = planeY into a half-res reflection target, exposed globally at mesh set 0 binding 7 — a water/floor shader (#include “plume3d.slang”) samples it screen-space via plume3d_reflection(screenUv) with no per-draw binding. Sticky state, single-camera; the cost is a second opaque scene draw, and setPlanarReflectionScale(scale) trades sharpness for cost (default 0.5 = half-res). The reflector must be TRANSPARENT (mesh.blendMode = “alpha”) so it’s excluded from its own opaque-only reflection, and the reflected content should be above the plane (oblique below-plane clipping is a follow-up). See the planar_reflection_demo example.
  • Scene depth for mesh shaders (shoreline / foam enabler)A transparent surface — water especially — can read the OPAQUE scene DEPTH behind and under it, the primitive for shoreline blend, intersection foam, and depth-tinted colour. A mesh shader #include “plume3d.slang” (the mesh ABI) and reads plume3d_sceneDepthLoad(int2(SV_Position.xy)) at set 0 binding 5 — the raw device depth [0,1] of the opaque scene at that screen pixel (plume3d_linearizeDepth(raw, nearZ, farZ) converts it to an eye-space distance, the caller passing its own camera planes). It rides the SAME gate as opaque-colour capture: Graphics.opaqueCaptureEnabled(true) copies the opaque-phase depth into a separate image after the opaque phase, so a transparent draw (blendMode = “alpha”) samples the copy and reads the scene behind it without touching the live depth attachment it’s tested against. Binding 5 is a Texture2D read with .Load() (a texel fetch, MoltenVK-safe for a D32), always declared so shaders that ignore it are unaffected, and the 1×1 default depth when capture is off (no cost, no crash). It joins opaque colour (binding 6) and planar reflection (binding 7) as a global mesh-ABI feed — the depth half of the Water pack (waves + shoreline/foam + refraction + reflection). See the scene_depth_demo example.
  • Stylized shading / ToonA complete cel/toon LOOK ships as a CONTENT PACK with no engine change — the reference proof that the engine hands out neutral capabilities while a pack supplies the style. A cel/toon SURFACE shader quantizes N·L into hard light bands, tints the darkest band toward a cool shadow colour, and hardens a view-space Fresnel into a toon rim light — tuned entirely through the set-5 custom params (Mesh.setCustomColor / setCustomParamFloat: band count, shadow tint, rim colour / power / strength), so a game re-skins it with no engine change. A depth-edge ink OUTLINE post effect inks silhouettes and depth steps — added each frame with Graphics.addPostEffect(“shaders/outline”, [inkR, inkG, inkB, thicknessPx, depthThreshold, edgeGain]). Both are built on only the public shader ABIs: the surface on the mesh ABI (#include “plume3d.slang”), the outline on the post ABI (#include “plume3d_post.slang”). The outline needs NO new engine pass — the post stack (Graphics.addPostEffect) plus sampleable scene depth already expose everything a depth-edge outline reads. v1 is depth-based, so it catches silhouettes and depth steps but not interior creases on a continuous surface (those need a scene normal buffer — a future engine enhancement, not a pack change). See the toon_pack_demo example.
  • Sky / Weather (time-of-day driver)A full day/night cycle ships as a CONTENT PACK with no engine change — the reference proof of the POLICY half of the engine-vs-pack boundary (the Toon pack proved the shader half). A pure-Wren TimeOfDay driver paints sky colour, sun direction and colour, ambient fill, and distance fog by driving only the engine’s neutral A#6 atmosphere knobs: 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). apply(t) linear-interpolates keyframe tables for horizon / zenith / sun / ambient colour and sun energy into Graphics.setSky (gradient — params[0..2] horizon, params[4..6] zenith), Graphics.setAmbient and Graphics.setFog (matched to the horizon), and aims a directional sun Light (type = 1) along a dawn→noon→dusk arc. The engine stays policy-free — it exposes the knobs and takes no opinion on time of day; the whole day curve is pack Wren, so you re-theme the sky by editing the keyframe tables with no engine change. A weather state machine (overcast / rain / storm) layers on top the same way — more Wren policy over the same knobs. See the sky_weather_demo example.
  • Weather (rain / snow / thunder / wind + dynamic clouds + fog)A full weather system ships as a CONTENT PACK with no engine change — the THIRD content pack, and the one that combines both halves the earlier packs proved separately: the shader half (Toon) and the policy half (Sky). A pure-Wren Weather state machine (clear / rain / snow / storm) drives only the engine’s neutral knobs: a dynamic CLOUD sky on Graphics.setSky (clouds.slang — a horizon→zenith gradient plus an animated fbm cloud layer, coverage per state, the driver drifting a scroll param each frame; #include “plume3d_sky.slang”), ambient fill (setAmbient), distance AND height fog (setFog / setFogHeight — low mist that thickens into storm murk), and global wind (setWind) that the rain slants with and the snow flutters against. RAIN and SNOW are GPU-instanced particle fields — one precip.slang shader serves both (a per-instance mode param picks 0 = rain streak / 1 = snow flake), the fall computed on the GPU from the frame clock + a per-instance seed, so thousands of particles cost ONE instanced draw each (addInstanceColored / setInstanceParams). While it’s raining the ground swaps to a wet-ground shader with animated rain-splash RIPPLES (the same pattern a water surface wants for rain bloops — that lands with the Water pack). Storm auto-fires a decaying lightning ambient-flash. The engine stays policy-free — wind, time, instancing and the atmosphere knobs are neutral; which state looks like what, when it thunders, how hard it blows, all live in Weather.wren, so you add states (overcast, hail) or re-theme with no engine change. Thunder AUDIO is a documented Source.queueSamples extension (v1 ships the visual flash). See the weather_demo example.
  • Grass / Foliage / Trees (instanced wind + translucency; Blender-authored)Stylized grass, foliage and trees ship as a CONTENT PACK with no engine change — the fourth content pack of the Environment & Stylization program. One instanced shader (foliage.slang) gives a whole field wind SWAY (the A#1 frame clock + global wind + a per-instance phase so blades don’t move in lockstep), a base→tip height gradient, two-sided leaf lighting, and back-light TRANSLUCENCY (thin leaves glow warm-green when the sun is behind them) — riding the enhanced-instancing path (addInstanceColored / setInstanceParams) so a whole grass field, plus every tree’s leaf canopy, is ONE GPU-instanced draw. Grass, foliage and trees are PRIMARILY AUTHORED IN BLENDER: model a blade / leaf / tree, scatter it, bake it with the Bake Scatter add-on, import it with Scene.loadBlendScene / BlendResult.instantiateScatter (Block BLD — one instanced draw per prototype), and render it with foliage.slang as the active shader. The apps/foliage_pack_demo demo (a procedural grass field + procedural trees — a lit trunk with an instanced leaf-card canopy) is the shader/engineering proof; the tree-specific authoring needs — multi-part prototypes and distance LOD / billboard imposters — are on the roadmap (own tickets), not shipped in v1. Spawn MASKING now ships: control WHERE grass / trees spawn from a top-down grayscale Bake-Scatter mask image (white = spawn, black = clear, gray = sparse, auto-fit to the scatter’s world-XY bounds and seed-deterministic so re-bakes are stable), or drive density from a geometry-nodes vertex-group / texture the bake captures automatically — a bake-time Blender authoring input the engine never sees. Authored-object colliders also ship — tag a tree / prop plume3d_collider; only per-scattered-instance colliders stay a follow-up. The instanced shader hand-declares its minimal descriptor set (no plume3d.slang include) and uses the active-shader path, the same discipline the Weather pack’s precipitation uses. See the foliage_pack_demo example (and the blend_scatter_demo / foliage_terrain_demo for the Blender import path).
  • Stylized water (PBR & Toon)Stylized water ships as a CONTENT PACK with no engine change — the fifth content pack of the Environment & Stylization program, and the SURFACE half of Plume3D’s water. It renders TWO variants that share ONE core — realistic PBR and anime/Toon — over the SAME waves, so a game picks realistic or stylized water from the same code. The shared core (water_common.slangh, on the mesh ABI #include “plume3d.slang”) is the whole surface: three Gerstner waves (hand-unrolled, MoltenVK-safe) steered by Graphics.setWind with an analytic normal plus fine procedural scrolling ripple normals (no texture); depth colour and Beer–Lambert UNDERWATER ABSORPTION from the scene-depth feed (set 0 binding 5) so water darkens and saturates with depth; REFRACTION of the opaque scene behind it at a normal-distorted, foreground-halo-guarded screen UV (binding 6); planar REFLECTION biased to the sky colour plus a Schlick FRESNEL mix (binding 7); and an infini-water horizon fade. The two variants are THIN and differ ONLY in the style remap: PBR = continuous depth colour + soft Blinn specular + soft (smoothstep) foam; Toon = floor()-quantized colour bands + a hard stepped foam line + a stepped specular glint / sparkle + a bright contact rim. It is tuned entirely through the set-5 custom params (Mesh.setCustomColor at byte offsets — shallow / deep / foam colour, wave gain / length / steepness / speed, refraction / fresnel / specular, and the Toon band count / rim / sparkle), gated by Graphics.opaqueCaptureEnabled(true) (the single gate feeding both the opaque-colour and scene-depth copies) with the water planes drawn transparent (mesh.blendMode = “alpha”) — so a game re-skins realistic or anime water with no engine change, exactly as the Toon and Foliage packs re-skin their looks. The foam is FAITHFUL to the tutorials it follows and, like everything else, procedural (no textures): Roystan-style DEPTH-SCALED shoreline foam — a foamDepth01 = depthBelow / foamMax scales the foam-noise cutoff, so foam is a crisp band that hugs shorelines and objects and stays clean in deep water — with his surface-distortion warp done procedurally (a second, slower noise field warps the foam-noise UV so the edges churn like whitewater); plus procedural CAUSTICS on the seen-through bottom and rain BLOOPS (expanding impact ring-ripples gated by a rain-intensity param — the water counterpart of the Weather pack’s wet-ground splash). This is the SURFACE half. Its follow-ups have since shipped: spline RIVERS with flow (a Spline swept into a MeshGen.ribbon of the shared water, flowing off the wind vector — the river_demo example), an UNDERWATER camera post (water_demo now dips below the surface — tint, fog, wobble, caustic, vignette), Jolt BUOYANCY (the Water card below), and rain bloops + caustics (above). Still not yet shipped: a masked buoyant open hull, custom water textures (the procedural stand-ins are ready to swap), and per-bend river flow. Built on capabilities the engine already ships — scene depth, opaque capture, planar reflection. See the water_demo and river_demo examples.
  • Water physics — height query & buoyancyGameplay can now ask “how high is the water at (x, z) right now?” and get a value that MATCHES the rendered Gerstner waves, and float rigid bodies on it — the physics half of the Water pack, and unlike the surface pack this IS a small engine feature (a new frozen Wren class + a Physics method, engine PLM-270 / PLM-271, ADR 0091). The new Water class is the CPU MIRROR of the stylized water shader’s waves: Water.new() then setWaves(gain, wavelength, steepness, speed) / setWind(x,z) / setBaseY(y) configure it, applyToMesh(mesh) writes those same waves into the mesh’s set-5 p3 so the RENDER matches the query, and heightAt(x, z, time) returns the surface Y — reproducing the shader’s Gerstner sum bit-for-bit from one shared formula (water_wave.h ↔ water_common.slangh) so a float bobs on the waves you SEE. Physics.applyBuoyancy(node, water, time, offsets, strength, density) then turns that height into forces on a Jolt body each frame: offsets is a flat list of LOCAL [x,y,z] probe points (a raft’s four corners, or a Blender hull’s sample points; empty = a single centre bob), and sampling several points across the footprint makes a body ROCK and TILT, not just bob — force per probe is strength · submersion / density applied at that world point. density is the object’s density vs water (default 1.0): LOW floats high and pops out fast (snappy), HIGH sits low and bobs sluggishly. For water rendered with the Realistic variant, Water.setRealistic(true) switches the height mirror to the 5-octave water_realistic surface (default is the legacy 3-wave) so buoyancy tracks the exact swell you SEE — the water_playground capstone opts in (engine WTR2 #4, ADR 0098). A parallel Water.setSeascape(true) selects the Alekseev OCTAVE (Seascape) mirror instead, for water rendered with the water_seascape surface — the water_raymarch seascape playground opts in so its crates and barrels bob on the same octave waves you see (engine ADR 0099; the octave sum is centred on the plane by a −0.9·gain bias so the query, buoyancy and waterline agree). Additive — no existing signature changed. See the buoyancy_demo example (rafts sampling four corners rock; cubes at density 0.35 / 0.7 / 1.25 float at different heights).
  • Blender water tag (author water in Blender — a mesh or a box volume)Tag a mesh plume3d_water in Blender — OR tag a box plume3d_water + plume3d_water_volume — and Scene.loadBlendScene draws (or GENERATES) a stylized WATER surface — no Wren wiring — the artist-authoring on-ramp for water, parallel to the plume3d_terrain tag for ground (engine PLM-274 / PLM-275, ADR 0092 + ADR 0093). The new BlendResult.instantiateWater(scene) (folded into loadBlendScene, so one call now also builds a scene’s water) turns each plume3d_water-tagged mesh into a transparent, auto-drawn surface running the shared stylized-water core (Gerstner waves + depth colour + refraction + reflection + fresnel + procedural foam). The tag is a STRING that names the water shader — toon → water_toon, realistic (or the pbr alias) → water_realistic, any other value verbatim as a shader basename — so the water look stays PACK CONTENT the engine never hardcodes; optional float-array props (plume3d_water_shallow, plume3d_water_deep, plume3d_water_foam, plume3d_water_waves, plume3d_water_horizon) tune the colours, waves and foam without touching script. NEW box/VOLUME authoring (PLM-275, ADR 0093): a box tagged plume3d_water_volume makes the engine GENERATE a level water plane at the box’s world TOP face — so a level designer drops a box to define a pool or lake with no water mesh to model or place — with optional plume3d_water_subdiv (int or subX/subZ, hard-clamped 1..256) and plume3d_water_level (an absolute world-Y waterline); it is folded into the SAME instantiateWater with no new Wren signature. The water now renders a vivid saturated cyan → blue palette (matching the reference tutorials, no longer muddy) with a blue reflected sky. WTR2 #3 (ADR 0097) wires the tag all the way to the underwater experience: realistic now resolves to the true water_realistic variant (was wrongly water_pbr) with a per-variant param block and its own clamped knobs (plume3d_water_pbr, plume3d_water_refract, plume3d_water_caustic, plume3d_water_crest, plume3d_water_caustic_strength, plume3d_water_fog), and instantiateWater now AUTO-REGISTERS the water body — a plume3d_water_volume box as a bounded volume, else the largest flat plane — so Graphics.cameraWaterState and the underwater post fire on authored water with NO manual Graphics.setWaterPlane; the blend_water_demo dives the camera into an authored Realistic volume to prove it (buoyancy stays an app-side Water.setWaves path — a WTR2 #4 follow-up). Under the hood a SceneNode can now carry an optional CustomMaterial (the general “a scene node drawn with a pack shader + set-5 params” capability, not water-specific), and the engine fills the one MEASURED param — the camera world position into set-5 p9 each frame — so fresnel / refraction / horizon track a moving camera while the artist owns the look. Additive — the Blender convention plus loader-internal changes, no existing Wren or public C++ signature changed (LowTide/Lexicon-safe), and untrusted-.blend safe (reads only already-parsed props, every set-5 write a hard-coded slot, box verts / transform / level finite-guarded, subdivision hard-clamped, the realistic numeric knobs finite-guarded AND range-clamped in a unit-tested guard header, security-reviewed). See the blend_water_demo example.
  • Underwater & interactive water (camera submersion)The camera can go UNDER the water — the renderer knows where the surface is and whether the camera is below it, so a game cross-fades an underwater look as you dive, completing Plume3D’s water program (engine Block WTR2 — Water Fidelity, ADR 0094). New frozen Wren methods register the surface and report submersion: Graphics.setWaterPlane(surfaceY, fogR, fogG, fogB, fogDensity) registers an infinite flat water surface at surfaceY with its underwater fog (sticky renderer state, like setFog / setSky / setPlanarReflection), Graphics.setWaterVolume(minX, minZ, maxX, maxZ, minY, surfaceY, fogR, fogG, fogB, fogDensity) registers a BOUNDED box pool instead (submerged only inside the box — the same registration a plume3d_water_volume Blender box auto-performs, engine WTR2 #3, ADR 0097), and Graphics.cameraWaterState() returns a List [submerged, amount, waterLevel, fogR, fogG, fogB, fogDensity] for the current camera — amount is the smooth 0 to 1 submersion depth to cross-fade effects on, so passing through the surface is a fade, not a hard cut. The underwater look itself is game-side content — a Graphics.addPostEffect (tint toward the deep colour, fog, caustics, god-rays) faded in by amount, plus a camera-facing waterline meniscus quad on the set-5 custom-material path — no engine change beyond the submersion methods. It composes with the rest of the water program: the two stylized surface variants (PBR and Toon) and Jolt buoyancy (the Water class + Physics.applyBuoyancy) so objects float on the same waves you dive through. The apps/water_playground capstone puts it together interactively: free-fly and dive, toggle the variant and the weather at runtime, and spawn buoyant crates and barrels. The apps/water_raymarch seascape playground is a second interactive dive built on the gameidea.org Seascape look (Alekseev octave waves on a mesh), with its underwater pass gated on the camera-vs-still-level so it never over-paints the surface seen from above. Honest scope: the underwater effect is a screen-space approximation, with a dedicated world-space underwater pass still future work. See the water_playground, water_raymarch, blend_water_demo and water_demo examples.
  • Water FX particles — ripples & sparkleWater SPLASHES and RIPPLES now ride the built-in particle system: the existing ParticleEmitter gains TWO non-physics SPRITE render modes that REUSE the sprite pipeline, so there is NO Vulkan renderer change. SpriteGround (setRenderMode(1)) lays a soft ring texture FLAT on the world XZ plane (a new BillboardMode::Ground) — burst one per beat at a site and concentric, expanding, fading rings build up, exactly a raindrop / footfall / floating object hitting water. SpriteBillboard (setRenderMode(2)) is the same ring CAMERA-FACING, a twinkling sparkle above the surface. The default PhysicsMesh mode (a Jolt body + instanced mesh) is unchanged — splash SPRAY stays physics. Sprite particles are PINNED kinematic decals — no Jolt body, they stay where they spawn and animate ONLY via a scale-over-life and an alpha-over-life curve. Authored from Wren on the same ParticleEmitter — setRenderMode, setScaleCurve (world-unit half-size spawn→death), setAlphaCurve, setColor and setTexture (a Texture from Texture.load). See the particle_fx_demo example.
  • Geometry & SplinesA 3D spline / curve primitive (Spline) — the keystone of Block SPL (splines / geometry / scatter). Build a curve from control points (addPoint / addPointFull with a per-point width for tapering), pick Catmull-Rom (interpolating — passes through every point) or Bézier, and make it an open curve or a closed loop with an optional constant roll about the tangent. Evaluate it two ways: by parameter t ∈ [0,1] (position(t) / tangent(t)) or, for even spacing, by constant-speed arc-length distance (positionAtDistance(s) / tForDistance(s)) — t is deliberately NOT constant-speed, so equal t steps are not equal distances along the curve. frameAtDistance(s) and sampleFrames(count) return rotation-minimizing orientation frames — a flat 13-number [position, tangent, normal, binormal, width] basis that transports along the curve without twist-flipping — the direct input to mesh extrusion (sweep a profile into a ribbon, tube, road, or rail). Vectors are [x, y, z] lists. See the spline_demo example.
  • Procedural MeshesA procedural mesh library (MeshGen) — part of Block SPL (splines / geometry / scatter) — so apps stop hand-rolling grids and boxes vertex by vertex. Static factories build the common primitives: MeshGen.plane(width, depth, subX, subZ) (a flat XZ plane of subX×subZ quads facing +Y), MeshGen.grid(nx, nz, cellW, cellD) (exactly nx×nz vertices — a terrain base), MeshGen.box(sx, sy, sz) (per-face flat normals, AABB = ±size/2), MeshGen.cylinder(radius, height, segments, caps) and MeshGen.cone(radius, height, segments, cap) (about Y, optional cap fans), and MeshGen.heightmap(heights, nx, nz, cellW, cellD, yScale) (a flat nx·nz height list, row-major with X fastest → a displaced grid with recomputed area-weighted normals). Each returns a standard Mesh (triangles, per-vertex unit normals + UVs, white vertex colour) you draw with Graphics.drawMesh(mesh, shader, model) or attach to a scene node — every primitive centered at the origin, Y up. Degenerate parameters are clamped (never a crash); the generators come from a device-free modules/geometry core. Tangents are deferred (v1 = normals only — normal-mapped pack shaders derive TBN from screen-space derivatives). MeshGen also sweeps a Spline into a mesh: MeshGen.ribbon(spline, width, samples, conformScene) sweeps a flat ribbon along the curve — pass a Scene as conformScene and each station snaps onto the ground so the ribbon hugs the terrain (a river down a valley), or null for a flat ribbon, and an optional 5th flow=true bakes per-bend flow so a river’s foam scrolls downstream along each bend of the channel (ADR 0113) — and MeshGen.tube(spline, radius, sides, samples) sweeps a capped closed tube (pipes, cables, rails), both built from the spline’s rotation-minimizing frames so they never twist-flip. See the mesh_primitives and spline_mesh_demo examples.
  • Scatter / PlacementFill an InstancedMesh with thousands of seeded, varied placements — grass, rocks, trees, debris — with the Scatter factories, each returning the count placed. Scatter.onSurface(mesh, scene, minX, minZ, maxX, maxZ, count, seed) scatters candidates across a rectangle and projects each onto the scene’s ground; Scatter.onSurfaceFull adds the knobs (scale range, a slopeMaxDegrees cliff-reject, and alignToNormal to stand upright or lie on the slope); Scatter.alongSpline lines instances along a Spline, jittered sideways across its width — a path, hedge, or roadside strip. It fills the InstancedMesh in C++ (never a 10k-item Wren list, no per-instance callback), bakes scale into each transform, and is seed-deterministic (the same seed reproduces the field). The ground projection rides new surface queries on Physics — Physics.raycastDown / groundPoint / randomPointOnGround return a flat [point, normal] over the scene’s registered colliders (addStaticMesh / addStaticHeightfield) — so surface placement needs a collider matching your terrain. The neutral placement engine is the geometry; density presets and biome masks are a pack concern on top. See the scatter_demo example (~10,600 foliage instances placed onto a heightfield and along a path into one instanced draw).
  • Blender foliage & terrain importAuthor an entire level in Blender — terrain, ground cover, and props — and load it as efficient runtime geometry with one call, part of Block BLD. Scene.loadBlendScene(path) loads a .blend and instantiates its whole environment in a single call — terrain, baked scatter, and static props — then returns the BlendResult so you instantiate any class_name-scripted units (characters, cameras) explicitly. It composes four primitives on BlendResult: instantiateScatter(scene) emits grass / tree / rock scatter (authored with geometry-nodes, particles, or collection-instances, then baked — and you mask WHERE it spawns at bake time with a geometry-nodes density mask or the Bake Scatter add-on’s top-down grayscale mask image, white = spawn / black = clear / gray = sparse) as ONE GPU-instanced draw per prototype (per-instance colour from the baked table, per-instance frustum culling, wind sway) — the same enhanced-instancing path the procedural Scatter factories fill, but authored in Blender instead of script; instantiateTerrain(scene) imports every mesh tagged plume3d_terrain (a bool / int, or a string custom property) as a renderable ground node AND — creating the scene’s physics world if absent — a static concave triangle-mesh Jolt collider from the same geometry, so props and physics bodies rest on it; and instantiateUnits(scene) places the remaining static mesh props (rocks, fences, buildings), skipping terrain, the scatter prototype, hidden objects, and scripted subtrees so nothing double-draws. Any imported object also gets a CONFIGURABLE static Jolt collider on import — tag it plume3d_collider = box / sphere / capsule / convex / mesh (plus plume3d_collider_layer and plume3d_collider_static) via Blender’s Physics collider add-on field, and a tree / rock / fence / prop blocks movement with NO script, generalizing the terrain auto-collider to any object; primitives are sized from the object bounds with its scale baked in, mesh / convex use the geometry. Each returns the count built; coordinates convert automatically (Blender Z-up → engine Y-up). See the blend_scatter_demo (196 baked blades as one instanced patch) and foliage_terrain_demo (loadBlendScene brings in a tagged terrain auto-collidered — dropped spheres rest on the hills — plus 256 baked blades and two rock props, each rock carrying a plume3d_collider tag so a probe rests on it) examples.
  • SceneScene graph, nodes, transforms, cameras, lights (point/sun/spot/hemi/area); findNodeById, tag/collision-layer system
  • ContentBlender 5 .blend loader: meshes, materials, UVs, animations, armatures, physics bodies, particles, blend instantiation
  • ScriptingWren API: Scene, Node, Entity, Mesh, Camera, Light, Input, Raycast, Physics, Resource, AnimationState, AnimController, InstancedMesh, ParticleEmitter, CharacterController, Gui, Ui, Texture, Sprite, Text, Net, PlayFlow, Http, SoundFontInstrument, Midi, Mixer, MixerEffect, Nav, SurroundRing, Ai, BehaviourTree, Bt, Blackboard, LostTargetTracker, AggroTable, Tactic, StatGate, TensionModel, NoiseBus, Shape, Hitbox, Hurtbox, Hit, Combat, Illumination, FogOfWar, SoundEmitter, PhaseEvaluator, Cam, CameraRig, SplitScreen, Viewport, Spline, MeshGen, Scatter, Water, Random, etc.
  • Coroutines / AsyncCooperative coroutines the engine ticks every frame: Async.run { … } starts a coroutine, Async.wait(seconds) pauses it in scene time without blocking the frame, Async.nextFrame() resumes next frame, with Async.pending / Async.clear. Write “do X, wait, then do Y” — sequenced animation, timed events, AI — as straight-line code instead of hand-rolled timers. Built on Wren fibers: cooperative and single-threaded (frame-scheduling, not parallelism/threads); the engine drives it, so the game wires nothing into its own update.
  • InputSDL3: keyboard, mouse, 4 gamepads; “just pressed” state. Mouse capture for FPS/orbit cameras — hide and lock the cursor to the window center and read relative deltas (Input.setRelativeMouse / Input.mouseDeltaX/Y), with an independent cursor-visibility toggle (Input.setCursorVisible) and automatic release on window focus loss so alt-tab never traps the cursor; the built-in cameras look correctly the instant capture is on. Touch is tracked in the C++ InputState (up to 8 fingers) but has no Wren binding yet — not scriptable.
  • AudioOpenAL: 3D positional audio, HRTF, Doppler, seek/tell, source clone, WAV/MP3/OGG/FLAC; streaming float PCM (Audio.newStreamingSource + source.queueSamples) for procedural/synthesized audio fed over time; SoundFont (.sf2) instrument playback (SoundFontInstrument) — load a bank and play polyphonic notes from any source; Unity-style mixer groups (Mixer/MixerGroup) — route SFX/Music/Ambience/instruments to buses with per-group volume, mute, solo, ducking (music-under-dialogue sidechain), and per-group effects via OpenAL EFX — the whole aux-slot suite (reverb, echo, chorus, flanger, distortion, EQ, pitch, ring-mod, autowah, frequency-shifter, vocal-morpher, compressor), chainable, tuned live with addEffect(type)/setParam, plus per-group direct-path low/high/band-pass filters (setLowPass/setHighPass)
  • MIDI inputLive MIDI device input on desktop (libremidi): Midi.inputDevices / openInput / poll normalized note, controller and pitch-bend events. Parsed off the device thread, delivered on the main thread. Midi.route(instrument) plays a SoundFont straight from a USB/virtual MIDI keyboard (native low-latency route).
  • PhysicsJolt: static/dynamic/kinematic bodies, box/sphere/capsule/cylinder/convexHull shapes, triangle-mesh + heightfield terrain colliders, tunable material (restitution/friction/damping), constraints (hinge, slider, cone, 6DOF), contact events (begin/end/stay), full Wren API (forces, impulses, velocities, raycast, pause); spatial queries for gameplay/AI — overlapSphere (bodies near a point) and a per-body gameplay collision-layer bitmask (setCollisionLayer/getCollisionLayer) that filters overlaps + masked raycasts (Raycast.fromPointMasked) without changing what physically collides; manual fixed-step simulation (Physics.step/stepN) for deterministic, headless runs; surface / ground queries (Physics.raycastDown / groundPoint / randomPointOnGround) that project a point straight down onto the scene’s registered colliders and return a flat [world point, surface normal] (or null) — the placement primitive behind scatter and terrain-conforming spline extrusion Motion is SMOOTH at any frame rate: physics runs at a fixed 60 Hz, and physics-driven objects are drawn interpolated between their last two simulation steps, so a fast-moving object does not step ‘velocity divided by 60’ at a time on a higher-refresh display (measured: per-frame jitter 0.1000 -> 0.0119 for a 6 m/s target at 120 fps). Gameplay is unaffected — the interpolated pose exists only for the render half of the frame, so node.getPosition(), raycasts and anything you replicate still read the exact simulation state, and physics stays independent of frame rate. Toggle with [Window] render_interpolation; a dedicated server never draws and turns it off. Characters are HITTABLE and SOLID: a CharacterController carries an inner kinematic body (Jolt mInnerBodyShape), so raycasts, sweeps and overlaps find characters and dynamic bodies collide with them instead of passing through — previously a character had no broadphase presence at all, so nothing in a game could hit a player. Characters register on their own collision-layer bit for deliberate include/exclude, hits attribute back to the character via RaycastHit.getBodyId() == controller.bodyId, Physics.overlapSphere returns a character’s bound node (AI perception sees players), and the camera occlusion collider masks characters out so they never yank the camera. Kill switch: [Physics] character_inner_body = false (ADR 0121).
  • Character ControllerCapsule-based CharacterVirtual (Jolt): ground detection, step, jump, state machine (idle/walk/run/jump/fall/swim/crouch), configurable speeds
  • CameraCinemachine-like camera rigs with no draw() plumbing: one-call Cam.thirdPerson / firstPerson / topDown / orbit / freeFly over a moving target, camera blending (Cam.blendTo with easing — position lerp + rotation slerp + lens crossfade), procedural handheld noise, impulse shake (Impulse.emit — decaying + distance-attenuated), and an occlusion collider (CameraRig.collider — pulls the camera in front of walls that would hide the target). Author the whole rig set (and an optional split-screen layout) as DATA in a cameras TOML — Cam.loadAssets(scene, path) builds every rig in one call and Cam.rig(scene, name) looks one up by name — the data-authored counterpart to the code-side rigs above (ADR 0104). A camera authored in a .blend drives the rig system too: Cam.fromNode(scene, node) binds its authored world pose + lens (blend focal length → FOV, near, far) as a static rig — or a cameras-TOML type = “static” rig does the same from data — so the framing an artist set in Blender becomes a first-class rig you can setActive or blendTo (ADR 0106). Local co-op split-screen (SplitScreen / SplitLayout): each player gets their own third-person view in its own screen region, with dynamic merge/split — the views fuse into one group-framed camera when players cluster and split back apart when they spread, as a smooth animated blend (transitionDuration / mergeFraction). Layouts include a Kronnect-style angled split (SplitLayout.angled) — a 2-player diagonal divider that rotates with the players — and an animated rect-peel merge (SplitScreen.peel), all composited from per-viewport offscreen targets. The same RTT path powers a raw Viewport primitive (Viewport.new) for picture-in-picture / minimap / security-cam — a screen region with its own camera, composited over the main view by z/opacity. Split-screen overlays follow the split: Graphics.perViewportOverlays(true) replays both the retained-UI HUD and SDF text into each cell — screen-space overlays scaled into the cell, world-space nameplates re-projected by that cell’s own camera, so each player’s HUD and labels stay in their own region (ADR 0064 / 0105). A per-scene director auto-ticks after update() and pushes the view/projection itself; coexists with the manual Graphics.setViewMatrix path (last-write-wins). Device-free, unit-tested solver core; plus the low-level Camera class (view/projection, world/screen conversion) it drives. Mouse-wheel and ARPG-style ZOOM: CameraRig.zoom(notches) dollies the boom multiplicatively (so one notch feels the same at 2 m and 20 m), clamped by zoomRange, with lensDamping to ramp FOV instead of popping — which is what an aim-down-sights transition wants. Damping now covers three separate quantities (the eye, the boom anchor, and what the camera POINTS AT), with optional per-axis time constants so vertical motion like stairs can be smoothed harder than strafing, plus velocity lookahead that leads a moving target and vanishes at rest. Aim modes are not a mode: a blend now carries look state across, so over-the-shoulder aiming is simply a second rig you blendTo.
  • AI & NavigationAuthor enemy AI in Wren: a behaviour tree (BehaviourTree + Bt.sequence/selector/leaf with closure leaves over a typed Blackboard) that the engine ticks for you — no per-frame plumbing; a lost-target → search → give-up FSM (LostTargetTracker), aggro/tactic/stat-gate selection (AggroTable, Tactic, StatGate) and an L4D-style pacing director (TensionModel); a detect-by-sound noise bus (NoiseBus / AlertMemory / PainMemory); and one-call perception (Ai.nearestVisibleTarget — range + field-of-view + line-of-sight, in one native call). Underneath: a Recast/Detour navmesh (Nav.bake / Nav.findPath) that routes around obstacles — genre-general (top-down / third-person / FPS), not a top-down- or swim-specific abstraction — and a surround-ring solver (SurroundRing) so a pack encircles a target instead of stacking on it, composing with the perception primitives on Physics (overlapSphere — who is near me; Raycast.fromPointMasked — layer-filtered line of sight). Device-free, unit-tested cores; see the ai_hunters example, which drives the whole loop from one Wren tree.
  • Behaviour-Tree AuthoringAuthor enemy / mini-boss / boss behaviour trees as DATA and load them at runtime — for a GUI editor. The runtime uses Wren closures for leaves (great for hand-written AI), but a GUI can’t draw a closure, so the data path adds NAMED leaves: Bt.registerLeaf(name, paramSpec) registers an action leaf the editor lists (Bt.leafCatalog) and a .bt.toml references by name with typed params, and Bt.load(path) builds an engine-ticked tree from a hardened, bounded, cycle-checked .bt.toml (unknown-leaf / bad-param / cycle → Bt.loadError, existing tree left intact — the loader passed a blocking /security-review). Additive to the closure surface (Bt.leaf is unchanged). Boss phases: PhaseEvaluator.evaluate(currentPhase, health01, elapsed, thresholds) returns a forward-only phase the game feeds health01 into (the engine never reads HP) and writes to a blackboard int a phase_switch node reads — a regular enemy is one root, a mini-boss ~2 phase entries, a boss 4+, the same data machinery. The engine runs the tree; your game owns the policy (HP, damage, phase thresholds). See the bt_editor example — a palette from the live catalog, a 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 — and the bt_boss_demo example, where a phaseless enemy, a 2-phase mini-boss, and a 4-phase boss are one schema differing only in data.
  • Light, Sound & VisionLight and vision as mechanics — the engine samples, the game owns the policy (no stealth/HP rules ship in the engine). Illumination.illuminanceAt(scene, x, y, z) → a continuous 0..1 “how lit is this point” (per-light falloff matching the render, with masked line-of-sight occlusion) — bidirectional (your lamp lights you), a raw value the game thresholds for a hard “spottable” stat or reads as a soft exposure meter. FogOfWar — a world-XZ visibility field (Unknown/Explored/Visible): configure bakes an occluder grid from static geometry, reveal marks line-of-sight cells with the view radius shrinking in the dark, Explored persists as memory, both team-shared and per-player, plus a wall-penetrating sonar reveal channel. The engine RENDERS the fog on-screen too — a depth-based post-process darkens the world by the field (unknown → black, explored → dim + desaturated, visible → full) with no game render code, pixel-exact at any camera angle, with a Graphics.fogSoftness dial from crisp cells to a smooth fade. NoiseBus.emitOccluded — the same wall that muffles a sound shrinks what the AI hears; SoundEmitter unifies the audible and audible-to-AI sides. Device-free, unit-tested cores; see the lsv_demo (exposure + noise HUD) and fog-demo (on-screen fog) examples.
  • Action CombatHit/hurt boxes are Jolt sensor bodies riding a character’s bones — authored in Blender (meshes in Plume_Hitboxes / Plume_Hurtboxes collections, name = tag) or built by hand — and Jolt’s own collision detection drives Combat.onHit / onStay / onExit (owner/layer filtered, once-per-swing by default, with independent / reHitSeconds for flurries and damage-over-time). Instantiated volumes surface on node.hitboxes / node.hurtboxes tag maps; node.attachToBone is an engine-driven weapon/VFX socket. The engine DETECTS hits; your game OWNS health — there is no HP/damage/death type in the engine, only an opaque payload carried into each Hit (your damage or entity id). Hitbox.bindWindow gates a strike to an animation hit-window authored as Blender pose-markers; a disabled hurtbox is i-frames. Plus swept/overlap/pierce shape queries on Physics (castShape / overlapShape / raycastAll) over rigid bodies. Device-free, unit-tested core; see the combat-blender-demo example.
  • AnimationKeyframe/FCurve from Blender: AnimationState, blend layers, crossfade, events (Blender markers), drivers, skeletal (bone hierarchy, skinning matrices)
  • Skinning debug rendererSee what the rig is actually doing, not just what it looks like. Graphics.setSkeletonOverlay(mode) draws each skinned node’s bone hierarchy as lines over the scene, straight from the engine’s own runtime bone matrices — the one view of a skinned character that bypasses the skinning shader, so a mangled mesh with a sane skeleton means the skinning is wrong and a mangled skeleton means the pose is. Modes filter to the deform bones the mesh really skins to (~68 of 344 on an AutoRigPro humanoid), optionally drawing the rest pose behind the animated one as a reference, or show every IK and control bone; line colour encodes hierarchy depth, so a chain on the wrong parent reads as a colour break. Alongside it, Graphics.setDebugView adds skinning views — dominant-influence weight heat map, a colour per bone index (the mis-binding test), and influence count. Graphics.setSkinnedMeshHidden(true) hides the mesh while still posing it, leaving the bones alone on screen against the static scene. Available in every runtime variant except shipping.
  • Animation ControllerPer-character AnimController (node.attachController): a TOML-authored graph of states, blend nodes (1D/2D), guarded transitions, a Mecanim-style Any State (global transitions that fire from any state), masked overlay layers (override/additive, with runtime per-layer weight via setLayerWeight) and root motion, with a lean State / Action / Context model — Actions overlay locomotion without exiting it, Contexts gate what’s allowed. Plus typed params, clip phase (normalizedTime, tagged sections, latch-and-refire END), behaviour hooks, and transition events (a named event per transition + an onTransition firehose). Auto-ticked and hot-reloaded each frame. Authored via a TOML asset, with a visual node editor (apps/node_editor) that round-trips it — states, blend trees and clips as nodes, with typed any-direction wires (transitions, plays, blend entries), a Layers panel, and a 3D preview that plays the edited controller on a skinned character with live layer-weight sliders. LAYERED STATES (engine PLM-333, ADR 0123): a [states] entry with layer = ”…” puts an AUTHORED blend tree (an aim pose, a look blend) on a masked overlay layer, played at runtime with ctrl.playOnLayer(name[, fade]) and cleared with stopLayer — the over-the-shoulder aim substrate with zero aim concept in the engine (aim = a second camera rig you blendTo + a masked pose layer you play). Additive-layer correctness is measured on the real shipped rig: weight 0 is the base pose exactly, and a known delta applies once, never twice.
  • ParticlesTier 2 Jolt physics particles: body pool, emission rate, burst, play/stop/pause; rendered via instanced mesh
  • Instanced MeshGPU instancing via SSBO: up to 65K instances per draw, per-instance position/transform/scale, dynamic updates, visibility toggle. Enhanced instancing gives each instance its OWN base colour (addInstanceColored) plus a small params block (setInstanceParams — a height/scale multiplier and a wind-phase offset so a scatter field sways without looking stamped from one copy), and the mesh can opt in to per-instance frustum culling (setCullingEnabled) so a big field only pays for the instances actually in view — visibleCount reads back how many were drawn. Per-instance colour + params ride one widened 96-byte SSBO record [transform, baseColor, params] — no extra buffer or descriptor. See the grass_field example (4096 grass blades, per-instance colour + height + wind sway + culling).
  • Sprites & TexturesTexture.load (PNG/JPG/WebP from the mounted project) → GPU texture; Sprite: camera-facing, depth-sorted, alpha-blended quads with world-space size, RGBA tint, and spherical/cylindrical/flat billboard modes
  • SDF TextSigned-distance-field text, crisp at any scale: world-space draw(model) or screen-space drawScreen(x, y); face/outline color, outline width, softness, align, wrap, line spacing, rich-text markup, measured width/height/lineCount; built-in Inter font
  • GUIFull Nuklear API: themes, fonts, layouts (dynamic/static/template/space), sliders, knobs, combos, trees, popups, menus, charts, color picker, custom drawing
  • Retained UIData-driven retained-mode UI (Ui.load / Ui.render): author a panel/text/button tree in a .ui TOML file, render it letterboxed into a screen rect (uGUI-style anchors, resolution-independent); buttons dispatch on_click → Game.<name>(); panels/images draw as quads, labels reuse the SDF text pass. Panels can draw a textured 9-slice (fixed corners, stretched edges/center) with the image referenced by path (image = “ui/panel.png”) and slicing authored once in a .ui.component sidecar beside the PNG. Widgets: panel, text, button, toggle (checkbox), slider, progress bar, radio (grouped, mutually exclusive), text input (editable single-line fields, focus by click, UTF-8-aware caret), and a canvas element for custom drawing (rects/lines/bezier/text via Ui.canvas*). Container elements can auto-position their direct children with layout groups (horizontal / vertical / grid, with spacing/padding) that compose with the anchor system. Includes a runtime authoring API and a WYSIWYG visual editor (apps/ui_editor, —tool): element tree, inspector, live canvas, selection gizmo
  • Configgame.toml, class-based scripts (e.g. Radio, GameCamera), hot reload
  • Save & LoadGame-authored, versioned save state: SaveState is a typed key/value store (bool / int / number / string); Save.write / Save.read / Save.exists serialize it to human-readable, versioned TOML under the app’s write directory. Not an automatic scene dump — the game chooses what to persist. A save written by a newer engine build reads back as null (rejected, never corrupted); enumerate slots with Resource.list.
  • LightingMultiple lights (point/sun/spot/area) with shadow casting; Graphics.setLights + shadow mapping controls
  • NetworkingGameNetworkingSockets transport: Net.startServer/connect/send (reliable or unreliable)/poll; server-authoritative entity replication with snapshot broadcast and client-side interpolation; dedicated-server mode (plume3d —server) runs game logic headless; PlayFlow Cloud client to request and poll a hosted server. Netcode can be tested OFF the LAN: game.toml [Network].sim_* simulates packet loss, added latency and reordering on the engine’s own link, so replication and interpolation can be exercised under a realistic connection instead of a perfect loopback one (see the net_conditions_demo example). The wire is now VERSIONED and ORDERED (engine PLM-332, ADR 0122): every replication snapshot carries a magic + protocol version + the server’s simulation tick + server time, a late reordered packet can never regress entity state (snapshots are tick-keyed), and connections complete a version-gated HANDSHAKE before they surface — ‘connected’ means both peers agree on the protocol, an incompatible build never surfaces at all, and the handshake reserves room for a player-identity token so an auth provider plugs in without a second wire break. Peers get engine-enforced receive budgets, the Wren listen path is capped by [Network] max_clients (Net.startServer(port, maxClients) to override), and games can read Net.rtt(conn), Net.serverTick() and Net.serverTime() — the clock later prediction phases are written in. SERVER AUTHORITY ships (engine PLM-334, ADR 0124, protocol v2): a client expresses INTENT, never position — Net.sendInput carries a typed, tick-stamped frame (move vector clamped to [-1,1], the ADR-0123 quantized yaw/pitch, buttons) that decodes in C++ with the full malformed-input treatment, the server reads each connection’s newest input with Net.inputFor (attributed by connection identity — unforgeable) and is the sole transform writer, entities replicate with an OWNER field so every client finds its own entity by owner == Net.clientId(), and every snapshot acks the consumed input tick per connection (the prediction hook, already on the wire). The movement hot path contains no string parsing end to end; co-op server-authoritative with interpolated remotes is the shipped, demonstrated model (the arena demo steers by WASD intent; its headless probe attributes itself by ownership).
  • HTTPHttp.post(url, body): blocking JSON POST over HTTP/HTTPS → Map of ok/status/body, for out-of-band traffic like opt-in telemetry. Blocking — call it at event boundaries, never per frame.
  • RandomnessSeeded deterministic RNG (PCG32): Random.new(seed) → int, intRange, float, shuffle; identical seed ⇒ identical sequence cross-platform, for reproducible runs and daily seeds. Engine.entropy() gives a non-deterministic OS-entropy seed when you need uniqueness (install ids) instead of reproducibility — not cryptographic.
  • Tooling & TestingHeadless test mode (plume3d —test) runs Wren game logic with no window/Vulkan and exits 0/non-zero for CI; drive physics deterministically with Physics.step to assert simulated outcomes headlessly; Engine.exit(code); —tool editor mode for in-engine tools; automatic local crash reports on macOS/Linux — a fatal signal writes crashes/crash_<pid>.txt (signal, pid, backtrace) and is never uploaded, on Windows this is currently a no-op
  • Command-line toolingplume3d —help prints the full command list; plume3d —tools lists the four built-in editor tools (ui_editor, node_editor, bt_editor, toml_editor) with a one-line description and the —tool <tool-app> <target-app-dir> launch form; plume3d init [dir] scaffolds a runnable project — a commented game.toml with default physics layers, a rendered-triangle main.wren, README.md, and the precompiled triangle shader — with -i/—interactive prompting for name/version/description/window size/author/terminal and —force to overwrite. See the Command line reference.
  • Packaging & distributionBundle a game into a single distributable with one command. A Plume3D app folder becomes a .p3d package — a zip of the cleaned app dir (the plume3d binary, logs and shader-compile helpers dropped; main.wren, game.toml, compiled shaders/bin/*.spv and assets kept) plus a package.toml manifest at its root (id, name, version, and the entry script — finally honored, replacing the hardcoded main.wren — plus the window config and the engine variant it was built against). The runtime mounts a bundled .p3d sitting next to it automatically, so a packaged app just runs when launched. The plume3d-pack host tool (separate from the runtime) produces the .p3d and wraps it in a native shell around the shipping runtime: a macOS .app (Info.plist + Resources/<id>.p3d), a Windows portable .zip, or a Linux .tar.gz. package.toml is a frozen, untrusted-input-validated manifest — an entry that is absolute or escapes the package (..) is rejected, the id charset is restricted, window dims clamped. Desktop unsigned ships now; native installers, code signing/notarization, and mobile/web shells are follow-ups. See the Packaging & distribution guide.

Short-Term: Indie-level (small–medium 3D)

Section titled “Short-Term: Indie-level (small–medium 3D)”
  • Material texturesNormal map, roughness/metallic texture maps — PBR structure in place; texture samplers not yet wired to Wren.
  • AudioOptional reverb zones (OpenAL EFX extension); environmental audio effects.
  • Basic LODOptional: swap mesh by distance (single LOD fine for many indies).
  • Depth pre-passOptional opaque depth pre-pass to cut overdraw (perf) — back-face culling and alpha/double-sided handling already ship (see Rendering, above).

Long-Term: AA-level (larger scope, higher fidelity)

Section titled “Long-Term: AA-level (larger scope, higher fidelity)”
  • Lighting pipelineMany lights (deferred or clustered forward), CSM, point cubemap shadows, contact shadows.
  • Global illuminationLight probes, SSAO, simple reflection (probes or SSR).
  • Post-processingBloom, tone mapping, DoF, motion blur, color grading.
  • Asset pipelineMore formats (FBX/glTF), texture compression (ASTC/BC), clear “cook” step for release.
  • Level streamingLoad/unload scene chunks by region; async asset loading.
  • Occlusion cullingPVS or GPU occlusion to skip hidden objects.
  • LOD systemMultiple LODs per model, selection by distance/screen size, optional impostors.
  • Animation+IK, runtime retargeting. (Blend trees already ship — see Animation Controller, above.)
  • Physics+Ragdolls, vehicles, trigger volumes.
  • TerrainHeightmap or splat terrain, texture splatting, optional foliage.
  • UI scaleScale Nuklear to complex HUD/menus; layout, themes, localization.
  • Profiling / toolsFrame profiler, scene inspector, Blender-centric workflow tools.

The engine uses these vendor libraries for shaders, file access, GUI, and config:

LibraryPurpose
SlangShaders. Compiles .slang shader source to SPIR-V for Vulkan. Apps ship compiled .spv files; the engine can watch source and recompile on change when hot-reload is enabled.
PhysicsFSVirtual filesystem. Mounts the game directory or a .zip/.p3d archive as the project root so all asset and script loading (shaders, Wren, audio, TOML, blends) goes through one path.
NuklearImmediate-mode GUI. Single-header UI toolkit for windows, layout, labels, buttons, and edit fields. The Vulkan integration provides a Nuklear backend so Wren’s Gui API draws with Vulkan.
TomlPlusPlusTOML parsing. Reads game.toml and engine.toml for window, design size, scale mode, physics layers, collision matrix, and other config; used by the host and config_toml module.

Supporting modules (used by the host and integrations) provide:

ModulePurpose
app_packageResolves the game path (directory, game.toml file, or .zip/.p3d archive) into a mount root for PhysicsFS so the engine can load assets and scripts from the project.
blend_loadLoads Blender .blend files (via Kaitai Struct): parses scene data, objects, cameras, lights, materials, textures, and mesh data so the engine can instantiate hierarchies and render them.
config_tomlParses game.toml and engine.toml: window size, design resolution, scale mode, physics tag/collision layer names, and collision matrix.
audio_loadDecodes audio from memory (WAV, MP3, OGG/Vorbis, FLAC) into PCM for OpenAL. Used with PhysicsFS so all file access stays on the mounted project.
hotreloadPolling-based file change detection: watches config and script files by mtime so the engine can reload games and shaders during development without restarting.
ToolPurpose
Blender addonLets you attach Wren class and config path references to objects and collections in Blender. The addon stores config path, script path, class name, Node Id, and tag/collision layers in the .blend; the engine loads config and scripts from the project and instantiates your classes. Save the .blend after configuring so the engine sees the metadata.
VaultisA companion desktop asset manager for game developers (macOS/Windows/Linux). Discovers Unity Asset Store packages (.unitypackage, .zip, .tar), imports them into a local vault, provides 3D model and audio previews, and exports assets to engine projects (Unity, Unreal, Godot, or custom — suitable for Plume3D). See vaultis.wyldmagic.gg for more.

New to Plume3D? The Getting Started guide walks you through:

  1. Get the binaries — Email hello@wyldmagic.gg or see Get Plume3D; extract and run from your app folder.
  2. Get the Blender addon — Email hello@wyldmagic.gg to request the addon zip; install in Blender to attach Wren scripts and config to objects and collections.
  3. Write an initial app — Create a folder with game.toml and main.wren, implement the Game lifecycle (init, update, draw), and run it with plume3d ..
  • API Reference — Full Wren API: Engine, Logger, Mesh, Input, Window, Graphics, Gui, Audio, Config, Resource, Scene, Node, Camera, Light, Raycast, Physics, CharacterController, InstancedMesh, ParticleEmitter, Animation, Math, Texture, Sprite, Text, Net, PlayFlow, Http, Random.
  • Examples — Run and learn from the example apps (aspect ratio, audio, GUI, mesh, blend loading, 3D radio, etc.).
  • Licensing — License terms for the Plume3D engine, this documentation site, and third-party components.