Overview
Hyperscape uses GPU instancing to render thousands of resource entities (rocks, ores, herbs, trees) with minimal draw calls. This system was introduced in PR #946 (February 2026) and provides dramatic performance improvements. Performance Impact:- Reduces draw calls from O(n) per resource to O(1) per unique model per LOD level
- Distance-based LOD switching with hysteresis to prevent flickering
- Supports depleted models (stumps, empty rocks) with separate instance pools
- Highlight mesh support for hover/selection effects
Architecture
Instancer Systems
Hyperscape provides two specialized instancers:| Instancer | Purpose | Location |
|---|---|---|
| GLBResourceInstancer | Rocks, ores, herbs (non-tree resources) | systems/shared/world/GLBResourceInstancer.ts |
| GLBTreeInstancer | Tree resources with dissolve materials | systems/shared/world/GLBTreeInstancer.ts |
- Load each model once, extract geometry by reference
- Render all instances via single
THREE.InstancedMeshper LOD level - Distance-based LOD switching per-instance every frame
- Matrix swap-and-pop for efficient instance removal
Visual Strategy Pattern
Resources use the Strategy Pattern to delegate rendering:- TreeGLBVisualStrategy - GLB tree models via GLBTreeInstancer
- InstancedModelVisualStrategy - Rocks, ores, herbs via GLBResourceInstancer
- StandardModelVisualStrategy - Fallback for non-instanced rendering
- FishingSpotVisualStrategy - Fishing spot particles
- PlaceholderVisualStrategy - Colored cubes for missing models
ResourceVisualStrategy API
Interface
BREAKING CHANGE: onDepleted Return Type
Before (pre-PR #946):onDepleted() signature:
New Method: getHighlightMesh
Purpose: Provide a positioned mesh for outline rendering on instanced entities. Signature:EntityHighlightService calls this method to get a temporary mesh for outline rendering:
GLBResourceInstancer
Overview
Manages instanced rendering for non-tree resources (rocks, ores, herbs). Location:packages/shared/src/systems/shared/world/GLBResourceInstancer.ts
Features:
- Pools instances by model path
- Separate
InstancedMeshper LOD level (LOD0, LOD1, LOD2) - Depleted model pools for stumps/empty rocks
- Highlight mesh support for hover effects
- Max 512 instances per pool
API
LOD System
The instancer automatically switches LOD levels based on camera distance:- LOD0:
model.glb(original file) - LOD1:
model_lod1.glb(inferred) - LOD2:
model_lod2.glb(inferred)
Depleted Models
Resources can specify depleted models in their configuration:- Resource spawns → added to normal pool at LOD0
- Resource depleted → removed from normal pool, added to depleted pool
- Resource respawns → removed from depleted pool, added back to normal pool
- No individual model loading for depleted states
- Instant visual transition (matrix swap)
- Collision proxy persists across transitions
- Separate Y-offset calculation for depleted models
InstancedModelVisualStrategy
Overview
Thin wrapper that integratesGLBResourceInstancer with the ResourceEntity lifecycle.
Location: packages/shared/src/entities/world/visuals/InstancedModelVisualStrategy.ts
Implementation
Collision Proxy
SinceInstancedMesh is not raycastable, the strategy creates an invisible collision proxy:
- Invisible mesh (
material.visible = false) - Proper userData for interaction detection
- Layer 1 for raycasting
- Persists across depletion/respawn transitions
Highlight System
EntityHighlightService Integration
The highlight service supports instanced entities via thegetHighlightRoot() method:
- User hovers over instanced entity
EntityHighlightService.setHoverTarget()called- Service calls
entity.getHighlightRoot() - Strategy returns positioned highlight mesh
- Service adds mesh to scene temporarily
- Outline pass renders highlight mesh
- Service removes mesh when hover ends
Highlight Mesh Lifecycle
Depleted Model System
Configuration
Resources specify depleted models in their manifest:Instancer Implementation
The instancer maintains separate pools for normal and depleted states:- Resource depleted →
setDepleted(entityId, true)called - Remove instance from current LOD pool (LOD0/LOD1/LOD2)
- Add instance to depleted pool with
depletedScale - Update collision proxy userData (
depleted: true, interactable: false) - Remove old highlight mesh if entity is currently hovered
- Resource respawns →
setDepleted(entityId, false)called - Remove instance from depleted pool
- Add instance back to LOD0 pool with normal scale
- Update collision proxy userData (
depleted: false, interactable: true)
Y-Offset Calculation
Each model pool calculates Y-offsets to ensure models sit flush on terrain:yOffset- Normal model offsetdepletedYOffset- Depleted model offset (stumps may have different proportions)
Performance Characteristics
Draw Call Reduction
Before Instancing:- 1000 oak trees = 1000 draw calls (one per tree)
- 500 copper rocks = 500 draw calls
- Total: 1500 draw calls for resources
- 1000 oak trees = 3 draw calls (LOD0, LOD1, LOD2)
- 500 copper rocks = 3 draw calls (LOD0, LOD1, LOD2)
- Total: 6 draw calls for resources
Memory Efficiency
Shared Resources:- Geometry buffers shared across all instances
- Materials shared per LOD level
- Highlight meshes shared per model pool
- Only instance matrices stored per-entity
- 1000 trees with individual meshes: ~50MB geometry data
- 1000 trees with instancing: ~50KB geometry data + 64KB matrices
- Savings: 99% reduction in geometry memory
LOD Hysteresis
Hysteresis prevents flickering when camera distance oscillates near LOD boundaries:- LOD0 → LOD1 switch at 100% distance
- LOD1 → LOD0 switch at 90% distance
- 10% hysteresis band prevents rapid switching
Fallback Behavior
When Instancing Fails
The strategy falls back toStandardModelVisualStrategy when:
- Instance pool is full (MAX_INSTANCES = 512)
- Model loading fails
- Instancer not initialized
- All methods delegate to fallback strategy
- No instancing benefits, but entity still renders
- Prevents visual gaps when pools are full
Integration
Initialization
The instancers are initialized increateClientWorld.ts:
Per-Frame Update
The instancer must be updated every frame for LOD switching:- Checks camera distance for each instance
- Switches LOD levels as needed
- Updates dissolve material uniforms (camera pos, player pos)
- Marks instance matrices as dirty when instances move between pools