Skip to content

Aura3D 4.1.0

Pre-release
Pre-release

Choose a tag to compare

@gchahal1982 gchahal1982 released this 24 Oct 19:47
· 3051 commits to main since this release

Superseded. This version is deprecated. Upgrade to Aura3D 4.7.12.

A large release: eight reusable kits extracted from the eleven Crayon game ports and the Aura Slam 3D conversion, plus the engine fixes those games exposed. Everything below is in the published @aura3d/* packages unless it is listed under Tooling and examples.

Highlights

  • Library kits for games: a published game kit that defaults on at @aura3d/engine/game, a QA and replay hook kit, an opt-in runtime safety net (?diag=1, black-frame self-heal), a procedural audio synth kit, a production model loader, a GLSL chunk library, seeded scatter planning with static merge, and racket-sport kits (exact ball flight, scoring, athlete rig).
  • Engine fixes found on real hardware and in production builds: sun shadows no longer darken far more than the caster's footprint, instanced and batched meshes show their textures, empty instance arrays no longer blank the canvas, custom shaders are NaN-safe on Apple GPUs, and repeated model() nodes share decoded textures.
  • CLI asset tools: assets optimize, assets merge-clips, an isolated assets import-meshy, and a meshy ledger command.
  • Deprecations extended: the ten deprecated @aura3d/engine/* subpaths and the deprecated root symbols are now planned for removal in 5.0.0, not 4.1.0 (see Deprecations).

Library kits

  • New: game.racketSports().scoring(rules?) (createRacketScoring): a pure game/match/tiebreak scoring state machine for rally sports. The defaults are the Aura Slam tennis rules (4 points win by 2, first to 3 games leading by 2, tiebreak to 7 win by 2 at 3-3); pointsToGame, winBy, gamesToMatch, matchWinBy and tiebreak are configurable, so table-tennis style formats work too. awardPoint(state, side) returns a new state plus gameWon/matchWon/tiebreakStarted, and label(own, opponent) gives 0/15/30/40/AD. Verified point for point against the original tennis simulation over 2,000 seeded matches.
  • New: game.racketSports().ballFlight(config) is an exact, frame-rate independent ball-flight kit for racket sports (x lateral, y along the court, z up): closed-form flight under gravity and drag, exact-time bounce/net/tape/board events, surface rebound and rolling, a shot solver that clears the net, a serve solver and a loose-ball settle. It was extracted from the Aura Slam 3D simulation and reproduces the original game's physics to 1e-12 (docs/api/ball-flight.md). The existing stepBallPhysics remains as the simple model.
  • New: game.racketSports().athleteRig (docs/api/racket-athlete-rig.md): a procedural racket-sport athlete extracted from the Aura Slam port. locomotion() is a pure stride/split-step/slide/planted-stance solver and applyLimbs() poses leg, arm (two-bone IK) and racket cylinders from root-local points; solveKnee() is the knee helper. Verified bit-identical against the original game's pose code (60,000 cases). The five existing geometry helpers moved to an internal module and are re-exported unchanged.
  • New: createBindingStore in the game kit (packages/game): validated, persisted, rebindable input bindings (keyboard primary + optional alternate keys, controller buttons), reserved keys with custom messages, conflict detection, per-device fallback on corrupt or wrong-version storage, and saved: false instead of throwing on write failure. Extracted from the Aura Slam 3D controls store; an equivalence test shows it validates, persists and restores identically when configured with that game's actions. It is exported from @aura3d/game (private until the game kit is published through @aura3d/engine/game).
  • New: QA and replay kit (docs/api/qa-kit.md): installQaHooks, createFixedStepLoop (alias fixedStepLoop), readQaParams, createCountedRng and the hook-name helpers, exported from @aura3d/engine. They replace the window.__AURA_<GAME>__ hooks and fixed-timestep loops that every Crayon port copied into its own main.ts. The Splat Rush port is migrated as the reference.
  • New: opt-in runtime safety net in @aura3d/debug (installSafetyNet, resolveQualitySwitches, createBlackFrameWatch, installDiagnosticsPanel). Parses the shared quality URL switches (?post=0 ?shadow=0 ?env=0 ?sky=0 ?aa=0 ?quality=low ?tm=aces ?heal=0 ?diag=1), reloads one rung down a compatibility ladder when the canvas stays black (SAFETY_BLACK_FRAME / SAFETY_COMPAT_MODE), and shows a GPU diagnostics panel (renderer, limits, draw calls, shader compile/link failures, context loss, last errors). Nothing runs unless safety is set; see docs/api/safety-net.md.
  • New: defineModelAssets(manifest, options?) and modelsEnabledFromUrl() declare a game's GLBs once with the sha256 and same-origin URL the production bridge requires, validate them at definition time (a bad hash or cross-origin URL used to draw nothing silently), return typed refs, and carry the procedural-fallback switch (?models=0, or enabled) as an option. See docs/api/model-loader.md.
  • New: procedural audio synth kit in @aura3d/audio (also @aura3d/engine/audio). createCueBank({ cues }) plays sample-free cues built from oscillator and filtered-noise layers (envelopes, pitch glides, delays, detune) on the page's single AudioContextManager, with first-gesture unlock, a master mute and key binding (install()), SFX/music buses, a voice budget with priorities, per-cue cooldown/concurrency limits and music ducking. createStepSequencer is a lookahead music scheduler, createToneLoop a parameter-driven bed (engine drone, wind, pad), and midiToHz, noteFrom, SCALES, scaleSemitones and a deterministic createNoiseSource cover pitch and noise. It replaces the hand-rolled Web Audio code the ports each carried; the Splat Rush port now uses it. See docs/api/audio-synth.md.
  • New: recorded takes in the synth kit (createSampleSlices, SampleSlices; docs in docs/api/audio-synth.md). Plays one slice of a sprite sheet per call on the cue bank's buses, never repeating the previous take, through an optional low-pass, gain and stereo panner, with a procedural fallback until the sheet decodes. CuePlayParams.data carries a free-form payload to a cue's layer function. Extracted from Aura Slam's tennis-audio.js.
  • New: author GLSL chunk library for material.custom (docs/api/glsl-chunks.md). #include <a3d_math> (saturate, safe divide/sqrt/acos/normalize, remap, a3dFinite), <a3d_noise> (sin-free hashes, value and simplex noise, fbm, ridged), <a3d_sky>, <a3d_fog> (linear, exp2, analytic height fog), <a3d_triplanar>, <a3d_toon>, plus <colorspace_decode> and the new <colorspace_encode>. All are GLSL ES 3.00, a3d-prefixed, NaN-safe and pow-free; a material patch or registerShaderChunk still overrides them. Verified on the mock device and on a real WebGL2 context (every chunk links; known-value probes render exact colours). They are GLSL only, so a material that includes them has no WGSL twin.
  • New: scatter planning and static merge (scatterAlongPath, scatterInArea, splitScatterByDistance, scatterTransforms, createScatterRandom, mergeStatic, mergeStaticByKey; docs in docs/api/scatter-merge.md). Seeded, filter-stable placement along paths or over areas (exclusion zones, minimum spacing, height and slope filters, radius LOD bands) that feeds instances.*, and a transform-and-tint baker that turns many static parts into one geometry per material. Extracted from the hand-written placement and merge code in the Crayon ports; the Cloud Runner mesa layout is reproduced exactly for its seed in tests/unit/scatter.

Engine fixes

  • Fix: material.custom shaders are NaN-safe on real GPUs. GLSL leaves pow(x, y) undefined for a negative base; software GL evaluates it, but Apple/Metal GPUs return NaN, which blending writes into the HDR target and bloom smears over the whole frame (a black scene on an Apple M4 Max in one port). Custom shaders whose source uses pow now get a total a3dPow (pow(max(abs(x), 1e-30), y), applied through a pow macro), and the output stage and the bloom downsample drop non-finite samples (a3dFinite). A negative base now yields pow(abs(x), y) instead of NaN. Not yet confirmed on the reporting machine.
  • Fix: the game kit imported the deprecated @aura3d/engine/contracts subpath, so every game using @aura3d/engine/game printed a deprecation warning that said removal in 4.1.0; it now imports from the @aura3d/engine root.
    Fix: directional (sun) shadows no longer darken far more than the caster's footprint. The forward path samples one cascade per render item, and a large receiver such as a ground plane has fragments whose light-space depth lies beyond that cascade's fitted depth range; the manual depth compare treated them as shadowed against the cleared depth of 1.0, so one small caster shadowed a whole half of the ground (about 80,000 of 230,400 pixels at fov 15; about 500 after). The receiver depth is now clamped to [0, 1] in every directional sampler (generated forward chunk, legacy core library, packaged pbr-direct, WebGPU forward), like a hardware compare sampler. tests/browser/shadow-footprint.spec.ts and examples/shadow-lab/ reproduce and guard it. Projects that switched sun shadows off because every receiver rendered fully shadowed (for example the Ninja Runner port) can switch them back on.
  • Fix: instances.mesh and batched.mesh now honour the material's texture maps. The batched/instanced handler only built the scalar PBR material, so texture, normalMap, emissiveMap and the other map slots (URLs, asset refs and texture.* handles) were dropped and the surface rendered white; ordinary primitives got them from the post-mount textured upgrade, which batched nodes never joined. The handler now runs the same upgrade once per node and swaps in the TexturedPBRMaterial when it resolves (the scalar material until then); dispose releases it. Also: a multi-geometry batched.mesh drew nothing unless qualityRebuild.flags enabled tiers.batching, because the packed multi-draw route's marker items need that submit path; without the flag it now uses per-geometry instancing (fallbackReason: "tiers-batching-off"). A texture.data handle used as a base-colour map must declare colorSpace: "srgb" (binding validation rejects linear base-colour textures).
  • Fix: createGame from @aura3d/engine/game returned the headless stub unless the caller passed qualityRebuild: { flags: ["game"] }. That entry point is itself the opt-in to the real game kit, so the game lane now defaults on there; -game, none, the ?a3d-qr=-game URL and A3D_QR_GAME=0 env still turn it off. The root createGame in @aura3d/engine stays the headless stub and now logs CREATE_GAME_STUB once, pointing to @aura3d/engine/game. The game kit imports from the @aura3d/engine root instead of the deprecated @aura3d/engine/contracts subpath (removal is planned for 5.0.0). The kit was already published as the @aura3d/engine/game subpath of the root tarball (no new package); a racing-starter build against the published 4.0.8 tarball was used as the check. Docs: docs/api/game-kit.md.
  • Fix: an empty instanced node no longer blanks the canvas. instances.<primitive> and instances.model with zero transforms used to throw at build time, and a zero-length instance array threw INSTANCING_CONTRACT inside ForwardPass (before sorting), which stopped the frame. Zero instances is now valid: the compiler skips the node and reports INSTANCES_EMPTY, batched.mesh/instances.mesh with no live members emit nothing, and ForwardPass drops zero-length instanced items. A geometry drawRange of count 0 (three's setDrawRange(0, 0)) draws nothing instead of throwing.
  • Fix: empty points / lines no longer throw. Zero positions builds and compiles to nothing and reports GEOMETRY_EMPTY; a one-point polyline is still an authoring error.
  • Perf: repeated model() nodes of one GLB share decoded textures. createTypedGLBActor now takes a ref-counted GLTFSharedTextureCache per (asset url/id/sha256, decode options) when none is passed, so N nodes decode and upload each glTF image once instead of N times (20 actors of a 3-image GLB: 60 decodes before, 3 after). Textures are freed when the last actor disposes; shareTextures: false opts out and cloneSkinned keeps its own cache. Geometry buffers are still per node.
  • Fix: example and app entry modules survive production bundles with plain imports. The root sideEffects allowlist now covers ./examples/**, ./apps/** and ./templates/**, so the eleven port wrappers dropped their __AURA_PORT_MODULES__ workaround.
  • Fix: the cue bank's mute key did nothing unless an onMuteChange callback was passed. install() evaluated options.onMuteChange?.(this.toggleMute()), which skips the toggle when the callback is absent. It now toggles first and then notifies.

Behaviour changes

  • Directional shadows cover less ground. After the shadow depth fix scenes with a shadow: true sun look different: the large dark areas on ground planes shrink to the casters' footprints. Check scenes that were tuned around the old darkening.
  • createGame from @aura3d/engine/game now has the game lane on by default; the root createGame stays a headless stub and logs CREATE_GAME_STUB. Opt out with -game, none, ?a3d-qr=-game or A3D_QR_GAME=0.
  • Fix: handle.setUniform on a model node no longer throws. Without a custom material it is a once-per-uniform MODEL_SETUNIFORM_NOOP warning; with material.custom it writes as before.
  • Fix: scene({ children }) no longer returns an empty scene. scene(init) accepts children, background, camera, timeline and diagnostics; any other key or a non-object throws invalid-scene-options.
  • A multi-geometry batched.mesh without qualityRebuild.flags enabling tiers.batching now falls back to per-geometry instancing (fallbackReason: "tiers-batching-off") instead of drawing nothing. Base-colour maps on instanced nodes need colorSpace: "srgb".
  • material.custom colour output is linear light that the output stage encodes (and the default ACES operator tones down); use #include <colorspace_decode> with toneMapping: "none" to get the authored colours. The default is unchanged.

CLI

  • New: CLI asset tools (docs/api/cli-asset-tools.md). aura3d assets optimize --file in.glb --out out.glb --role hero|vehicle|character|prop|background (role vertex and texture budgets, float32 geometry kept), aura3d assets merge-clips --base rig --out file.glb name=clip ... (merge Meshy animate clips into one rig by node name; a foreign skeleton is rejected), aura3d assets import-meshy <dir> --into <assets-dir> --prefix <game> --name <role> (isolated admission in a temporary project; it is the default inside the Aura3D monorepo, so shared aura.assets.json, src/aura-assets.ts, public/aura-assets/* and aura-evidence/* are never written; --shared-manifest opts out), and aura3d meshy ledger status|reserve|settle|reconcile|list (per-game and global credit caps under a lock; a rate-limited or timed-out call stays unconfirmed and keeps counting instead of being recorded as a failure). The meshy-cli and aura3d-assets skills point to the new commands.

Deprecations

  • The ten deprecated subpaths @aura3d/engine/{contracts, media-node, apps, rendering, rendering/production-runtime, rendering/webgpu, production-runtime, advanced-runtime, assets/browser, workflows/production} and the deprecated root symbols in packages/engine/src/public/index.ts keep working in 4.1.0. Their announced removal moves from 4.1.0 to 5.0.0: the warning text, removeIn in aura.exports.json and the @deprecated notes now say 5.0.0. Replacements are listed in docs/migration/4.0-to-4.1.md.

Tooling and examples (not published packages)

  • Aura Slam 3D native port (examples/aura-slam/, wrapper apps/showcase-aura-slam/, not routed): a typed, differential-tested simulation (physics, strokes, rules, AI, swipe, input), pose and rig, renderer, audio and UI shell. The live route is not swapped: Auto mode does not yet match the original's autoplay, the live-game replay gate and matched-tick render parity are not met, desktop draw calls (about 261) are above the 150 budget, and the tour, tournament and online screens are deferred.
  • Meshy art pass over the eleven Crayon ports (about 4.2k credits at last count by the helper ledger, which undercounts rate-limited calls) and the look-spec extractor under tools/crayon-capture/.
  • Apex Overdrive (showcase-roadster-rush and -v2) runs on a native Aura3D simulation; the 1.1 MB third-party engine.js bundle is gone.
  • The 15 index games dropped their blanket post, compiler and fog pins and share a black-frame self-heal; every port page supports ?diag=1. Strict live-port smoke specs, a hi-dpi spec and the port page generator were added.
  • Docs: docs/whats-new-4.1.md, docs/migration/4.0-to-4.1.md, docs/workflows/game-conversion-playbook.md, docs/aura-slam/ARCHITECTURE.md, and the topic pages under docs/api/.

Verified vs not verified

  • Verified: unit and differential tests (see each kit's tests), typecheck, build, public-API contract, surface diff, export and import checks, and remote runs on software WebGL2 (SwiftShader) in headless and headed Chromium.
  • Verified on real hardware for some ports only: an Apple M4 Max (ANGLE/Metal, WebGL2) was used by the owner for Drift & Deliver, Silt & Signal and Last Train via ?diag=1. The NaN fix for the black Drift scene and the new Silt rings are not yet confirmed there.
  • Not verified: any hardware-GPU run of the full port set (blocked on cloud GPU quota), real-headset WebXR, and performance on weak GPUs (several ports use 130 to 1,400 draw calls).
  • The Crayon ports are not indistinguishable from the originals. Under the blind model-judge protocol (a model, not a human; seed 20261023) the judge still separated every port from its original (original identified in 35 of 48 trials for HEATSINK, the best result, 39 of 48 for Roadster Hours and 47 or 48 of 48 for most others; for Splat Rush the result reversed to 8 of 48 because the original's cards overflow at the test size); structural metrics improved but Phase B of docs/workflows/threejs-parity-gaps.md is unmet.