Skip to content

Particle effects, physics impacts & VR/collider polish - #53

Merged
AlexZ005 merged 8 commits into
mainfrom
feature/particles-pfx-a
Jul 26, 2026
Merged

Particle effects, physics impacts & VR/collider polish#53
AlexZ005 merged 8 commits into
mainfrom
feature/particles-pfx-a

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Particle effects, physics impacts & VR/collider polish

A self-contained feature arc (8 commits, one per phase) adding a full particle system, physics-driven impact effects, object throwing, and the physics/VR/Inspector polish that fell out of play-testing it. Every mutation replicates; nothing changes OSS-vs-cloud behavior.

Particles (PFX-A/B/C)

  • Deterministic analytic engine — each particle is a closed-form function of a per-particle hash, its config, and the synced clock, evaluated in the vertex shader. No per-frame CPU sim, no netcode for continuous emission; one THREE.Points per emitter at the scene root (never in objectsGroup/GLTF sync).
  • Core 6 presets (Sparkles, Fire, Smoke, Dust puff, Confetti, Sparks) attachable from the object context menu (Effects ▸), the Inspector Particles section, or as standalone Add ▸ Effects markers. Config rides userData.particles (replicates like userData.physics).
  • Particles flow node (Effects group) with wired inputs (count, speed, gravity, size, offset, color, trigger); targets via Object Selector or the implicit-owner rule.
  • Ground-impact events — new rapier collision-event plumbing (EventQueue + ActiveEvents, per-substep drain, pre-step downward-velocity filter + per-body cooldown). Emitters set to "On impact", or the new On Impact trigger node, fire when an object lands — replicated via the shared-timestamp particleburst/nodetrigger paths.
  • Emission modes continuous / burst / on-impact; burst presets auto-fire on attach and re-fire on edit (so moving the emission offset is visible); additive presets glow through the emitting mesh.

Physics & throwing

  • Throw dynamic bodies mid-sim — desktop gizmo drag and VR grip-grab both release with the estimated hand velocity (mass = momentum).
  • Oriented colliders — collider size from the object's local AABB carrying its rotation, so a rotated ramp/box collides as itself (was a fat world-AABB block). New collider shapes: Box / Sphere / Capsule / Cylinder / Convex hull.
  • Fun by default — standard primitives spawn Dynamic (mass 1), so a fresh cube falls, stacks and throws immediately (Terrain + module/effect objects stay scenery).

VR & Inspector

  • VR physics/animation freeze fixedwindow.rAF is suspended in immersive XR, which froze the flow tick (and its physics post-tick). Scene now pumps the tick from threlte's XR-aware loop while presenting.
  • Radial menu Scene ring — new Physics On/Off toggle (the P key was desktop-only) and the environment presets nested under Environment ▸.
  • Inspector — collapsible sections (persisted) + a property search box, so the growing panel stays navigable.

Verification

Committed e2e suites: particles (17), flow-particle (8), particles-impact (8), physics-colliders (8), plus inspector/vr-radial-menu updated. npm run build green; npx svelte-check holds the 485/72 baseline (no new errors). Two-peer suite halves are exercised via send-spy asserts (the local hosts mapping is off). On-device VR feel was play-tested by the author.

Public docs updated in theprototype-docs (particle effects page, On Impact/Particles node pages, physics collider shapes + throwing).

🤖 Generated with Claude Code

AlexZ005 and others added 8 commits July 25, 2026 22:14
… (PFX-A)

- particleShader/particleRuntime: deterministic STATELESS particles - every
  particle is a closed-form function of (per-particle hash, config, synced
  clock) evaluated in the vertex shader; no integration, no netcode for
  continuous emission; hour-wrapped clock for float precision; one
  THREE.Points per emitter under scene-root particle-root (never in
  sceneObjects/GLTF sync); soundRuntime-style Map-diff lifecycle; procedural
  sprite textures (dot/streak/puff/star/square); local vs world sim space
  (world = per-rebirth aOrigin stamping, trails behind moving objects)
- particlePresets: Core 6 (Sparkles, Fire, Smoke, Dust puff, Confetti,
  Sparks) over one schema; particleActions = replicating mutators
  (userData.particles via objectParameters, props undo entries)
- burst emitters fire via replicated particleburst {uuid,t} shared-timestamp
  message (keypress/nodetrigger precedent) - PFX-C wires ground impacts to it
- UI: Inspector Particles section (preset/emission/motion/look/space +
  Burst now), object context menu Effects submenu (set-aware), viewport
  Add > Effects standalone marker spheres
- flowRuntime tick hands particle node pairs + userData sweep to
  updateParticles (particle NODE ships in PFX-B); caps 8 emitters x 500
  particles (VR drawRange 200); Points hidden in wireframe view mode
  (overrideMaterial would clobber the shader)
- e2e particles.test.cjs: 15 checks (attach/replicate/burst/undo/cap/menus,
  no-GLTF-leak, shader-error probe) - ALL PASS; build green; svelte-check
  holds the 485/72 baseline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dust puff / Confetti / Sparks are burst-mode: they idle until a trigger, so
attaching one showed nothing (looked broken). The runtime now auto-fires a
burst ONCE when it builds a burst-mode emitter entry - immediate LOCAL
feedback on attach / count change (each peer fires when it builds its own
entry; tightly-synced bursts still ride the replicated particleburst
timestamp from Burst now / PFX-C impacts). Rendering was already correct
(verified via screenshot - fire embers + confetti squares both draw).

- particles.test.cjs: +auto-fire-on-attach check (16 total, all pass)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire particle emitters in the node editor: a `particle` node that targets the
connected object (Object Selector) or the graph owner (H1 implicit rule),
rendered by the same deterministic particleRuntime as the Inspector emitters
(keyed by node id).

- nodeCatalog: particle node in the Effects group, seeded from a preset,
  params for the key fields (NOT in animationTypes - a keyed runtime like sound)
- flowSockets: INPUT particle {count:number, color:color, trigger:event};
  output is the effect channel automatically
- ParticleNode.svelte: card with preset picker (seeds full config), emission/
  motion/look controls, and count/color/trigger input sockets
- particleRuntime: a wired `color` tints both gradient stops; a wired event
  fires a burst-mode emitter on its RISING edge (once per pulse; rides the
  replicated nodetrigger stamp so peers stay ~in sync)
- NODES.md audit row
- e2e flow-particle.test.cjs (5 checks: catalog/sockets, node-keyed emitter,
  wired count rebuild, event->burst, implicit-owner) - ALL PASS. build green;
  svelte-check holds 485/72. Docs updated in theprototype-docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ission offset

Fixes from user testing of PFX-B:

- Continuous emitters (Sparkles/Fire/Smoke) looked invisible: they spawn from
  the object CENTER and were occluded inside the solid mesh. Additive (glow)
  presets now render with depthTest OFF so they glow through the object (the
  standard VFX trick); normal-blend presets (smoke/confetti/dust) keep proper
  occlusion.
- On Click wired THROUGH the particle node did nothing on object click:
  fireObjectClick only matched On Click to Object Selector DIRECT wiring. It now
  walks outgoing edges (BFS), so On Click, Particles, Object Selector fires on
  the target object's click. (Key Press already worked.)
- NEW emission offset: offset (vector3, object-local) sets WHERE particles
  spawn - lift them to the top or corner instead of the center. Exposed as an
  Inspector "Emit from" XYZ row and a node Vector3 input.
- More node inputs: offset, speed, gravity, size (alias of size-start) alongside
  count, color, trigger.

particleEntries() debug view gains offset + depthTest. flow-particle.test.cjs
extended (offset uniform, onclick-through-node BFS, additive/normal depthTest) -
8/8; particles 16/16. build green; svelte-check 485/72. Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups from user testing:

- ParticleNode sockets drifted (the trigger handle landed next to the preset
  row) because they were absolutely positioned at guessed pixel offsets. Each
  wired input now lives in its own row with the Socket anchored to that row
  (top:50% inside a padding-cancelled relative wrapper - the ObjectFlowNode
  pattern), so handles always line up with their label. preset/emission/sprite/
  space are plain params with NO socket.
- Editing a burst emitter (Dust puff / Confetti / Sparks) now re-fires it on any
  look change - moving the emission offset especially - so you see the effect at
  the new origin immediately, no manual trigger. Count changes already rebuilt +
  auto-fired; this covers offset/speed/gravity/size/sprite/color via a config
  signature.

particles.test.cjs +offset-re-fire check (17); flow-particle 8/8. build green;
svelte-check 485/72.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PFX-C)

Rapier collision events (none existed): the stepping peer passes an EventQueue
to world.step, dynamic colliders carry ActiveEvents.COLLISION_EVENTS, and the
queue drains PER SUBSTEP (events from early substeps would merge/get lost).
Collider handles map to body entries (they were discarded); the ground cuboid's
handle is captured. A contact counts as an impact when the body's PRE-step
downward velocity >= 1.2 m/s (contact resolution rewrites linvel during the
step - a bounce reads as upward after) with a 300ms per-body cooldown, so
rolls and settling can't machine-gun.

Impacts are INITIATOR-detected and replicate over the existing shared-stamp
paths (golden rule 8 kept):
- On Impact node (Triggers, event output, wirable minStrength gate): pulses
  via applyNodeTrigger/nodetrigger; targeting reuses the fireObjectClick BFS
  (works wired THROUGH a Particles node) + the implicit-owner rule.
- Emitters with emission "On impact" (new Inspector + node option, mode
  'impact' = a triggered mode like burst): burstObjectParticles ->
  replicated particleburst. Node-based impact emitters detected through
  particleRuntime.hasImpactEmitter.

THROW (user request): VR grip-grabs of dynamic bodies mid-sim now ride the
SAME holdBody/releaseBody contract as the desktop gizmo - release imparts the
estimated hand velocity (mass makes momentum), via the multiTransform primed
dynamic import pattern (no static physics edge into vrControls). Desktop
already threw; on-device VR feel is the user's manual check.

e2e particles-impact.test.cjs (8 checks incl. real rapier fall -> burst +
nodetrigger + cooldown + clean stop); particles 17/17 + flow-particle 8/8
regression-green. build green; svelte-check 485/72. NODES.md row + docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rework

- PHYSICS (and every flow animation, particle sweep, sound update) FROZE the
  moment a headset went on: flowRuntime's loop rode window.requestAnimationFrame,
  which browsers SUSPEND during an immersive WebXR session (only
  session.requestAnimationFrame is serviced) - and the physics step is that
  loop's postTick. The tick body is now split from its scheduler and
  Scene.svelte PUMPS it from threlte's task loop (setAnimationLoop, XR-aware)
  while renderer.xr.isPresenting; a 3ms timestamp guard makes a double delivery
  a no-op on browsers that keep window rAF alive.
- Radial menu Scene ring reworked: the environment presets (Daylight/Night/...)
  moved into a nested "Environment >" sub-ring (they crowded the ring), and a
  new "Physics: On/Off" toggle starts/stops the scene simulation from VR (the P
  key is desktop-only; greyed out while a remote peer runs the sim). VRMenu
  re-derives sectors on simulating/remoteSimulating so the label tracks live.
- vr-radial-menu.test.cjs updated for the new ring layout + a Scene-ring
  assertion (28 checks, all pass). flow-runtime single-page 8/8; particles
  17/17 + flow-particle 8/8 + particles-impact 8/8. flow-runtime's two-peer
  half is env-gated here (hosts mapping off) - connection code untouched.
  build green; svelte-check 485/72. On-headset feel = user's manual check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es, inspector search

Follow-ups from VR play-testing:

- ORIENTED colliders: the collider size now comes from the object's LOCAL AABB
  (rotation stripped for the measure, restored after) and the collider carries
  the object's rotation RELATIVE to the identity-start body (joints keep their
  C3 world-aligned requirement). A rotated wall/ramp/box collides as itself -
  the old world-AABB capture inflated any rotated object into a fat
  axis-aligned block, which is why VR-rotated objects had wrong collision.
- Collider SHAPES: Sphere / Capsule / Cylinder join Box + Convex hull
  (shapeDesc fits the local extents; capsule/cylinder stand along local Y,
  sphere takes the largest extent). Inspector Physics gains the options -
  userData.physics.collider replicates as before.
- FUN BY DEFAULT: standard primitives (/create Box/Sphere/Stairs/... -
  explicit FUN_PRIMITIVES allow-list) spawn with userData.physics
  {mode:'dynamic', mass:1}, so a fresh cube falls, stacks and THROWS the
  moment a sim runs - no Inspector trip first. Deterministic (receivers run
  the same builder); Terrain + module primitives stay scenery; the
  Add > Effects marker sphere explicitly clears the stamp (replicated).
- Inspector UX: every Section is now COLLAPSIBLE (chevron, persisted per
  label) and a property SEARCH box sits under the drawer header (scene +
  object) - typing filters sections by their RENDERED text (labels, values,
  hints), with matches force-expanded; Esc clears. inspectorFilter store +
  Section.svelte rework (label in its own span - getByText exact-match).

e2e physics-colliders.test.cjs (8: dynamic-default, Terrain excluded, ball
rolls down a 30-degree ROTATED ramp - impossible with the old AABB, search
filters/restores, collapse persists/reopens); inspector 17/17, particles
17/17, flow-particle 8/8, particles-impact 8/8 green. physics-joints'
two-peer half env-gated (known). build green; svelte-check 485/72. Docs
updated (physics.md collider shapes + ready-to-play tip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit 54f5ac3 into main Jul 26, 2026
@AlexZ005
AlexZ005 deleted the feature/particles-pfx-a branch July 26, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant