Skip to main content

Terrain System

The terrain system generates procedural 3D terrain using multi-layer Perlin noise with support for flat zones under stations and buildings. The system uses worker-based computation for height and normal calculations to maximize performance.
Location: packages/shared/src/systems/shared/world/TerrainSystem.ts

Architecture

Worker-Based Computation

The terrain system offloads height and normal calculations to a Web Worker for optimal performance:
Performance Benefits:
  • Zero noise calls on main thread for non-flat-zone tiles
  • Parallel computation across multiple tiles
  • Normals computed in worker with centered finite differences
  • Main thread only handles flat zone blending

Height Parameter Synchronization

All terrain generation constants are centralized in TerrainHeightParams.ts to prevent parameter drift between main thread and worker:
Implementation:
  • TerrainSystem imports params as TypeScript constants
  • TerrainWorker receives params via buildGetBaseHeightAtJS() injection
  • Single source of truth prevents parameter drift
  • Worker and main thread produce identical heights
  • All numeric constants baked into worker code at runtime

Worker Height Computation

The worker computes fully correct heights including shoreline adjustments:

Normal Computation in Worker

Normals are computed in the worker using centered finite differences on an overflow grid:
Benefits:
  • Accurate normals at tile boundaries (no edge artifacts)
  • Centered differences produce smoother lighting
  • Worker handles all computation (zero main thread cost)

World Specs

Noise Layers

Terrain height is generated from multiple Perlin noise layers defined in TerrainHeightParams.ts: Combined height is normalized to [0, 1], raised to power curve (1.1), then scaled by MAX_HEIGHT (50m). Coastline Variation: Natural irregular shorelines created by sampling noise on a circle around the island:
  • Large-scale: 3 octaves, weight 0.2
  • Medium-scale: 3x frequency, 2 octaves, weight 0.08
  • Small-scale: 8x frequency, weight 0.02
  • Varies island radius by ±20% for organic coastlines

Flat Zones

Flat zones create level terrain under stations and buildings with smooth blending to procedural terrain.

How It Works

Height Calculation Priority:
  1. Flat zones checked before procedural terrain
  2. Core Flat Area: Inside the zone, terrain returns exact height value
  3. Blend Area: Within blendRadius of zone edge, smoothstep interpolation blends to procedural terrain
  4. Spatial Indexing: Terrain tiles (100m) used for O(1) lookup
  5. Manifest-Driven: Stations with flattenGround: true automatically create flat zones
Blend Formula:

Flat Zone Registration

When a station spawns, TerrainSystem registers a flat zone: Dimensions calculated from:
  • Station footprint (from model bounds)
  • flattenPadding (extra space around footprint)
  • flattenBlendRadius (smooth transition zone)
Height sampled from:
  • Procedural terrain at station center
  • Ensures flat zone matches surrounding terrain elevation

Height Calculation

When terrain height is requested, flat zones are checked first:

TerrainSystem API

Duel Arena Floor Fix (commits b8f56e81, 7a60135e, 51453da)

Players and agents were sinking ~0.4m into duel arena floors because flat zones were not being registered with the terrain system. This caused getHeightAt() to return raw procedural terrain height instead of floor-level height, and also allowed grass to grow through floor surfaces. Problem:
  • Flat zones were not registered for duel arena floors
  • getHeightAt() returned procedural terrain height (~0.4m below arena floors)
  • Players/agents spawned at procedural height, sinking into visual floor meshes
  • Grass system used procedural heights, rendering grass through floors
  • Terrain mesh rendered at procedural height, creating z-fighting with floor geometry
Solution: Flat zones are now registered programmatically from DuelArenaVisualsSystem for all 8 floor areas (6 arenas + lobby + hospital):
Key Implementation Details:
  1. Procedural Height Sampling: Uses getProceduralTerrainHeight() to get the raw terrain height at each arena center, then adds 0.4m offset for player standing height
  2. Flat Zone Parameters:
    • height: Procedural terrain height + 0.4m (where players stand)
    • blendRadius: 1.0m smooth transition to surrounding terrain
    • carveInset: 1.0m inset from zone edges to preserve blend padding
  3. Registration Timing: Flat zones are registered in DuelArenaVisualsSystem.start() after terrain system is initialized but before arena meshes are created
  4. Cleanup: Flat zones are unregistered in DuelArenaVisualsSystem.destroy() to prevent memory leaks
Affected Areas:
  • 6 duel arenas (20m × 24m each)
  • Lobby floor (40m × 25m)
  • Hospital floor (30m × 25m)
Impact:
  • Terrain height queries now return correct floor-level values (procedural + 0.4m)
  • Players/agents spawn at proper height (no sinking)
  • Grass system respects flat zones (no grass through floors)
  • Terrain mesh is carved under floor areas to prevent overdraw
  • Visual floor meshes positioned 2cm above terrain mesh to prevent z-fighting
  • Terrain tiles are automatically regenerated when flat zones are registered after initial tile generation
Terrain Mesh Regeneration: When flat zones are registered after terrain tiles have already been generated, the terrain system automatically regenerates affected tiles to reflect the flat zone heights:
This ensures terrain meshes always reflect flat zone heights, even when buildings or arenas are added after terrain initialization. Console Output:

Console Logging

TerrainSystem logs flat zone activity:

Performance Characteristics

Worker-Based Computation:
  • Height calculation: ~0ms on main thread (worker handles all noise)
  • Normal calculation: ~0ms on main thread (worker computes with overflow grid)
  • Tile generation: ~5-10ms total (worker + transfer + geometry build)
  • Flat zone tiles: ~15-20ms (requires main thread recomputation)
Spatial Indexing:
  • Flat zone lookup: O(1) via tile-based spatial index
  • Typical world: 5-10 flat zones, 10-20 tile keys
  • Negligible memory overhead (~1KB per zone)