Overview
The Hyperscape particle system uses GPU instancing and TSL (Three.js Shading Language) shaders to render thousands of particles with minimal CPU overhead. Introduced in PR #877 (February 2026), this system provides a 97% reduction in draw calls and an 80% FPS improvement for fishing spots.Architecture
The particle system follows a centralized routing pattern with specialized sub-managers:ParticleManager
Central router that dispatches particle events to specialized sub-managers based on particle type. Location:packages/shared/src/entities/managers/particleManager/ParticleManager.ts
Key Features:
- Single entry point for all particle systems
- Type-based routing to appropriate sub-manager
- Ownership tracking eliminates need for type hints on unregister/move
- Extensible design for future particle types (dust, smoke, blood, magic)
WaterParticleManager
GPU-instanced water particle effects for fishing spots using InstancedMesh and TSL shaders. Location:packages/shared/src/entities/managers/particleManager/WaterParticleManager.ts
Performance Metrics
Technical Implementation
4 InstancedMeshes:- Splash: Water droplets with parabolic arc trajectories (MAX_SPLASH=96)
- Bubble: Rising bubbles with lateral wobble (MAX_BUBBLE=72)
- Shimmer: Surface sparkle with twinkle effect (MAX_SHIMMER=72)
- Ripple: Expanding rings with fade (MAX_RIPPLE=24)
Vertex Buffer Budget:
- 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)
Particle Animations
All animations computed on GPU via TSL shaders:Splash Particles
Parabolic arc trajectories with pop-in fade:- Lifetime: 0.6-1.2 seconds
- Peak height: 0.12-0.2 units (burst: 0.25-0.6)
- Radial distance: 0.05-0.3 units
- Fade-in rate: 12x (snappy)
- Fade-out: Power 1.2 (smooth)
Bubble Particles
Rising with lateral wobble:- Lifetime: 1.2-2.5 seconds
- Rise height: 0.3-0.55 units
- Wobble frequency: direction × 4.0
- Drift pattern: sin(angle + t × wobbleFreq) × radius
- Size: 0.09 units
Shimmer Particles
Surface sparkle with twinkle effect:- Lifetime: 1.5-3.0 seconds
- Wander radius: 0.15-0.6 units
- Twinkle frequencies: 8 Hz and 13 Hz
- Circular wander pattern on water surface
- Size: 0.055 units
Ripple Rings
Expanding rings with fade:- Scale range: 0.15 → 1.45 (expansion)
- Early fade (0-15%): Linear ramp to 0.55 opacity
- Late fade (15-100%): Power 1.5 decay from 0.55 to 0
- Continuous expansion driven by time uniform
- Phase offset per ripple for staggered animation
Fishing Spot Variants
Three fishing spot variants with different visual characteristics:
Variant Detection:
Burst System
Fish activity bursts fire 2-4 splash particles simultaneously to simulate fish jumping: Burst Mechanics:- Burst center randomized within 0.05-0.15 radius
- Burst particles cluster around center with 0.06 spread
- Burst particles have higher peak heights (0.25-0.6 vs 0.12-0.32)
- Burst timer resets to random interval after each burst
- Only fires when existing splash particles are past 60% lifetime
Integration with ResourceSystem
The ParticleManager is created and managed by ResourceSystem on the client:Lazy Registration Pattern
ResourceEntity uses a lazy registration pattern to handle timing issues where entities may be created before ResourceSystem starts:- Entities may be created before ResourceSystem.start() creates the ParticleManager
- Timing/lifecycle issue where entity init runs before system initialization
- Lazy registration retries on every frame until successful
- Ensures all fishing spots eventually register with the particle manager
Usage Example
Extensibility Guide
To add new particle types (fire, magic, dust, blood):1
Create Sub-Manager Class
Create new file in
packages/shared/src/entities/managers/particleManager/2
Instantiate in ParticleManager
Add to constructor
3
Add Routing Logic
Update register/unregister/move/handleEvent methods
4
Call update() and dispose()
Add to ParticleManager lifecycle methods
TSL Shader Implementation
The particle system uses Three.js Shading Language (TSL) for GPU-computed animations:Billboard Orientation
Particles always face the camera using right/up vectors:Parabolic Arc (Splash)
Wobble Pattern (Bubble)
Twinkle Effect (Shimmer)
Ring Expansion (Ripple)
Performance Considerations
Memory Usage
Per Fishing Spot:- 4 InstancedMeshes (shared across all spots)
- Per-spot data: ~20 particle slots × 11 floats = ~220 floats = ~880 bytes
- Texture cache: 2 DataTextures (64×64 RGBA) = 32KB (shared)
- Particle data: ~8.8KB
- Shared textures: 32KB
- Shared geometries: ~2KB
- Total: ~43KB (vs ~500KB+ for individual meshes)
Draw Call Budget
Before (per fishing spot):- 5-8 splash particles × 1 draw call = 5-8
- 4-5 bubble particles × 1 draw call = 4-5
- 4-5 shimmer particles × 1 draw call = 4-5
- 2 ripple rings × 1 draw call = 2
- Total: ~15-20 draw calls per spot
- 10 spots: ~150-200 draw calls
- 1 splash InstancedMesh = 1 draw call
- 1 bubble InstancedMesh = 1 draw call
- 1 shimmer InstancedMesh = 1 draw call
- 1 ripple InstancedMesh = 1 draw call
- Total: 4 draw calls for all spots
CPU Overhead
Before:- Per-frame updates for ~150 individual meshes
- Trigonometry (sin, cos) for each particle
- Quaternion copies for billboard orientation
- Opacity writes for fade animations
- ~450 lines of CPU animation code
- Zero CPU overhead for particle animation
- All math computed on GPU via TSL shaders
- Only camera right/up vectors updated per frame (2 vec3 copies)
- Age increments in typed arrays (simple addition)
Best Practices
When to Use GPU Instancing
Use GPU instancing for particle systems when:- You have many similar particles (10+)
- Particles share the same geometry and material
- Animation can be expressed in shader code
- Per-particle data fits in InstancedBufferAttributes
When to Use Individual Meshes
Use individual meshes when:- Particles have unique geometries
- Complex CPU-driven behavior (physics, AI)
- Per-particle data exceeds attribute budget
- Very few particles (< 10)
Attribute Budget Management
WebGL/WebGPU has a limit of 8 vertex attributes per mesh. Plan your attribute usage: Current Usage (Particle Layers):- position (built-in)
- uv (built-in)
- instanceMatrix (built-in for InstancedMesh)
- spotPos (custom)
- ageLifetime (custom)
- angleRadius (custom)
- dynamics (custom)
- 1 slot remaining
- position (built-in)
- uv (built-in)
- instanceMatrix (built-in for InstancedMesh)
- spotPos (custom)
- rippleParams (custom)
- 3 slots remaining
See Also
- ECS Architecture - Entity Component System overview
- ResourceSystem - Resource entity management
- Three.js TSL - Three.js Shading Language