feat: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation#1018
Conversation
Replace procgen tree rendering with GLB model loading via modelVariants array in woodcutting manifest. Consolidate tree types to single fir tree with 5 model variations. Refactor GLBTreeInstancer to support multi-material/multi-primitive GLB meshes, dynamic collision proxy sizing, double-sided leaf textures with repeat wrapping, and per-instance fresnel rim highlight driven by an InstancedBufferAttribute — eliminating the previous highlight mesh overlay and outline post-process pass for trees. Made-with: Cursor
Shadow receiving on VRM characters, duel arena objects, GLB resources, and placeholder instancers is a significant FPS bottleneck with no meaningful visual benefit for these objects. Made-with: Cursor
Replace post-processing outline highlight with per-instance Fresnel rim shader across GLBResourceInstancer, PlaceholderInstancer, and their visual strategies. This eliminates the costly extra render pass and separate highlight meshes, significantly reducing FPS drops on hover. Also split the monolithic GPUVegetation.ts (1370 lines) into: - LODConfig.ts: LOD distance table, scaling, caching (pure data/math) - GPUMaterials.ts: material factories (vegetation, dissolve, imposter) Tuned highlight brightness (BRIGHTEN 0.2→0.08) to reduce whitewash. Made-with: Cursor
…eShadow on stations Extend shader-based Fresnel rim highlight to all non-instanced entities (stations, items, mobs, NPCs, players) via base Entity class. Uses a per-material uniform toggle instead of instanced attributes, lazily applied on first hover. Eliminates post-processing outline pass for these entities, reducing GPU overhead. - Add applyRimHighlight() to GPUMaterials.ts for uniform-based highlight - Add setShaderHighlight() to base Entity class with lazy initialization - Remove dead legacy highlight code (getHighlightRoot, getHighlightMesh, activeHighlightMesh) from ResourceEntity, ResourceVisualStrategy, and EntityHighlightService - Disable receiveShadow on all station entities for performance Made-with: Cursor
The resourceVariants map was only populated for tree-type resources, causing ore/fishing/herb gathering to fall back to 'tree_normal' which no longer exists in manifests after tree type consolidation. Made-with: Cursor
…d terrain lighting - Add createTreeDissolveMaterial with vertex-color AO, saturation boost, and per-instance rim highlight for trees - Extract shared applyTerrainSunShade function used by both terrain and tree shaders for consistent shadow-side sky tinting - Refactor TerrainShader to use shared computeTerrainBaseColor function and applyTerrainSunShade in outputNode - Sync sun direction and shade color uniforms from Environment system for both terrain and tree materials (day/night cycle) - Remove failed terrain color blending at tree base Made-with: Cursor
…sed variant matching Replace per-variant InstancedMesh with BatchedMesh to consolidate all model variants into a single draw call per tree type per material slot. Update GPUMaterials highlight detection to use vBatchColor, and TreeGLBVisualStrategy to pass treeType + variants + variantIndex. Add texture fingerprint matching to fix material-geometry mismatch when GLB files have meshes in different traversal order. Made-with: Cursor
…d InstancedMesh for single models Split tree rendering into two paths: GLBTreeBatchedInstancer (BatchedMesh) for multi-variant trees and GLBTreeInstancer (InstancedMesh) for single-model trees. Unified TreeGLBVisualStrategy dispatches to the correct instancer based on config.modelVariants. GPUMaterials supports both highlight methods via a batched option. Made-with: Cursor
Models without COLOR_0 attribute (e.g. willow) were rendered at 35% brightness because the AO shader read zeroes from missing vertex data. Now checks source material's vertexColors flag and skips AO darkening when vertex colors are absent. Made-with: Cursor
Add a client-side quadtree LOD terrain rendering system that runs alongside the existing flat-grid for game logic. Key changes: - TerrainQuadTree: hierarchical quad-tree with configurable depth, split ratio, uniform resolution per chunk, and neighbor tracking - TerrainVisualManager: bridges quad-tree events to Three.js scene with async web worker dispatch and sync fallback - QuadChunkWorker: inline web worker for off-thread heightmap, normal, color, and biome computation with Transferable buffers - TerrainQuadChunkGenerator: main-thread assembly from worker output with road influence, flat zone overrides, and skirt generation - Centralized shadow config (TERRAIN_RECEIVE_SHADOW / TERRAIN_CAST_SHADOW) as single source of truth across flat-grid and quad-tree meshes Made-with: Cursor
…up, retry - Fix flat zone blending: use getFlatZoneHeight (smoothstep) instead of hard cutoff getFlatZoneAt - Add split/unsplit hysteresis (unsplitMultiplier: 1.2) to prevent thrashing at LOD boundaries - Fix qtContainer leak: remove from scene graph on dispose - Guard terrainContainer.parent null access in init - Add bounded retry (max 3 attempts) for failed chunk generation - Remove unused heightModifier from worker biomes type - Deduplicate sync fallback: generateQuadChunkDataSync produces QuadChunkWorkerOutput, then feeds through assembleQuadChunkGeometry - Make per-frame limits configurable via CONFIG Made-with: Cursor
…itecture - Add BiomeType enum (Tundra/Forest/Desert) as single source of truth - Create TerrainWorkerShared.ts to eliminate code duplication between workers - Implement per-biome noise profiles (terrain shape, terracing, power curves) - Add biome-blended GPU shader with distinct palettes per biome - Place exactly 3 biome centers (one per type) in equilateral triangle layout - Set Forest as default biome with sharp Gaussian boundaries - Clean up dead code (mountain boost, legacy biome types, duplicate worker logic) Made-with: Cursor
- Add quadtree chunk refresh pass to refreshRoadInfluence() so roads render correctly when USE_QUADTREE_LOD is enabled - Increase QUADTREE_RESOLUTION from 32 to 64 to match tile vertex density, preventing roads from appearing thinner in quadtree mode Made-with: Cursor
…are snow cap - Increase ISLAND_RADIUS from 350 to 525 (1.5x), scale falloff and ocean buffer proportionally - Add terraceSteps to BiomeNoiseProfile for per-biome terrace density (tundra: 4, forest: 0, desert: 8) - Remove global TERRACE_STEPS constant — now blended per-vertex from biome weights - Snow overlay now only appears in tundra biome areas instead of all non-desert high elevations - Landscape features (mountains/ponds) sourced from LANDSCAPE_FEATURES constant array Made-with: Cursor
Replace Gaussian bell-curve landscape features with a configurable stepped plateau algorithm. Each feature now supports per-layer terracing with tunable shape, edge sharpness, slope, and noise wobble. - New LandscapeFeatureDef params: layers, shapePower, edgeSharpness, layerSlope, noiseScale, noiseAmount (replaces gaussianCoeff) - layerSlope controls incline within each terrace band (0=flat, 1=full slope) - Noise wobble on boundaries for organic non-circular edges - Island radius increased to 788 (1.5x from 525) - Updated worker mirror and serialization to match Made-with: Cursor
…control Replace round-based terracing (caused low-lying artifacts) with floor-quantize algorithm that creates natural downward-stepping contour bands. Add per-biome terraceSharpness param controlling flat shelf vs cliff transition ratio. Tune biome profiles: disable terracing for forest, strengthen for desert. Made-with: Cursor
- Increase MAX_HEIGHT from 50 to 150 for taller terrain - Raise WATER_THRESHOLD from 9 to 50 to match new height scale - Halve all noise layer scales for spatially larger terrain features - Tweak forest hillWeight and landscape feature positions/strengths Made-with: Cursor
…fy constants - Move MAX_HEIGHT, WATER_LEVEL_NORMALIZED, SHORELINE_CONFIG to TerrainHeightParams - Replace per-biome terraceSteps with global TERRACE_STEPS (10) to fix boundary artifacts - Add terraceHeightScale to BiomeNoiseProfile — stretches shelf separation for taller cliffs - Add terraceSlope to BiomeNoiseProfile — blends natural slope back onto shelves - Fix Forest terraceSharpness from 5.1 to 0, increase hillWeight for better terrain depth - Sync worker mirror (buildGetBaseHeightAtJS / PROFILES_JS) with all changes Made-with: Cursor
…r, GameConstants - Rename TerrainShader TERRAIN_CONSTANTS to TERRAIN_SHADER_CONSTANTS to avoid naming conflict - TerrainSystem now imports MAX_HEIGHT, WATER_LEVEL_NORMALIZED, SHORELINE_CONFIG from TerrainHeightParams - Remove dead CONFIG fields (CAMERA_FAR, FOG_NEAR, FOG_FAR, shoreline duplicates) from TerrainSystem - TILE_SIZE now sourced from TERRAIN_CONSTANTS.TERRAIN_TILE_SIZE - Clean up stale WORLD_CONSTANTS (WORLD_SIZE, TERRAIN_HEIGHT_SCALE, SEA_LEVEL, etc.) - Fix WATER_THRESHOLD to 8.0 for correct water level Made-with: Cursor
Made-with: Cursor
…ystem - Rename BiomeType.Desert to BiomeType.Canyon across all code, types, workers, shaders, and tests - Remove stale biome types (plains, valley, mountains, swamp, lakes, jungle) from all configs - Update biomes.json manifest to only contain tundra, forest, canyon - Align TownSystem, RoadNetworkSystem, POISystem, BiomeResourceGenerator, ProcgenRockCache, ProcgenPlantCache suitability/cost/preset maps to 3-biome system - Update all test files to reference only tundra/forest/canyon - Narrow type unions in terrain.ts and world-types.ts Made-with: Cursor
Root cause: during duel combat, camera radius and phi (pitch) were both computed from live entity separation distance. As agents repositioned every ~1.2s via DuelCombatAI, separation oscillated causing ~2+ units of camera height swing per movement tick. Additionally, the LOS grid search (500ms refresh cycle) caused phi oscillation when it alternated between rating adjacent angles as "clear" vs "blocked", and 3 cascading smoothing layers with different linear rate caps created mechanical motion and inter-layer conflicts. Changes: - Lock entity Y positions during duel combat (COUNTDOWN/FIGHTING/RESOLUTION) to eliminate terrain sampling noise from competing systems - Lock separation distance at phase start so radius and phi stay stable - Skip LOS grid search during combat phases (arena has clear sightlines); use smooth exponential damping toward desired angles instead - Collapse 3-layer smoothing (cache→targetSpherical→spherical) into single-layer exponential damping for film-quality motion - Smooth phase bias transitions to prevent focus-point Y jumps - Remove separation-dependent phi variation that caused discontinuous pitch changes when agents moved - Remove all combat-reactive camera effects (punch-in, shake, dramatic low angle, FOV intensification) for clean cinematic experience - Add phase-aware camera parameters (radius, phi, FOV, orbit amplitude, focus bias) per duel phase for intentional cinematic language
…iming - Use MIN_ANNOUNCEMENT_DURATION for getPhaseEndTime during ANNOUNCEMENT phase since the phase early-exits as soon as both agents are alive - Tune FIGHTING camera hold timing: reduce min/max hold and idle threshold for more dynamic camera switching during combat
…olors Replace stepped-power-plateau with envelope × domain-warped ridge noise so terrace contours follow organic ridges instead of concentric circles. Add per-biome cliff colors (tundra/forest/canyon) and remove snow line. Made-with: Cursor
…tect external agents in monitor - Add `type: "STREAMING_STATE_UPDATE"` to streaming state API response so the keeper bot can process duel lifecycle events (was silently discarding) - Add external agent detection in admin monitor endpoint by scanning world entities with isAgent flag or agent- ID prefix - Update local anvil world address
Resolve contract address conflicts by taking main's values (ephemeral local anvil addresses). Camera system auto-merged cleanly.
Swap procedural flat-color terrain shading for CDN-loaded biome textures with triplanar mapping on cliffs and top-down projection on flat surfaces. Textures load async with palette-colored placeholders for instant display. Made-with: Cursor
- Replace PBR toon banding with portfolio-style Lambert lighting - Bypass PBR output, sample albedo directly for custom soft lighting - smoothstep + clamp compresses light range; clamp floor scales with dayFactor so trees properly darken at night - Add Fresnel edge brightening on leaves (morpho effect) - Apply terrain sun shade for consistent shadow-side tinting - Wire wind uniforms and isLeafMaterial detection in both instancers - Add knotwood tree type with spawn weight Made-with: Cursor
…dows CSM is too heavy and exceeds WebGPU's 16-texture binding limit when combined with the terrain's 13 biome/utility textures. Default to a single 2048 shadow map (ENABLE_CSM=false) which adds only 2 shadow textures (total 15, within limit). Terrain now receives shadows with an auto-disable guard when CSM is on. Made-with: Cursor
- Remove unused DEFAULT_BIOMES, BIOME_IDS, BIOME_TYPE_WEIGHTS from BiomeSystem and their exports from procgen/terrain/index - Remove hardcoded town override from TerrainSystem.getBiomeAt() that forced forest biome near legacy starter town coordinates - Fix BiomeSystem coordinate math: use tile center (tileX * tileSize) instead of offset (tileX * tileSize + tileSize/2) - Delegate TerrainSystem.getBiomeAt() directly to BiomeSystem.getBiomeForTile() - Use dynamic biome keys for fallbacks instead of hardcoded "plains" - Update TerrainGenerator.test.ts to use local TEST_BIOMES fixture - Remove temporary biome debug logging Made-with: Cursor
…init
The World constructor registers a default PhysicsSystem whose init() calls
waitForPhysX() with a 120s timeout. In streaming/spectator mode we skip
replacing it with the full Physics system, but the default stub remained
registered and stalled the entire init chain — causing the loading screen
to freeze at 3% "Initializing world systems...".
Now we explicitly removeSystem("physics") in streaming mode so the init
pipeline proceeds immediately without waiting for PhysX WASM.
- Add physics null guards in RigidBody.ts and Collider.ts for stream mode viewports where physics system is removed - Re-enable autonomous behavior (mining, chopping, fishing) for duel bot agents between duels instead of suppressing it - Remove dedicatedDuelBot gates that killed all open-world autonomy - Make shouldRunOpenWorldAutonomy() always return true - Start autonomous behavior on ElizaDuelBot connect - Request bank state on player spawn so goal planner has item data - Relax post-duel restore position from 120-unit lobby radius to 2000-unit world boundary so agents roam freely - Interleave Anthropic/Groq model agents for provider diversity - Bump @elizaos/plugin-elizacloud to 2.0.0-alpha.7
Resolve conflicts by accepting main for contract deploy artifacts, stream capture simplification, and createClientWorld PhysX block. Made-with: Cursor
…e unload Fixes sparse trees when moving between distant locations. Root causes: 1. Individual entityAdded packets were silently dropped by BandwidthBudget during rapid tile generation (NORMAL priority throttled under load) 2. ResourceEntities were never destroyed in EntityManager on tile unload, causing entity leaks and preventing re-creation on tile revisit Changes: - Batch all entity spawns for a tile into a single entitiesBatchAdded packet - Add sendHighPriority to ServerNetwork (configurable via useHighPriorityBatch) - Add suppressBroadcast option to EntityManager.spawnEntity() for batching - Destroy ResourceEntities in EntityManager when tiles unload - Register entitiesBatchAdded packet and client handler Made-with: Cursor
…ee shader highlight, batched instancer
…t T-pose fix, streaming state updates
…dInstancer fingerprinting
…etRockPresetsForBiome plains fallback
…ons, vi.stubEnv test isolation
…-biome placement - Add TreeId enum to replace hardcoded "tree_" prefix strings - Move per-biome tree configs (distribution, placement, density) to TerrainBiomeTypes.ts alongside BiomeType enum - Add per-biome TreePlacementRules so each tree type can have different placement constraints per biome (e.g. dead tree minHeight differs between canyon and tundra) - Remove spawnWeight from TreeTypeDefinition (now dead code since getTreeConfigForBiome always returns a config) - Remove DEFAULT_TREE_CONFIG from TerrainSystem - Add WindPine tree type using dead_06.glb for tundra biome - Update BiomeResourceGenerator to read placement from biome config instead of global tree type registry Made-with: Cursor
Resolved conflicts: - stream-to-rtmp.ts: kept darwin channel fix, removed duplicate CAPTURE_MODE - createClientWorld.ts: kept main's PhysX background load for stream mode Made-with: Cursor
PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree AllocationNice work on this PR — the biome-aware tree placement pipeline, batched entity spawning, and tile unload cleanup are solid improvements. Here's a detailed review: Code Quality & DesignStrengths:
Suggestions:
Potential Bugs
Performance Considerations
SecurityNo security concerns — the changes are server-authoritative and don't expose new client-controlled inputs. Test Coverage
Minor / Nits
Overall: Strong architecture changes with clean data-driven design. The batched entity spawn is a meaningful optimization. Main action items are the potential waterAffinity logic clarification (#4), adding a batch size cap (#5), and considering bandwidth impact of HIGH-priority batches during rapid tile generation (#6). Tests would give good confidence for the networking and tile lifecycle changes. |
Add maxSlope to BiomeTreeConfig — estimates terrain gradient at each candidate position using central differences and skips placement when the slope exceeds the threshold. Defaults: Forest 1.5, Canyon 2.0, Tundra 1.5. Made-with: Cursor
PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree AllocationOverall this is a well-structured PR that cleanly separates biome-specific tree configs from the core tree type registry, introduces batched entity networking to reduce per-entity overhead, and fixes an entity leak on tile unload. The PR description is excellent. Below are findings organized by severity. Potential Bugs / Correctness1. Batch broadcast goes to ALL clients, not spatially filtered 2.
3. Reference equality check for distribution objects may be fragile if (posConfig && posConfig.distribution !== tileDist) {This relies on reference equality. If Performance Considerations4.
5. Per-position distribution recomputation Code Quality6. Inline type casts for const network = this.world.network as {
sendHighPriority?: (name: string, data: unknown) => void;
send?: (name: string, data: unknown) => void;
} | null;This pattern also appears for 7. 8. What Looks Good
Minor / Nits
SummaryThe biome system and tree placement architecture are well-designed and data-driven. The main concern is the broadcast-to-all for batched entities (#1 above) — this should be spatially scoped before merging to avoid unnecessary bandwidth on populated servers. The batch handler's missing special-case logic (#2) is worth addressing or explicitly documenting. The performance items (#4, #5) are worth profiling but not blocking. |
…p in BiomeTreeConfig Eliminates duplicate tree definitions by combining spawn weight and placement rules into a unified TreeSpawnConfig per tree type per biome. Made-with: Cursor
PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree AllocationOverall this is a well-structured PR that cleanly separates biome-specific tree configuration from the core tree type registry. The batch entity spawning optimization and the per-position biome lookup for tree placement are solid improvements. Here are some specific observations: Code Quality & DesignStrengths:
Minor items:
Potential Bugs & Issues
Performance Considerations
Security ConcernsNo security issues identified. The changes are server-authoritative (entities spawned server-side, broadcast to clients). The Test Coverage
SummarySolid PR with clean architecture. The main things to verify before merge:
|
Assets sub-PR: PlayHyperia/assets#15
Summary
This PR introduces three major terrain features: a quad-tree LOD rendering system for large map support, a data-driven biome and landscape system for terrain generation, and a biome-aware tree placement pipeline.
1. Quad-Tree LOD Terrain Rendering
A hybrid quad-tree visual LOD system that replaces the flat 100m grid for rendering only — gameplay logic, physics, resource spawning, and server sync continue to use the flat grid.
Config flags (in
TerrainSystem.CONFIG):USE_QUADTREE_LOD— enables the quad-tree LOD system (set tofalseto use the previous flat grid renderer, no breakage)QUADTREE_DEBUG_WIREFRAME— renders terrain as wireframe with depth-colored chunks: red (depth 0, largest) → orange → yellow → cyan → green (depth 4, finest)2. Biome & Landscape Terrain System
A data-driven terrain generation pipeline where biomes and landscape features are defined declaratively.
3 biomes defined in
TerrainBiomeTypes.tsvia theBiomeTypeenum:2 landscape types defined in
TerrainHeightParams.tsvia theLandscapeTypeenum:Currently 3 example features are placed in
LANDSCAPE_FEATURES[]: 2 mountains and 1 pond. To add new biomes or landscapes, add entries toTerrainBiomeTypes.ts(biomes) andTerrainHeightParams.ts(landscape features).Terrain shader (
TerrainShader.ts):3. Biome-Aware Tree Placement
Each biome now defines its own tree distribution, density, spacing, and per-tree placement rules in
TerrainBiomeTypes.ts:4. GLBTreeBatchedInstancer — Multi-Variant Tree Rendering
A new
BatchedMesh-based instancer (GLBTreeBatchedInstancer.ts) for tree types that have multiple model variants (e.g.tree_deadhas 5 dead tree models,tree_cactushas 8).Why BatchedMesh instead of InstancedMesh:
InstancedMeshbinds a single geometry — to support N model variants you'd need N separateInstancedMeshobjects per material slot per LOD level, multiplying draw calls.BatchedMeshregisters all variant geometries viaaddGeometry()and each instance picks its variant viaaddInstance(geometryId), keeping it to 1 draw call per material slot regardless of how many variants exist.The old
GLBTreeInstancer(InstancedMesh-based) is still used for single-model resources.TreeGLBVisualStrategyroutes to the correct instancer based on whether the manifest entry hasmodelVariantsor a singlemodelPath.