Skip to content

terrain water shader overhaul#1104

Closed
tcm390 wants to merge 73 commits intomainfrom
tcm/terrain-water-shader-overhaul
Closed

terrain water shader overhaul#1104
tcm390 wants to merge 73 commits intomainfrom
tcm/terrain-water-shader-overhaul

Conversation

@tcm390
Copy link
Copy Markdown
Contributor

@tcm390 tcm390 commented Mar 28, 2026

sub pr: HyperscapeAI/assets#20

Summary

This PR is a major overhaul of the world rendering pipeline — terrain generation, shaders, water, sky/clouds, vegetation, and a brand-new procedural grass system. The goal is to make the world feel more like an open-world environment with fully procedural generation, unified anime-style lighting across all world objects, and dense vegetation coverage.

1. Terrain Generation & Shader Overhaul

Refactored the terrain from a shared-noise-with-weight-blending architecture to per-biome independent height generation. Each biome (Forest, Canyon, Tundra) now has its own BiomeTerrainConfig with full control over FBM octaves, erosion, altitude, rivers, terracing, and more

https://www.youtube.com/watch?v=SwobuCOmJIU

(You might want to watch the video at 2× speed.)


2. Water Shader Overhaul

Refactored the water system with a new shader pipeline, quadtree-driven mesh generation, and proper reflections.

water.mp4

3. Procedural Grass System

Built a new chunk-based procedural grass system from scratch with per-biome configuration, multi-LOD rendering, and web worker offloading.

forest-grass.mp4
canyon-grass.mp4
tundra-grass.mp4

4. Sky & Cloud Shader

  • Overhauled cloud shader with sprite atlas UV sampling, B-channel alpha dissolve, and sun proximity lighting
cloud2.mp4

5. Tree & Vegetation Improvements

  • Added new tree types
  • Biome-driven snow shader on trees (configurable via enableSnow in BiomeTreeConfig)
  • Poisson disk sampling for natural tree spacing with configurable clustering (radius, inter-cluster spacing)

tcm390 added 30 commits March 17, 2026 02:37
Centralize lighting config into LightingConfig.ts. Replace tree's
rawSunDir-based shade with dayIntensity-driven tint so trees and
terrain (scene lights) transition to blue at the same rate. Remove
unused applySunShade function and rawSunPosition plumbing.

Made-with: Cursor
Replace hardcoded day cycle thresholds and sun tilt with centralized
DAY_CYCLE and SUN_LIGHT config values from LightingConfig.ts.

Made-with: Cursor
Extract dayIntensity-driven shade logic into a shared function so any
future custom shader can import it for consistent blue tint timing.

Made-with: Cursor
Use normalWorldGeometry instead of manual modelNormalMatrix * normalLocal
to get the correct world-space sphere normal with per-instance rotation
applied, without normal map or face-direction distortion.

Made-with: Cursor
Add NIGHT.BRIGHTNESS master knob that controls both the tree shader
nightDim floor and scene light intensity bases, so adjusting one value
changes tree and terrain night brightness together.

Made-with: Cursor
…rough

Lobby floor, hospital floor, hospital cross, and banner cloths had
emissive properties that overpowered the blue-shifted scene lights at
night. Removing emissive lets them respond to ambient/hemisphere
lighting like all other standard materials.

Made-with: Cursor
…ital

Inset corner pillars by PILLAR_BASE_SIZE/2 so they sit flush at the
floor edge. Reduce hospital floor from 30x25 to 28x23 to avoid terrain
clipping at the edges.

Made-with: Cursor
…e tuning

Replace floor-based terrace quantization with ceil-based (raises terrain
to plateaus, never lowers). Add global ELEVATION_OFFSET, ELEVATION_NOISE_AMOUNT,
and ELEVATION_NOISE_SCALE constants for large-scale elevation variation.
Update biome noise profiles (tundra, forest, canyon) and increase island
radius to 2000. Mirror all changes in the worker JS string builder.

Made-with: Cursor
Create arena-layout.ts as the single source of truth for all duel arena
coordinates and dimensions. Update duel-manifest, world-areas,
DuelArenaVisualsSystem, and ClientCameraSystem to import from it instead
of using scattered hardcoded values. Shift arena Z by +50 from original.

Made-with: Cursor
Reduce NOISE_SCALE (0.0008 → 0.0003) so dirt/grass patches don't
visibly tile across the 4000-unit island. Reduce TERRAIN_TEX_TILE
(0.08 → 0.025) to match. Shift shoreline height thresholds up to
account for the global elevation offset (+8..+58 world units).

Made-with: Cursor
Sample each biome texture at two non-harmonic UV scales (1x and 0.13x)
and blend between them using Perlin noise. This prevents the tile grid
from being visually detectable across the terrain.

Made-with: Cursor
Terrain shader: slope drives dirt (low) vs cliff (high) on slopes,
noise-driven dirt patches on flats, dual-scale texture blending,
road overlay disabled. Move arena to (362, 434) and home to (444, 330).
Disable skull markers and wilderness border bands.

Made-with: Cursor
Lower ELEVATION_NOISE_SCALE from 0.0015 to 0.0005 for broader,
more natural elevation undulation across the island.

Made-with: Cursor
Increase WATER_THRESHOLD from 8.0 to 30.0 to account for the global
elevation offset applied to the terrain.

Made-with: Cursor
- Increase server chunk ranges (core 1→3, ring 1→5) so resource
  entities spawn up to ~1100m instead of 300m
- Increase client chunk ranges to match (ring 3→5, horizon 5→7)
- Decouple VegetationSystem fade from shadow maxFar so decorative
  trees render at distance without shadows instead of disappearing
- Set GPU vegetation fade to 1000-1200m across all systems
- Increase MAX_VEGETATION_TILE_RADIUS from 2 to 5 tiles
- Increase LOD distances for all vegetation/resource categories
- Note: VIEW_DISTANCE was dead code (unused checkPlayerMovement);
  the real tile loading uses coreChunkRange/ringChunkRange

Made-with: Cursor
…aware initial LOD

Boost Palm/Banana selection weight 20x near water so they dominate pond
shores. Increase waterSearchRadius to 120m and waterMaxDistance to 100m
for wider influence. Fix tree pop-in by computing correct initial LOD
based on camera distance instead of always starting at LOD0.

Made-with: Cursor
…cement rules

Increase LOD distances for all vegetation/resource categories. Expand
single shadow map frustum to 200m and size to 4096. Update fog config.
Add waterSearchRadius and waterMaxDistance to TreePlacementRules.

Made-with: Cursor
Remove Bamboo, ChinaPine, Yucca, Cactus, and WindSwept — none are
referenced in any biome config.

Made-with: Cursor
… load

Workers take multiple frames to JIT-compile and return results on first
page load, leaving terrain holes until they finish. Now the nearest 30
chunks (within 1200m) are generated synchronously on the main thread when
workers haven't returned yet. Also adds burst-mode processing, distance-
based priority sorting, and updates fog distances (300/1200).

Made-with: Cursor
…nd fog tuning

Water shader uses portfolio-style depth opacity curve (pow/saturate) so
deep water fully hides underwater terrain. Reflection fades out beyond
500m to avoid artifacts at distance. Terrain texture tiling and slope
thresholds adjusted. Fog range tightened for better atmosphere.

Made-with: Cursor
…olor

Replace the broken manual planar reflection system with the Three.js TSL
reflector, positioned at the correct water level. The previous "sheet shape"
artifact was caused by the reflector target being at Y=0 instead of Y=waterLevel.

- Use TSL reflector() which handles camera mirroring, render target, and
  oblique clipping internally via updateBefore()
- Position reflector target at waterLevel and sync on setWaterLevel()
- Remove manual renderReflection(), reflection camera, texture matrix,
  and all associated temp vectors
- Replace Beer-Lambert absorption with depth-fade color formula
  (pow(saturate(1 - depth/scale), falloff)) for smoother results
- Add distance-based color fade so far water shows uniform deep color
  instead of depth-buffer noise artifacts
- Increase sky dome radius to 5000 so the reflected camera stays inside

Made-with: Cursor
Introduce WaterVisualManager that listens to the terrain quad-tree and
generates flat water meshes per leaf node, replacing per-tile water when
USE_QUADTREE_LOD is enabled.

- Add WaterVisualManager: creates PlaneGeometry water chunks aligned
  with quad-tree nodes, with resolution scaling by tree depth
- Add CompositeQuadTreeListener to forward events to both terrain and
  water visual managers from the same quad-tree
- Gate per-tile generateWaterMeshes() behind !USE_QUADTREE_LOD
- Clean up water visual manager in TerrainSystem.destroy()

Made-with: Cursor
tcm390 added 23 commits March 25, 2026 18:56
…heck

Add enableSnow flag to BiomeTreeConfig and set it on tundra. Snow weight
is now computed by summing all snow-enabled biome weights at each tree
position, removing the hardcoded "tundra" string dependency.

Made-with: Cursor
- Refactor tree placement to use Poisson disk sampling for natural spacing
- Add clustering support with configurable radius and inter-cluster spacing
- Make species zone allocation weight-proportional (reduce zone boost 10x→3x)
- Fix tree LOD using "resource" distances (380m) instead of "tree" (800m)
- Fix snow shader LOD issue where distant trees appeared all white
- Lower vegetation alphaTest 0.5→0.1 to preserve sparse leaf texture edges
- Add clusterRadius/clusterSpacing config options to BiomeTreeConfig
- Update biome configs with clustering and corrected spacing values

Made-with: Cursor
The hardcoded starter_area was superseded by central_haven from the
world-areas.json manifest. Both were safe zones, producing two floating
home icons. Remove the stale default so only the JSON-loaded area remains.

Made-with: Cursor
Introduces GrassVisualManager, a QuadTreeListener that creates instanced
grass meshes (GLB model) on max-depth terrain leaves. Includes per-chunk
CPU-side frustum culling using the terrain chunk bounding box (same
approach as portfolio project) to toggle mesh.visible, avoiding the
InstancedMesh bounding-sphere bug with Three.js frustumCulled.

Made-with: Cursor
… anime shading

- Convert CPU terrain color output from sRGB to linear before passing to
  grass shader (float vertex attributes have no auto color space conversion)
- Add half-lambert anime shade to grass material matching terrain shader
  so shadow tint is consistent at the grass/terrain boundary
- Update CPU fallback color constants to match actual texture averages
- Use darken() helper (×0.65) for dark variants matching GPU TEX_DARKEN
- Sync sun direction uniform to grass from environment system
- Remove COLOR_VARIATION, derive gradient purely from root ground color
- Flip gradient: root is brighter (×1.5) to match terrain, tip fades darker
- Add rebuildAllChunks/invalidateRegion for flat zone and road updates
- Fix height data storage order (store after tile added to terrainTiles)
- Invalidate quad-tree chunks when flat zones register/unregister

Made-with: Cursor
Completes the flat zone invalidation support — terrain quad-tree chunks
overlapping a registered/unregistered flat zone are destroyed and
regenerated with correct heights, matching GrassVisualManager behavior.

Made-with: Cursor
- Fix CPU color constants: non-texture constants (height gradient, variation,
  cliff tint, sand, shoreline) now use raw linear values matching GPU vec3()
  instead of incorrectly applying sRGB-to-linear conversion via lin()
- Fix mismatched canyon constants (_CANYON_SAND_HIGH, _CANYON_VARIATION)
- Remove fresnel from applyAnimeShade (caused view-dependent color shifts)
- Add nightDim to applyCustomLighting matching tree shader pattern
- Bypass PBR for both terrain and grass via shared applyCustomLighting
- Add dayIntensity sync for terrain and grass day/night cycle
- Refactor GrassVisualManager to be biome-agnostic via getTerrainColorAt
- Add deferred chunk loading to reduce FPS hiccups on grass spawn
- Re-enable terrain texture loading

Made-with: Cursor
…D optimization

- Add BiomeGrassConfig with per-biome density, slope, height, and patchiness
- Blend grass configs by biome weight in TerrainSystem.getTerrainColorAt
- Noise-based patchy grass distribution (uniform for forest, clustered for canyon/tundra)
- Restore fresnel rim highlight to applyAnimeShade (always-on, shared by terrain and grass)
- Add GPU distance fade: grass shrinks into ground from 350-500m
- Prune far chunks beyond MAX_RENDER_DISTANCE to free GPU memory
- Tighten LOD tiers: full detail 0-80m, medium 80-200m, low 200-500m
- Remove global MIN_GRASS_WEIGHT in favor of per-biome minGrassWeight

Made-with: Cursor
Move all CPU-intensive grass computation (terrain height, biome color,
road influence, placement probability) into GrassWorker running off the
main thread, eliminating FPS hiccups during chunk generation and LOD
transitions. Main thread only creates InstancedMesh from pre-computed
Float32Arrays returned via zero-copy transfer.

- Rewrite GrassWorker with full terrain pipeline (shared builders)
- GrassVisualManager dispatches to worker pool, sync fallback kept
- LOD transitions keep old mesh visible until replacement arrives
- Set player position before quad-tree update for correct initial LOD
- TerrainSystem passes worker config, biome data, road segments

Made-with: Cursor
Pass flat zone rectangles to the grass worker and add an isInFlatZone
check in both the worker and sync fallback paths so grass is no longer
placed inside artificially flattened areas.

Made-with: Cursor
Sun and moon were rendering in front of clouds due to higher renderOrder.
Move celestial bodies before clouds in the render pipeline and extract
all render order values into a SKY_RENDER_ORDER constant object.

Made-with: Cursor
- Add Rodrigues rotation to tilt grass blades along terrain slope
- Override grass normalNode with terrain normal (world→view via
  cameraViewMatrix.transformDirection) so PBR N·L matches terrain
- Enable receiveShadow on grass meshes for shadow map sampling
- Apply applyAnimeShade as albedo tint on both terrain and grass;
  let PBR handle single Lambert N·L + shadow on top
- Terrain colorNode feeds anime-shaded albedo + vertex lights into
  PBR; outputNode only applies fog to PBR result

Made-with: Cursor
Add optional tintColor/tintStrength to BiomeGrassConfig so each biome
can tint grass tips while keeping roots blended with the terrain.
Tint data is passed as a separate per-instance vec4 attribute
(instanceGrassTint) interleaved with instanceGroundColor into a single
InstancedInterleavedBuffer to stay within WebGPU's 8 vertex buffer limit.

Made-with: Cursor
…stems

WorkerPool: track active tasks per worker and reject them on terminate()
so in-flight promises no longer dangle.

GLBTreeBatchedInstancer: guard against duplicate addInstance for same
entityId; make addToPool atomic (pre-validate all geoIds, use fixed-
length ids array aligned with batches).

GLBTreeInstancer: add MAX_INSTANCES capacity check in addToPool; check
target LOD pool instead of just LOD0; guard duplicate entityId; clone
attributes in createSharedGeometry so disposal doesn't corrupt the
model cache.

ProcgenTreeInstancer: use tracked.preset in removeInstance instead of
caller-provided preset; add capacity guard in showInMesh to prevent
slot overwrite on wrap.

GrassVisualManager: add destroyed flag to guard async callbacks; cancel
workerInflight/pendingLodSwap on prune, destroy, invalidate, and
rebuild; read latest pendingLodSwap in LOD swap completion for freshest
target; dispose lodGeometries in destroy(); deduplicate pendingNodes
pushes.

Made-with: Cursor
Canyon's computeCanyonHeight had a hardcoded water carving (riverWidthVar/edge1/edge2)
that was always active even with riverWidth: 0. Removed so canyon water features are
controlled purely by the rivers/lakes/lakesFalloff config params like other biomes.

Made-with: Cursor
Replace fixed 4-layer scrolling normals with two-phase flow crossfade
(FlowUVW technique from cloud-sea shader) for organic, non-repeating
water surface motion. Load waterNormal.png and noise28.png textures
with procedural fallbacks. Shift cosine gradient palette from teal/cyan
toward deep blue with subtle indigo undertone.

Made-with: Cursor
Remove TreeId.Fir and TREE_TYPES.fir since Fir is not used in any
biome config or woodcutting manifest. Bump water green offset from
-0.30 to -0.22 for a blue-green tint.

Made-with: Cursor
tcm390 added 2 commits March 28, 2026 12:48
…der-overhaul

Made-with: Cursor

# Conflicts:
#	packages/shared/src/systems/shared/world/GLBTreeBatchedInstancer.ts
#	packages/shared/src/systems/shared/world/GLBTreeInstancer.ts
Main's dissolve system uses the B channel (1.0 - dissolveVal), which
collided with our snow weight that was also in B. Reassign channels:
R = highlight, G = snow weight, B = dissolve. Update shader reads
in GPUMaterials to match (snow from batchColor.y, highlight from
batchColor.x only).

Made-with: Cursor
@claude
Copy link
Copy Markdown

claude bot commented Mar 28, 2026

Code Review: Terrain Water Shader Overhaul

This is a massive PR (~7K additions, ~5K deletions, 80 files) that overhauls terrain generation, water rendering, sky/clouds, procedural grass, and vegetation. The overall direction is solid — per-biome terrain configs, centralized lighting, and the move to TSL-based anime shading are good architectural decisions. Below are the issues I found, ordered by severity.


Critical

1. @ts-nocheck on 1232-line new file (GrassVisualManager.ts)
The entire file disables TypeScript checking. This suppresses all type errors across a massive new file and violates the project's "NO any types" rule. The stated reason is incomplete TSL type definitions, but @ts-nocheck is far too broad. Use @ts-expect-error on specific TSL lines, or introduce a shared TSL node type alias (e.g., type TSLNode = ShaderNodeObject<Node>) to replace bare any across all shader code.

2. Widespread any types in shader code
Found across multiple files:

  • LightingConfig.ts: applySunShade(color: any, dayIntensity: any, shadeColor: any), applyCustomLighting (6 any params)
  • GPUMaterials.ts: srcMat as any, baseAlbedo: any, snowMask: any, result: any
  • GLBTreeBatchedInstancer.ts: world!.getSystem<any>("terrain")
  • TerrainShader.ts: applyAnimeShade and terrain color function params (~10 instances)

Recommendation: Create a type TSLNode = ShaderNodeObject<Node> alias in a shared types file and use it everywhere instead of any.


High

3. No test coverage for major new features
The grass system (1232 lines), dock placement, toon shading pipeline, snow blending, species zoning via Poisson disk, and the WorkerPool activeTask bug fix all lack tests. The existing BiomeResourceSpawning.test.ts was actually weakened — it now only checks normalCount > willowCount instead of testing relative proportions.

4. Camera far plane / depth buffer precision
Far plane increased from 800 to 10000 (World.ts), near remains 0.2. The 50,000:1 ratio will cause z-fighting on distant geometry. Combined with shadow map frustum increase (80 → 200) and shadow map size (2048 → 4096, which is 4x the VRAM), this needs performance testing on target hardware.

5. LOD distance inflation (5-20x) without perf validation
Tree LOD1: 30m → 800m. Fog: 60/150 → 400/800. GPU vegetation fade: 270/300 → 1000/1200. These collectively increase visible scene complexity dramatically. The 28 cloud meshes (up from 4) add 7x more draw calls for sky alone. Has this been profiled?


Medium

6. Dock rotation convention may be inverted (DockDefinition.ts)
Comment says 0 degrees = north (-Z) but Math.cos(0) = 1 gives z = +1 (south). The single test dock has rotation: 180 which would then extend in -Z (north) — opposite of what the label says. Either the comment or the math needs fixing.

7. Code duplication in ResourceSystem.ts
The new client-side resource spawning path (lines 1229-1322) duplicates ~90 lines of entity construction from the server path. The client path also uses Math.random() for rotation while the server may use deterministic seeding, causing potential client/server visual disagreement.

8. GrassWorker terrain color sync is fragile
GrassWorker.ts builds shader logic as concatenated JavaScript strings with hardcoded color constants that must manually match TerrainShader.ts. The comment says "exact match of TerrainShader.ts lines 869-1022" but there's no test verifying parity. This will drift.

9. Dual water-level constants
The game uses TERRAIN_CONSTANTS.WATER_THRESHOLD = 16 while procgen keeps DEFAULT_WATER_THRESHOLD = 5.4 and adds GAME_WATER_LEVEL = 16. If the game value changes, GAME_WATER_LEVEL in procgen must be manually updated — fragile.

10. WATER_HEIGHT_PRECHECK = 35 hardcoded in BiomeResourceGenerator
This guard is disconnected from WATER_THRESHOLD config. If water threshold changes, this precheck becomes stale.


Low

11. Several features silently disabled

  • RoadNetworkSystem commented out in both createClientWorld.ts and createServerWorld.ts
  • Zone border visuals: shouldHaveBorder always returns false
  • Waterfall system: computeWaterfalls removed, waterfalls array always empty
  • starter_area removed from world areas; STARTER_TOWNS is now empty

These should be tracked or removed entirely rather than left as dead code.

12. Magic numbers in GrassVisualManager

  • Bounding box Y range: -50 to 200
  • Water threshold buffer: 0.1
  • Road influence threshold: 0.8
  • Wind constants: 0.35, 0.12, 0.28, 2.0, 1.8
  • Tip color multiplier: 1.4

13. pendingNodes.some() linear scan in hot paths
GrassVisualManager uses pendingNodes.some((p) => p.node === node) which is O(n) per call. For large pending queues, a Set-based check would be more efficient.

14. LightingConfig.ts ambient light decreases during daytime
AMBIENT_DAY_TOTAL(0.5) - NIGHT.BRIGHTNESS(0.8) = -0.3 means ambient decreases during day. This may be intentional (auto-exposure compensation) but should be documented since it's counterintuitive.

15. getRiverSurfaceYAtX ignores its _x parameter
Returns a single ocean level regardless of position. Fine with one bridge, but will break if bridges span different water bodies.


Positive Notes

  • Per-biome terrain configs (BiomeTerrainConfig) is a great architectural improvement over the old shared-noise-with-weight-blending approach
  • Centralized LightingConfig.ts — single source of truth for lighting constants is excellent
  • WorkerPool activeTask tracking — fixes a real bug where terminate() during active tasks left promises hanging
  • Duplicate-add guards in both tree instancers prevent orphaned instances
  • Initial LOD selection based on camera distance avoids pop-in
  • Poisson disk sampling for tree placement is well-implemented (correct Bridson's algorithm with grid acceleration)
  • Water level consolidation to a single WATER_THRESHOLD = 16 is a good cleanup

Security

No hardcoded secrets, credentials, or security concerns found.

@tcm390 tcm390 closed this Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant