Skip to main content

ParticleManager Architecture

GPU-Instanced Particle System for Hyperscape

Overview

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.Mesh objects
  • ~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
After ParticleManager:
  • 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() or move() - ownership map resolves automatically
  • Extensible architecture for adding new particle types
API:
Config Types:

2. WaterParticleManager

Location: packages/shared/src/entities/managers/particleManager/WaterParticleManager.ts Purpose: GPU-instanced rendering for fishing spot water effects. Layers:
  1. 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
  2. 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)
  3. Shimmer Layer (MAX_SHIMMER = 72 instances)
    • Surface sparkle on water plane
    • Fast twinkle using global time + per-particle phase
    • Circular wander pattern
  4. Ripple Layer (MAX_RIPPLE = 24 instances)
    • Expanding ring geometry (CircleGeometry)
    • Phase-based scale and opacity animation
    • Ring texture with Gaussian falloff
Per-Instance Data (InstancedBufferAttributes): Particle layers (splash, bubble, shimmer):
  • spotPos (vec3) - fishing spot world center
  • ageLifetime (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)
Ripple layer:
  • spotPos (vec3) - fishing spot world center
  • rippleParams (vec2) - phase offset (x), ripple speed (y)
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)
TSL Shader Features:
  • 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
Fishing Spot Variants: Based on resourceId string matching: Burst System:
  • 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 ParticleManager on client startup
  • Retroactively registers any fishing spot entities created before system started
  • Listens for RESOURCE_SPAWNED events and routes to particle manager
  • Calls particleManager.update(dt, camera) per frame
  • Disposes particle manager on system stop
Code:

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
Lifecycle:

Adding New Particle Types

To add a new particle type (e.g., fire, magic, dust):
  1. Create Sub-Manager Class
    • Location: packages/shared/src/entities/managers/particleManager/
    • Example: FireParticleManager.ts
    • Implement: registerSpot(), unregisterSpot(), moveSpot(), update(), dispose()
  2. Update ParticleManager
  3. Add Config Type
  4. 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
Ring Texture:
  • 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
Update Flags:
  • Dirty flags track which InstancedBufferAttributes need GPU upload
  • Only modified attributes are marked needsUpdate = true
  • Minimizes GPU bandwidth usage
Example:

Integration Points

ResourceSystem

Startup:
Event Routing:
Per-Frame Update:

ResourceEntity

Registration:
Lazy Registration:
Cleanup:

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%
CPU Usage:
  • 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
Memory:
  • Before: Individual mesh + material + geometry per particle
  • After: Shared geometry + material, per-instance attribute arrays
  • Reduction: ~80% memory footprint
FPS:
  • 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)
Optimization Opportunities:
  • 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