ParticleManager Architecture
GPU-Instanced Particle System for HyperscapeOverview
The ParticleManager is a centralized GPU-instanced particle rendering system introduced in PR #877 (commit 4168f2f). It replaces per-entity CPU particle animation with a unified architecture that dramatically reduces draw calls and improves performance.Performance Impact
Before ParticleManager:- Each fishing spot created 10-21 individual
THREE.Meshobjects - ~150 draw calls for particle rendering
- ~450 lines of per-frame CPU animation code (trig, quaternion copies, opacity writes)
- FPS: 65-70 on reference hardware
- 4 GPU InstancedMeshes for all fishing spots
- 4 draw calls total (97% reduction)
- All animation computed on GPU via TSL shaders
- FPS: 120 on reference hardware
Architecture
Core Components
1. ParticleManager
Location:packages/shared/src/entities/managers/particleManager/ParticleManager.ts
Purpose: Central entry point and router for all particle systems.
Key Features:
- Discriminated union config (
ParticleConfig) for type-safe registration - Ownership map tracks which sub-manager owns each emitter
- No type hints required for
unregister()ormove()- ownership map resolves automatically - Extensible architecture for adding new particle types
2. WaterParticleManager
Location:packages/shared/src/entities/managers/particleManager/WaterParticleManager.ts
Purpose: GPU-instanced rendering for fishing spot water effects.
Layers:
-
Splash Layer (MAX_SPLASH = 96 instances)
- Parabolic arc animation:
y = peakHeight * 4 * t * (1-t) - Radial distribution from spot center
- Fast fade-in, smooth fade-out
- Burst system: periodic fish activity clusters
- Parabolic arc animation:
-
Bubble Layer (MAX_BUBBLE = 72 instances)
- Gentle rise from below water surface
- Lateral wobble with frequency modulation
- Longer lifetime than splash (1.2-2.5s)
-
Shimmer Layer (MAX_SHIMMER = 72 instances)
- Surface sparkle on water plane
- Fast twinkle using global time + per-particle phase
- Circular wander pattern
-
Ripple Layer (MAX_RIPPLE = 24 instances)
- Expanding ring geometry (CircleGeometry)
- Phase-based scale and opacity animation
- Ring texture with Gaussian falloff
spotPos(vec3) - fishing spot world centerageLifetime(vec2) - current age (x), total lifetime (y)angleRadius(vec2) - polar angle (x), radial distance (y)dynamics(vec4) - peakHeight (x), size (y), speed (z), direction (w)
spotPos(vec3) - fishing spot world centerrippleParams(vec2) - phase offset (x), ripple speed (y)
- Particle layers: 7 of 8 max attributes
- position(1) + uv(1) + instanceMatrix(1) + spotPos(1) + ageLifetime(1) + angleRadius(1) + dynamics(1)
- Ripple layer: 5 of 8 max attributes
- position(1) + uv(1) + instanceMatrix(1) + spotPos(1) + rippleParams(1)
- Billboard orientation computed on GPU using camera right/up vectors
- Parabolic arcs for splash particles
- Wobble/drift for bubbles
- Twinkle animation for shimmer
- Ring expansion and fade for ripples
- All opacity/fade curves computed in shader
resourceId string matching:
| Variant | Ripples | Splash | Bubble | Shimmer | Burst Interval | Activity |
|---|---|---|---|---|---|---|
Net (resourceId.includes("net")) | 2 | 4 | 3 | 3 | 5-10s | Calm/gentle |
| Bait (default) | 2 | 5 | 4 | 4 | 3-7s | Medium |
Fly (resourceId.includes("fly")) | 2 | 8 | 5 | 5 | 2-5s | Active |
- Periodic fish activity creates simultaneous splash clusters
- Timer countdown per fishing spot
- Fires 2-4 splash particles from a random point near spot center
- Adds visual variety and liveliness
3. GlowParticleManager
Location:packages/shared/src/entities/managers/particleManager/GlowParticleManager.ts
Purpose: GPU-instanced glow billboards for altars, fires, and other glowing effects.
Features:
- Additive blending for glow effect
- Preset-based configuration (altar, fire, etc.)
- Color override support (single hex or three-tone palette)
- Geometry-aware spark placement for altar preset
- Model scale and Y-offset support
4. ResourceSystem Integration
Location:packages/shared/src/systems/shared/entities/ResourceSystem.ts
Changes:
- Creates
ParticleManageron client startup - Retroactively registers any fishing spot entities created before system started
- Listens for
RESOURCE_SPAWNEDevents and routes to particle manager - Calls
particleManager.update(dt, camera)per frame - Disposes particle manager on system stop
5. ResourceEntity Delegation
Location:packages/shared/src/entities/world/ResourceEntity.ts
Changes:
- Removed per-entity particle meshes and animation state
- Removed ripple ring meshes
- Retained only lightweight glow mesh for interaction detection
- Delegates to ParticleManager via
tryRegisterWithParticleManager() - Lazy registration pattern handles timing/lifecycle edge cases
Adding New Particle Types
To add a new particle type (e.g., fire, magic, dust):-
Create Sub-Manager Class
- Location:
packages/shared/src/entities/managers/particleManager/ - Example:
FireParticleManager.ts - Implement:
registerSpot(),unregisterSpot(),moveSpot(),update(),dispose()
- Location:
-
Update ParticleManager
-
Add Config Type
-
Export from Index
Technical Details
TSL Shader Node System
The particle system uses Three.js Shading Language (TSL) for GPU-computed animation:Texture Generation
Glow Texture:- Procedurally generated DataTexture with Gaussian falloff
- Configurable sharpness parameter
- Cached by generation parameters to avoid duplicates
- Gaussian ring pattern with transparent center
- Configurable ring radius and width
- Soft edge fade for natural look
Memory Management
Slot Allocation:- Free slot stacks (LIFO) for efficient allocation/deallocation
- Slots recycled when particles respawn
- No dynamic allocation during runtime
- Dirty flags track which InstancedBufferAttributes need GPU upload
- Only modified attributes are marked
needsUpdate = true - Minimizes GPU bandwidth usage
Integration Points
ResourceSystem
Startup:ResourceEntity
Registration:Performance Characteristics
Draw Calls:- Before: ~150 draw calls (10-21 meshes per fishing spot × ~10 spots)
- After: 4 draw calls (1 per particle layer, shared across all spots)
- Reduction: 97%
- Before: Per-frame trig, quaternion copies, opacity writes for each particle
- After: Only age increment and dirty flag tracking
- GPU handles all position/rotation/opacity computation
- Before: Individual mesh + material + geometry per particle
- After: Shared geometry + material, per-instance attribute arrays
- Reduction: ~80% memory footprint
- Before: 65-70 FPS with 10 fishing spots
- After: 120 FPS with 10 fishing spots
- Improvement: 75% FPS increase
Future Enhancements
Planned Particle Types:- Fire particles (campfires, torches, explosions)
- Magic particles (spell effects, enchantments)
- Dust particles (footsteps, mining, woodcutting)
- Weather particles (rain, snow, fog)
- Combat particles (blood splatter, impact effects)
- Frustum culling for particle layers
- LOD system for distant particle spots (reduce instance count)
- Particle pooling across multiple managers
- Compute shader for particle updates (WebGPU)
References
- PR #877: GPU-instanced fishing spot particles via centralized ParticleManager
- Commit 4168f2f: Main implementation
- Files Changed: 6 files, +1161 lines, -597 lines
- Performance Video: See PR description for before/after comparison
Related Documentation
- CLAUDE.md - Development guidelines and architecture overview
- ResourceSystem - Resource spawning and management
- ResourceEntity - Resource entity implementation