Skip to main content

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: Both instancers share the same architecture:
  • Load each model once, extract geometry by reference
  • Render all instances via single THREE.InstancedMesh per 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:
Visual Strategies:
  1. TreeGLBVisualStrategy - GLB tree models via GLBTreeInstancer
  2. InstancedModelVisualStrategy - Rocks, ores, herbs via GLBResourceInstancer
  3. StandardModelVisualStrategy - Fallback for non-instanced rendering
  4. FishingSpotVisualStrategy - Fishing spot particles
  5. PlaceholderVisualStrategy - Colored cubes for missing models

ResourceVisualStrategy API

Interface

BREAKING CHANGE: onDepleted Return Type

Before (pre-PR #946):
After (PR #946, February 2026):
Migration Guide: All custom visual strategies must update their onDepleted() signature:

New Method: getHighlightMesh

Purpose: Provide a positioned mesh for outline rendering on instanced entities. Signature:
Implementation Example:
Usage: The 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 InstancedMesh per 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:
LOD File Naming Convention:
  • 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:
Lifecycle:
  1. Resource spawns → added to normal pool at LOD0
  2. Resource depleted → removed from normal pool, added to depleted pool
  3. Resource respawns → removed from depleted pool, added back to normal pool
Benefits:
  • 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 integrates GLBResourceInstancer with the ResourceEntity lifecycle. Location: packages/shared/src/entities/world/visuals/InstancedModelVisualStrategy.ts

Implementation

Collision Proxy

Since InstancedMesh is not raycastable, the strategy creates an invisible collision proxy:
Key Points:
  • 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 the getHighlightRoot() method:
Highlight Flow:
  1. User hovers over instanced entity
  2. EntityHighlightService.setHoverTarget() called
  3. Service calls entity.getHighlightRoot()
  4. Strategy returns positioned highlight mesh
  5. Service adds mesh to scene temporarily
  6. Outline pass renders highlight mesh
  7. Service removes mesh when hover ends

Highlight Mesh Lifecycle

State Transition Handling: When a resource transitions between normal and depleted states, the old highlight mesh is removed:

Depleted Model System

Configuration

Resources specify depleted models in their manifest:

Instancer Implementation

The instancer maintains separate pools for normal and depleted states:
Depletion Flow:
  1. Resource depleted → setDepleted(entityId, true) called
  2. Remove instance from current LOD pool (LOD0/LOD1/LOD2)
  3. Add instance to depleted pool with depletedScale
  4. Update collision proxy userData (depleted: true, interactable: false)
  5. Remove old highlight mesh if entity is currently hovered
Respawn Flow:
  1. Resource respawns → setDepleted(entityId, false) called
  2. Remove instance from depleted pool
  3. Add instance back to LOD0 pool with normal scale
  4. Update collision proxy userData (depleted: false, interactable: true)

Y-Offset Calculation

Each model pool calculates Y-offsets to ensure models sit flush on terrain:
Separate Offsets:
  • yOffset - Normal model offset
  • depletedYOffset - 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
After Instancing:
  • 1000 oak trees = 3 draw calls (LOD0, LOD1, LOD2)
  • 500 copper rocks = 3 draw calls (LOD0, LOD1, LOD2)
  • Total: 6 draw calls for resources
Reduction: 99.6% fewer draw calls

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
Memory Savings:
  • 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:
Effect:
  • 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 to StandardModelVisualStrategy when:
  • Instance pool is full (MAX_INSTANCES = 512)
  • Model loading fails
  • Instancer not initialized
Fallback Behavior:
  • 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 in createClientWorld.ts:

Per-Frame Update

The instancer must be updated every frame for LOD switching:
Update Logic:
  • 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

Testing

Unit Tests

The instanced rendering system includes comprehensive tests:

Integration Tests

Full resource lifecycle tests:

ECS Architecture

Entity Component System architecture and patterns

Particle System

GPU-instanced particle rendering system

Resource Entities

Resource entity lifecycle and gathering mechanics

Performance Guide

Performance optimization techniques and benchmarks