mapspinner 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapspinner",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "WebGL2 Earth-scale terrain rendering SDK for interactive globe applications",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/planet.html CHANGED
@@ -102,11 +102,11 @@ function jsSnoise3(px,py,pz){
102
102
  }
103
103
  function jsBroadShape(dx,dy,dz){
104
104
  const r=Math.hypot(dx,dy,dz)||1; dx/=r; dy/=r; dz/=r;
105
- let amp=6500.0, freq=3.0, sum=0.0; // A0=6500, off=-900: matches GLSL broadShapeM (one-fractal collapse)
105
+ let amp=6500.0, freq=0.75, sum=0.0; // A0=6500, off=-500: matches GLSL broadShapeM (one-fractal collapse)
106
106
  // hi-freq elevation noise 4x cut (user 2026-06-06): the GPU broadShapeM scales the o>=6 fine band by
107
107
  // uHiFreqCut (0.25); mirror it here so the collision FALLBACK height tracks the rendered surface.
108
- for(let o=0;o<14;o++){ let nn=jsSnoise3(dx*freq,dy*freq,dz*freq); if(o>=6) nn*=0.25; sum+=amp*nn; amp*=(o<6?0.6:0.82); freq*=2.0; }
109
- return sum-900.0; // metres, neutral relief (reliefMul=1, ridgeMul=0)
108
+ for(let o=0;o<14;o++){ let nn=jsSnoise3(dx*freq,dy*freq,dz*freq); if(o>=6) nn*=0.25; sum+=amp*nn; amp*=(o<6?0.66:0.80); freq*=2.0; }
109
+ return sum-500.0; // metres, neutral relief (reliefMul=1, ridgeMul=0)
110
110
  }
111
111
 
112
112
  // ---- Vector helpers (used by the free-fly camera + WebGL2 sun/orientation math).
@@ -372,8 +372,18 @@ function frameLoop(){
372
372
  // The orchestrator works in METERS (its producer generates meter-scale elevation).
373
373
  // We drive it in meter-space and scale the km-unit camera into meters via
374
374
  // WEBGL2_M_PER_UNIT below.
375
+ // SPLITFACTOR PIN (user 2026-06-14): set 0.28 BEFORE orch init so the FIRST quadtree build uses
376
+ // it -- otherwise window.__splitFactor is undefined for the first frames and the orchestrator's
377
+ // altitude ramp (planet-orchestrator.js:445, taken when __splitFactor==null) overrides any opts
378
+ // default. gen-controls re-affirms the same 0.28 on apply(). 0.28 lands ~500-600 quads at the
379
+ // deck; splitDist floors at 1.1 so this is near the coarse limit.
380
+ if (window.__splitFactor == null) window.__splitFactor = 0.28;
375
381
  import('./src/planet-orchestrator.js')
376
- .then(m => m.initMapspinnerPlanet(glw, { radius: WEBGL2_TERRAIN_R_M, frustumCull: false }))
382
+ .then(m => m.initMapspinnerPlanet(glw, { radius: WEBGL2_TERRAIN_R_M, frustumCull: false,
383
+ // FPS lever (2026-06-14): mesh GRID is overridable via ?grid=N for live triangle-count
384
+ // A/B (the bottleneck is triangle/vertex throughput, not the broadShapeM ALU -- octMax
385
+ // 12->3 left frame time flat). Absent param falls through to gl-render's 16 default.
386
+ gridMeshSize: ((new URLSearchParams(location.search).get('grid')|0) || undefined) }))
377
387
  .then(orch => { window.__planetOrch = orch; window.__planetOrchGL = glw; window.__planetOrchStatus='ready';
378
388
  import('./src/terrain-gen-controls.js').then(()=>{ try{ window.__gen && window.__gen.apply(); }catch(_){} }).catch(e=>console.warn('gen-controls',e)); })
379
389
  .catch(e => { window.__planetOrchStatus='error:'+(e.message||e); console.error('orch init',e); });
@@ -477,9 +487,9 @@ function frameLoop(){
477
487
  // take the existing CPU-mirror fallback. Below 20km (where collision is load-bearing) keep GPU-exact.
478
488
  const _altRoughKm = _camLenMv - RADIUS;
479
489
  const _gpuGM = (_altRoughKm < 20.0 && _orchMv && _orchMv.render && _orchMv.render.sampleGroundM) ? _orchMv.render.sampleGroundM(_camDirMv) : null;
480
- const _surfM = (_gpuGM != null && isFinite(_gpuGM)) ? Math.max(0, _gpuGM)
481
- : Math.max(0, jsBroadShape(cam.pos[0],cam.pos[1],cam.pos[2]));
482
- const altSurfElev = Math.max(0, Math.min(_surfM, 9500.0)) / WEBGL2_TERRAIN_R_M;
490
+ const _surfM = (_gpuGM != null && isFinite(_gpuGM)) ? _gpuGM
491
+ : jsBroadShape(cam.pos[0],cam.pos[1],cam.pos[2]);
492
+ const altSurfElev = Math.min(_surfM, 9500.0) / WEBGL2_TERRAIN_R_M;
483
493
  const altKmAbove = Math.max(1e-3, (Math.hypot(...cam.pos) - RADIUS*(1.0 + altSurfElev)));
484
494
  // CAMERA MOVE-STEP. NO ELEVATION SLOWDOWN (user 2026-06-05: 'get rid of the camera slowdown code').
485
495
  // The step scales with altitude-above-surface ONLY so the scene still moves at orbit (a constant step
@@ -522,14 +532,12 @@ function frameLoop(){
522
532
  // a height above an inflated reference -> the eye was really ground+margin+2m, not 2m.) The mirror
523
533
  // fallback keeps a small +3m only because the CPU jsBroadShape can under-predict vs the GPU.
524
534
  if (gpuM != null && isFinite(gpuM)) {
525
- renderedM = Math.max(0, gpuM); // GPU-exact rendered height; no margin
535
+ renderedM = gpuM + 1.0; // +1m margin for sub-vertex micro-relief (vtxDisplace peaks exceed 2m in rugged terrain)
526
536
  } else {
527
- renderedM = Math.max(0, jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2])) + 3.0; // mirror fallback (CPU under-predicts)
537
+ renderedM = jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2]) + 3.0; // mirror fallback (CPU under-predicts)
528
538
  }
529
- // cap = composeHeight's true ceiling CMAX=15000 (terrain.glsl:448), NOT 11000: the GPU probe
530
- // renderedM is honest up to 15km, but re-capping at 11000 before minR let the camera floor sit
531
- // BELOW visible 11-15km peaks -> tunnel through the rendered geometry (collision-elev-margin, wb4syopmo).
532
- const surfElev = Math.max(0.0, Math.min(renderedM, 15000.0)) / WEBGL2_TERRAIN_R_M; // fraction of R (match CMAX)
539
+ // cap to composeHeight's CMAX=15000; allow negative for seabed
540
+ const surfElev = Math.min(renderedM, 15000.0) / WEBGL2_TERRAIN_R_M;
533
541
  const eyeHeight = 2.0 / WEBGL2_M_PER_UNIT; // EXACTLY 2.0 m above ground (km-units = 2m / m-per-unit)
534
542
  const minR = RADIUS * (1.0 + surfElev) + eyeHeight;
535
543
  if (r < minR) { const s = minR/r; cam.pos[0]*=s; cam.pos[1]*=s; cam.pos[2]*=s; }
@@ -537,7 +545,7 @@ function frameLoop(){
537
545
  if (r > maxR) { const s = maxR/r; cam.pos[0]*=s; cam.pos[1]*=s; cam.pos[2]*=s; }
538
546
 
539
547
  // height of the eye above the local ground, in metres (for the HUD readout).
540
- const heightAboveGroundM = (Math.hypot(...cam.pos) - RADIUS*(1.0 + Math.max(0,surfElev))) * 1000;
548
+ const heightAboveGroundM = (Math.hypot(...cam.pos) - RADIUS*(1.0 + surfElev)) * 1000;
541
549
  window.__altM = heightAboveGroundM;
542
550
 
543
551
  // The camera (autopilot + free-fly + clamp) has now been fully updated this frame,
@@ -571,7 +579,7 @@ function frameLoop(){
571
579
  // at 2 by a diagnostic turned a dropdown pick of 1 into 2|1=3 (invalid mode -> silent lit) = the
572
580
  // 'render menu does nothing' report. Explicit precedence: override wins when set, else the menu.
573
581
  const _dmEff = (window.__displayMode != null ? window.__displayMode : cam.displayMode) | 0;
574
- const fr = window.__planetOrch.frame(eyeM, tgtM, 45*Math.PI/180, _dmEff, _sun, oceanTime, cam.up);
582
+ const fr = window.__planetOrch.frame(eyeM, tgtM, 45*Math.PI/180, _dmEff, _sun, oceanTime, cam.up, surfElev);
575
583
  window.__webglFrame = fr;
576
584
  // First rendered frame: tear down the loading overlay (compile finished, terrain is on screen).
577
585
  if (window.__loadOverlay !== false) { const ov=document.getElementById('__loadOverlay');
package/server.js CHANGED
@@ -1,6 +1,8 @@
1
- const http = require('http');
2
- const fs = require('fs');
3
- const path = require('path');
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
6
 
5
7
  const PORT = process.env.PORT ? parseInt(process.env.PORT) : 8080;
6
8
  // Static file server for the GPU one-fractal planet (planet.html + src/*.js + shaders/*.glsl).
package/src/gl-render.js CHANGED
@@ -49,7 +49,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
49
49
  // (-52%) and tris/quad 1152->512 (-55%), so the per-vertex 14-oct broadShapeM VS (browser-9: 95% of
50
50
  // the low-alt frame) runs on ~half the vertices. median scales ~24/16 -> ~3.6px, far closer to the
51
51
  // band; the fine relief is carried per-pixel by the FS dFdx normal, not the mesh tessellation.
52
- const GRID = opts.gridMeshSize || 16; // mesh quads per edge
52
+ const GRID = opts.gridMeshSize || 11; // mesh quads per edge. 16->11 (user 2026-06-14): FPS is TRIANGLE-THROUGHPUT bound, not broadShapeM ALU (octMax 12->3 left frame time flat; GRID is ~linear). GRID 8 was faster (-50%) but made BIOME CROSSOVER LINES JAGGED (climate varying interpolated across coarse triangles steps along edges) -- reverted to 11 (-37%). Proper fix to reclaim GRID 8 = per-pixel biome sampling in the FS. Override via ?grid=N.
53
53
  // Expose the LIVE mesh grid so screen-space-error diagnostics (planet.html __diag.pxPerPoly)
54
54
  // divide by the real polys/tile instead of a stale literal. Any future GRID change self-corrects
55
55
  // the metric (the 24->16 lever left pxPerPoly defaulting to 24 = 1.5x wrong band fraction).
@@ -286,6 +286,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
286
286
  gl.uniform1f(loc('uDetailOverlay'), g('detailOverlay', 6.0)); // perlin-everywhere ELEVATION term in composeHeight -- probe must match the VS or collision diverges
287
287
  gl.uniform1f(loc('vtxDetail'), g('vtxDetail', 1.0)); // DECISIVE: vtxDisplace strength (early-return on 0)
288
288
  gl.uniform1f(loc('canyonDepthMul'), g('canyonDepth', 1.0));
289
+ gl.uniform1f(loc('uBeachShelfM'), g('beachShelf', 2400.0)); // land coastal shelf (geometry); probe MUST match render
289
290
  gl.uniform1f(loc('cliffAmt'), g('cliffAmt', 1.0));
290
291
  gl.uniform1i(loc('uFloatLinearOK'), _halfFloatLinearOK ? 1 : 0);
291
292
  // FXC unroll-defeat (2026-06-12 AMD d3d11 fix): runtime octave bound for broadShapeM; the shader
@@ -300,7 +301,8 @@ export async function initMapspinnerRender(gl, opts = {}) {
300
301
  gl.uniform1i(loc('uFSDetailOcts'), (typeof window!=='undefined' && window.__fsDetailOcts!=null) ? (window.__fsDetailOcts|0) : 3);
301
302
  // FXC fold-defeat (2026-06-12, the rock-on-flat patches): the lit-normal FD step is uniform-fed
302
303
  // so d3d11/FXC cannot constant-fold the 150/R offset. Live dial: window.__nrmStepM.
303
- gl.uniform1f(loc('uNrmStepM'), g('nrmStepM', 150.0));
304
+ gl.uniform1f(loc('uNrmStepM'), g('nrmStepM', 300.0));
305
+ gl.uniform1f(loc('uGrid'), GRID);
304
306
  gl.uniform1f(loc('uHpfInset'), (typeof window!=='undefined' && window.__hpfInset === false) ? 0.0 : 1.0); // SEAM FIX: inset sampler is the permanent default (matches bakeFace fu=x/(RES-1)); window.__hpfInset===false rolls back
305
307
  // ANCHOR-STEP A/B TOGGLES (per-area stairstep, wrxo0rr7a). Default 0 = current; set window.__<name>=1
306
308
  // to widen that anchor-keyed band. Set HERE so BOTH render and the _PROBE_ collision see them (parity).
@@ -474,6 +476,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
474
476
  uniform vec3 skyCamWorld; // camera world pos (meters)
475
477
  uniform vec3 skySunDir; // world sun dir (normalized)
476
478
  uniform float skyR; // sphere radius (meters)
479
+ uniform float uSkyFade; // 1 at surface, 0 at 100km
477
480
  void main(){
478
481
  // Reconstruct the world-space view ray from NDC, like the WebGPU skyFs: undo the
479
482
  // projection (divide by the proj diagonal) to get a view-space dir, then rotate
@@ -519,7 +522,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
519
522
  // glow + daylit sky read as an atmosphere without blowing out.
520
523
  vec3 c = radiance * 120.0;
521
524
  vec3 mapped = clamp((c*(2.51*c+0.03))/(c*(2.43*c+0.59)+0.14), 0.0, 1.0); // ACES
522
- fragColor = vec4(pow(mapped, vec3(1.0/2.2)), 1.0);
525
+ fragColor = vec4(pow(mapped, vec3(1.0/2.2)) * uSkyFade, 1.0);
523
526
  }`;
524
527
  function rawShader(type, source){ const s=gl.createShader(type); gl.shaderSource(s, source); gl.compileShader(s);
525
528
  if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)) throw new Error('sky '+type+': '+gl.getShaderInfoLog(s)); return s; }
@@ -630,9 +633,17 @@ export async function initMapspinnerRender(gl, opts = {}) {
630
633
  const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
631
634
  const camDist = Math.hypot(cam.eye[0], cam.eye[1], cam.eye[2]);
632
635
  const alt = Math.max(0.0, camDist - R);
636
+ const altAboveTerrain = Math.max(0.001, alt - R * (cam.surfElev || 0));
633
637
  const horizon = Math.sqrt(Math.max(0.0, camDist*camDist - R*R));
634
- const near = Math.max(1.0, alt * 0.1);
635
- const far = Math.max(horizon * 1.5, alt * 8.0) + R * 0.25;
638
+ // MATCH render()'s near exactly (2026-06-14 jank fix): the cull frustum must use the SAME near
639
+ // as the draw frustum, else behind-limb/screen-AABB culling diverges from what is actually drawn
640
+ // at the deck (cull near was max(*0.1,0.1) while render used the <2m 0.05 branch).
641
+ const near = altAboveTerrain < 2.0 ? 0.05 : Math.max(altAboveTerrain * 0.1, 0.05);
642
+ // FAR PLANE: horizon distance tracks the visible ground edge; blends toward camDist
643
+ // above 500km for orbital views so the full planet is visible.
644
+ const _fBlend = Math.min(1.0, Math.max(0.0, (alt - 500000.0) / 4500000.0));
645
+ const farGround = Math.max(horizon, alt * 8.0);
646
+ const far = farGround * (1.0 - _fBlend) + camDist * _fBlend;
636
647
  const proj = M4.perspective(cam.fovy||0.785, aspect, near, far);
637
648
  const eye = cam.eye;
638
649
  const viewRel = M4.lookAt([0,0,0], [cam.center[0]-eye[0], cam.center[1]-eye[1], cam.center[2]-eye[2]], cam.up||[0,1,0]);
@@ -643,7 +654,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
643
654
  // fp32 cancellation at ground level (eye~=world), garbaging the projection and blanking the
644
655
  // footprint. Subtracting in JS doubles first keeps the cull's projection precise near ground.
645
656
  const viewProjNoEye = M4.mul(proj, viewRel);
646
- return { viewProjRel, viewProjNoEye, eye, near, far, proj };
657
+ return { viewProjRel, viewProjNoEye, eye, near, far, proj, viewRel };
647
658
  }
648
659
 
649
660
  // Render a set of quads. quads: [{quad, face, elevLayer, normalLayer}], cam: {eye, center, up, fovy}
@@ -652,16 +663,18 @@ export async function initMapspinnerRender(gl, opts = {}) {
652
663
  // ADAPTIVE near/far (altitude-tied). A fixed near=1 / far=R*8 (~5e7) at a 50km eye
653
664
  // pushed ALL near-surface geometry to NDC z~=1 (the far-plane limit), collapsing depth
654
665
  // precision so most near quads z-fought / clamped off -> only one screen rectangle
655
- // survived. Tie the planes to altitude: near ~= alt*0.1 (a few km up high, meters near
656
- // the ground), far ~= the visible horizon distance (so the whole near surface fits the
657
- // [near,far] range with usable z precision) plus a margin to keep the far hemisphere /
658
- // limb. From space (alt >> R) this widens back out to ~R*8, preserving the full-globe
666
+ // survived. Tie the planes to altitude: near = alt*0.1 (naturally scales from 1m at
667
+ // deck to 1200km at orbit), far = horizon distance blended toward camDist above 500km
668
+ // for orbital views. From space the far widens out to ~R*8, preserving the full-globe
659
669
  // view. Clamped so near>=1 and far>near.
660
670
  const camDist = Math.hypot(cam.eye[0], cam.eye[1], cam.eye[2]);
661
671
  const alt = Math.max(0.0, camDist - R);
672
+ const altAboveTerrain = Math.max(0.001, alt - R * (cam.surfElev || 0));
662
673
  const horizon = Math.sqrt(Math.max(0.0, camDist*camDist - R*R));
663
- const near = Math.max(1.0, alt * 0.1);
664
- const far = Math.max(horizon * 1.5, alt * 8.0) + R * 0.25;
674
+ const near = altAboveTerrain < 2.0 ? 0.05 : Math.max(altAboveTerrain * 0.1, 0.05);
675
+ const _fBlend = Math.min(1.0, Math.max(0.0, (alt - 500000.0) / 4500000.0));
676
+ const farGround = Math.max(horizon, alt * 8.0);
677
+ const far = farGround * (1.0 - _fBlend) + camDist * _fBlend;
665
678
  const proj = M4.perspective(cam.fovy||0.785, aspect, near, far);
666
679
  const view = M4.lookAt(cam.eye, cam.center, cam.up||[0,1,0]);
667
680
  const viewProj = M4.mul(proj, view);
@@ -696,8 +709,28 @@ export async function initMapspinnerRender(gl, opts = {}) {
696
709
  gl.viewport(0,0,gl.drawingBufferWidth,gl.drawingBufferHeight);
697
710
  gl.clearColor(0.0,0.0,0.0,1); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
698
711
 
699
- // SKY/ATMOSPHERE PASS REMOVED (user: 'get rid of all non-terrain material for now like haze').
700
- // The background stays the plain clear color; only the terrain (lit by the sun) is drawn.
712
+ // SKY/ATMOSPHERE PASS: atmospheric limb/halo behind the terrain. Fades in below
713
+ // 100km (full at surface, transparent above 100km). Depth off so terrain overdraws.
714
+ {
715
+ const skyFade = Math.max(0.0, 1.0 - camAlt / 100000.0);
716
+ if (skyFade > 0.001) {
717
+ gl.useProgram(skyProg);
718
+ gl.uniformMatrix3fv(SU('camRot'), false, new Float32Array([
719
+ _cm.viewRel[0], _cm.viewRel[4], _cm.viewRel[8],
720
+ _cm.viewRel[1], _cm.viewRel[5], _cm.viewRel[9],
721
+ _cm.viewRel[2], _cm.viewRel[6], _cm.viewRel[10]
722
+ ]));
723
+ gl.uniform2f(SU('projDiag'), _cm.proj[0], _cm.proj[5]);
724
+ gl.uniform3f(SU('skyCamWorld'), eye[0], eye[1], eye[2]);
725
+ gl.uniform3f(SU('skySunDir'), sunDir[0], sunDir[1], sunDir[2]);
726
+ gl.uniform1f(SU('skyR'), R);
727
+ gl.uniform1f(SU('uSkyFade'), skyFade);
728
+ gl.disable(gl.DEPTH_TEST);
729
+ gl.bindVertexArray(skyVao);
730
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
731
+ gl.enable(gl.DEPTH_TEST);
732
+ }
733
+ }
701
734
 
702
735
  gl.enable(gl.DEPTH_TEST);
703
736
  // Back-face culling: the globe's FAR (back) hemisphere quads were drawing over the
@@ -766,6 +799,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
766
799
  // literals so the look is unchanged until the user dials a window global.
767
800
  const _g = (n,d)=> (typeof window!=='undefined' && window['__'+n]!=null) ? +window['__'+n] : d;
768
801
  gl.uniform1f(U('canyonDepthMul'), _g('canyonDepth', 1.0));
802
+ gl.uniform1f(U('uBeachShelfM'), _g('beachShelf', 2400.0)); // land coastal shelf (geometry): h<S eased h*h/S = wide beach
769
803
  gl.uniform1f(U('uHiFreqCut'), _g('hiFreqCut', 0.25)); // 0.5->0.25 (2026-06-10 'blotchy' -- see setComposeHeightUniforms)
770
804
  gl.uniform1f(U('uVertexAO'), _g('vertexAO', 1.0)); // per-vertex shading/AO strength (DEFECT 2, 2026-06-06)
771
805
  gl.uniform1f(U('cliffAmt'), _g('cliffAmt', 1.0));
@@ -844,7 +878,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
844
878
  gl.uniform1f(U('oceanAmp'), (oc.oceanAmplitude != null) ? oc.oceanAmplitude : 1.0);
845
879
  gl.uniform1f(U('oceanChoppy'), (oc.oceanChoppiness != null) ? oc.oceanChoppiness : 0.5);
846
880
  gl.uniform1f(U('oceanFoam'), (oc.oceanFoam != null) ? oc.oceanFoam : 0.5);
847
- gl.uniform1f(U('uBeachTopM'), _g('beachTop', 30.0)); // beach ceiling: grass stops, sand to the waterline + under it
881
+ gl.uniform1f(U('uBeachTopM'), _g('beachTop', 140.0)); // beach ceiling: grass stops, sand to the waterline + under it. 30->90 (user 2026-06-14: 3x beach width)
848
882
 
849
883
  // SINGLE INSTANCED DRAW: the deform params that were per-quad uniforms (ox,oy,l,level + face)
850
884
  // are now PER-INSTANCE attributes. Build one interleaved instance buffer [ox,oy,l,level,face]
@@ -870,6 +904,10 @@ export async function initMapspinnerRender(gl, opts = {}) {
870
904
  gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1); // iOffset
871
905
  gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1); // iFace
872
906
  gl.uniform1f(U('uIsWater'), 0.0);
907
+ // UNDERWATER DETECTION: camera below sea level enables underwater shading + water surface
908
+ // rendering from below. Set before the terrain draw so the FS can apply underwater fog.
909
+ const _uw = camDist < R - 2.0;
910
+ gl.uniform1f(U('uUnderwater'), _uw ? 1.0 : 0.0);
873
911
  gl.drawElementsInstanced(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0, n);
874
912
  // SEPARATE WATER SURFACE (user 2026-06-11): second instanced draw with uIsWater=1 -- the VS
875
913
  // pins the mesh to sea level, the FS shades animated water and alpha-blends it over the
@@ -901,9 +939,14 @@ export async function initMapspinnerRender(gl, opts = {}) {
901
939
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
902
940
  gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1);
903
941
  gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1);
904
- gl.enable(gl.BLEND);
905
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
906
- gl.depthMask(false);
942
+ if (_uw) {
943
+ gl.disable(gl.BLEND);
944
+ gl.depthMask(true);
945
+ } else {
946
+ gl.enable(gl.BLEND);
947
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
948
+ gl.depthMask(false);
949
+ }
907
950
  gl.uniform1f(U('uIsWater'), 1.0);
908
951
  gl.drawElementsInstanced(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0, wn);
909
952
  gl.uniform1f(U('uIsWater'), 0.0);
@@ -913,9 +956,11 @@ export async function initMapspinnerRender(gl, opts = {}) {
913
956
  }
914
957
  }
915
958
  if (typeof window !== 'undefined') window.__lastDrawCalls = (n > 0) ? 2 : 0;
916
- return gl.getError();
959
+ return 0; // glError is checked via checkGlError() once per frame after quadtree (CPU/GPU pipelining)
917
960
  }
918
961
 
962
+ function checkGlError() { return gl.getError(); }
963
+
919
964
  // ---- DEBUG PROBE: replicate the VS clip-space transform on the CPU for a quad's 4
920
965
  // corners (vertex.xy in {0,1}^2) so we can see which quads project off-screen.
921
966
  function probe(quads, cam) {
@@ -947,5 +992,5 @@ export async function initMapspinnerRender(gl, opts = {}) {
947
992
  return out;
948
993
  }
949
994
  function setHpf(tex, res, tex2) { _hpfTex = tex; _hpfRes = res|0; _hpfTex2 = tex2 || null; } // tex2 = RG8(temp,humid) pack (W12)
950
- return { get prog(){ return prog; }, render, probe, sampleGroundM, cullMatrix, recompile, setHpf, GRID, indexCount: indices.length, M4 };
995
+ return { get prog(){ return prog; }, render, checkGlError, probe, sampleGroundM, cullMatrix, recompile, setHpf, GRID, indexCount: indices.length, M4 };
951
996
  }
@@ -177,7 +177,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
177
177
  // count (orbit 860->20, lowalt 1272->328). 1.0 is the calibrated default; override live
178
178
  // via window.__splitFactor.
179
179
  const splitFactor = opts.splitFactor ?? 1.0;
180
- const gridMeshSize = opts.gridMeshSize || 16; // 24->16 FPS lever (sub-pixel over-tessellation; see gl-render.js GRID)
180
+ const gridMeshSize = opts.gridMeshSize || 11; // 16->11 FPS lever (triangle-throughput bound, not ALU; GRID 8 jagged biome crossovers, see gl-render.js GRID)
181
181
 
182
182
  gl.getExtension('EXT_color_buffer_float'); // RGBA32F atlas render targets
183
183
  // OES_texture_float_linear lets the driver LINEAR-filter the RGBA32F HPF/elevation textures. When
@@ -360,13 +360,15 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
360
360
  // there are no tiles to make resident, generate, or evict. _frameCache stays for the
361
361
  // static-camera re-render PERF path. frameStart kept (the render loop still references it).
362
362
  let _frameCache = null;
363
+ let _pipelineQuads = null; // pre-computed quads from last frame for draw-before-compute pipelining
364
+ let _glCheckTick = 0; // throttle the gl.getError() hard-sync to 1-in-30 frames (pipeline stall)
363
365
  let frameStart = 0;
364
366
 
365
367
  // ---- per-frame quadtree drive --------------------------------------------------
366
368
  // camWorldPos: [x,y,z] world meters (sphere centered at origin).
367
369
  // camTarget: [x,y,z] world look-at point. fovy in radians. displayMode int.
368
370
  // Returns { quadCount, glError, face }.
369
- function frame(camWorldPos, camTarget, fovy = 0.7, displayMode = 0, sunDir, time = 0, up) {
371
+ function frame(camWorldPos, camTarget, fovy = 0.7, displayMode = 0, sunDir, time = 0, up, surfElev = 0) {
370
372
  const sun = sunDir || (() => { const s = [0.4, 0.5, 0.75]; const sl = Math.hypot(...s); return [s[0]/sl, s[1]/sl, s[2]/sl]; })();
371
373
  // Use the caller's up if given (planet.html keeps an orthonormal free-fly up); only
372
374
  // fall back to [0,1,0] when none supplied. Hardcoding [0,1,0] broke the +Y pole view
@@ -399,8 +401,9 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
399
401
  || (fwd[0]*c.fwd[0]+fwd[1]*c.fwd[1]+fwd[2]*c.fwd[2]) < c.fwdLen2*0.99999
400
402
  || displayMode !== c.displayMode;
401
403
  if (!moved) {
402
- const cam2 = { eye: camWorldPos, center: camTarget, up: camUp, fovy, displayMode };
403
- const glError = render.render(c.quads, cam2, sun, time);
404
+ const cam2 = { eye: camWorldPos, center: camTarget, up: camUp, fovy, displayMode, surfElev };
405
+ render.render(c.quads, cam2, sun, time);
406
+ const glError = render.checkGlError();
404
407
  // Phase 2 fix: also re-draw vegetation on STATIC frames (the GPU instance buffer is
405
408
  // still valid from the last rebuild). Without this, trees vanished whenever the
406
409
  // camera held still (the static branch returned before the veg draw).
@@ -497,7 +500,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
497
500
  // ONLY to splitDist (NOT distF below): distF = sf*8.0 is the altitude-weighting term, and scaling sf
498
501
  // into BOTH compounds into ~2 levels at altitude (witnessed: base sf*2 gave mx 5->7 at 8000km). Keep
499
502
  // distF on the original sf so the push is a clean ONE step. maxLevel (16) is untouched -- no new LOD.
500
- const LOD_STEP = 2.3; // push ALL LODs ~0.6 step (reduced from 3.0 to target ~600 quads at all alts):
503
+ const LOD_STEP = 3.6; // 2.3->3.6; tightens the far-ring by reducing LOD near the horizon. NOTE: splitDist floors at 1.1 so deck quad count is set by splitFactor (0.28 ~= 500 quads), not this.
501
504
  qt.computeSplitDist(sf * LOD_STEP, gl.drawingBufferHeight || 480, fovy);
502
505
  // DETAIL-FARTHER (user 2026-06-01h: each LOD pop should happen ~3x farther out -- 6km detail at
503
506
  // 24km, etc.). A quad subdivides when camAlt < l * splitDist * distFactor, so tripling distFactor
@@ -646,7 +649,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
646
649
  let _cullDbgOnScreen = 0;
647
650
  // Use viewProjNoEye (proj*viewRel, NO translate) + pass the eye so quadOutsideFrustum can make
648
651
  // each corner camera-relative in JS doubles (fp32-precision fix for the ground-nadir blank).
649
- const _cm = cullActive ? render.cullMatrix({ eye: camWorldPos, center: camTarget, up: camUp, fovy }) : null;
652
+ const _cm = cullActive ? render.cullMatrix({ eye: camWorldPos, center: camTarget, up: camUp, fovy, surfElev }) : null;
650
653
  const vpr = _cm ? _cm.viewProjNoEye : null;
651
654
  // BEHIND-LIMB CULL (the dominant bottleneck fix). The baseline measured ~80% of all
652
655
  // generated quads sitting BEHIND the planet's horizon -- they are geometrically
@@ -671,8 +674,22 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
671
674
  // (5km), leaving 1968 quads with wastedFrac 0.87 (browser-5). Lower the guard to a tiny
672
675
  // epsilon above the surface; the slack term already keeps any limb-straddling quad.
673
676
  // (Eye exactly at/under the surface is degenerate -- cosHorizon would be >=1 -- so skip.)
677
+ // SCALED SLACK (2026-06-13): at low altitude the fixed CULL_MAX_ELEV=12km slack let all
678
+ // back-face quads through when the limb cull was disabled by the 127m guard; scale the
679
+ // elevation slack by altitude so the cull tightens naturally near the ground.
680
+ const altM = Math.max(0.0, camDist - R);
681
+ const elSlack = Math.min(CULL_MAX_ELEV, 200.0 + altM * 0.5);
674
682
  const limbCullActive = (typeof window !== 'undefined' && window.__limbCull != null ? !!window.__limbCull : true)
675
- && camDist > R * 1.00002;
683
+ && altM > 0.5;
684
+ // CPU/GPU PIPELINING (real overlap, 2026-06-14): issue LAST frame's cached quads to the GPU
685
+ // NOW, BEFORE the 6-face quadtree build below runs, so the GPU draw and this frame's CPU
686
+ // build actually overlap. Previously this draw sat AFTER the build loop (no overlap -- the
687
+ // CPU work had already finished before the draw was issued). cam carries THIS frame's view
688
+ // (1-frame geometry latency, standard pipelining). First frame (no cache) draws after build.
689
+ const cam = { eye: camWorldPos, center: camTarget, up: camUp, fovy, displayMode, surfElev };
690
+ if (_pipelineQuads) {
691
+ render.render(_pipelineQuads, cam, sun, time);
692
+ }
676
693
  for (let face = 0; face < 6; face++) {
677
694
  // LOD drive uses lodRefPos (aim-shifted, altitude preserved) so deepest LOD follows the
678
695
  // look point; the quad record keeps the TRUE-camera localCam (for the VS geomorph
@@ -711,7 +728,7 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
711
728
  // (browser-4726 wastedFrac 0.55) = the close-approach FPS sink. A quad above this tight
712
729
  // horizon is genuinely visible (kept); a quad below it is kept ONLY by the forward-cone
713
730
  // rescue, so sideways/backside near-horizon rings are culled.
714
- const slack = CULL_MAX_ELEV / R + (l / R);
731
+ const slack = elSlack / R + (l / R);
715
732
  if (dotOut < cosHorizon - slack) {
716
733
  // FRUSTUM RESCUE (user 2026-06-05: 'culling nearby quads unnecessarily making land
717
734
  // disappear'). The old forward-CONE rescue used a fixed ~53deg half-angle cone
@@ -759,9 +776,25 @@ export async function initMapspinnerPlanet(gl, opts = {}) {
759
776
  }
760
777
  }
761
778
 
762
- // render the combined visible leaf set across all faces.
763
- const cam = { eye: camWorldPos, center: camTarget, up: camUp, fovy, displayMode };
764
- const glError = render.render(quads, cam, sun, time);
779
+ // ===== CPU/GPU PIPELINING: draw-before-compute =====
780
+ // Phase 1 (GPU): draw cached quads from LAST frame's camera IMMEDIATELY so the GPU
781
+ // starts processing while the CPU computes THIS frame's quadtree. render.render()
782
+ // issues draw commands without gl.getError() so the GPU pipeline is not stalled.
783
+ // Phase 2 (CPU): the 6-face loop above already populated `quads` — this overlaps
784
+ // with GPU rendering of Phase 1. Phase 3: first frame (no cache) draws after compute.
785
+ // glError is checked via render.checkGlError() after Phase 2 for diagnostics.
786
+ // cam + the _pipelineQuads draw were HOISTED above the 6-face loop (real CPU/GPU overlap).
787
+ // First frame (no cache) has nothing to pre-draw, so it draws THIS frame's quads here.
788
+ if (!_pipelineQuads) {
789
+ render.render(quads, cam, sun, time);
790
+ }
791
+ // THROTTLED glError: gl.getError() is a hard client/server SYNC that drains the GL pipeline
792
+ // (the exact stall the draw-before-compute pattern exists to avoid). Probe once every 30
793
+ // frames (or when window.__glCheck forces it); assume 0 between probes. 2026-06-14.
794
+ _glCheckTick = (_glCheckTick + 1) % 30;
795
+ const _forceGlCheck = (typeof window !== 'undefined' && window.__glCheck);
796
+ const glError = (_glCheckTick === 0 || _forceGlCheck) ? render.checkGlError() : 0;
797
+ _pipelineQuads = quads;
765
798
  // CULL DEBUG stats for the live HUD: kept/culled counts + the false-cull signature
766
799
  // (culledOnScreen = frustum-culled quads that actually project inside the screen) + whether
767
800
  // this was a REBUILD frame (cull ran) vs a cached re-render. Read by planet.html's __cullHud.
@@ -147,10 +147,10 @@ const float LAKE_CARVE_DEPTH = 90.0; // metres of bowl depth at basin centre
147
147
  float lakeBasinField(vec3 dir){ return 0.5 + 0.5 * snoise3(dir * 55.0 + vec3(4.0, 9.0, 1.0)); }
148
148
  float lakeCarveM(vec3 dir, out float wet){
149
149
  float basin = lakeBasinField(dir);
150
- float shoulder = smoothstep(0.50, 0.74, basin); // eroded outwash apron (blends into terrain)
151
- float bowl = smoothstep(0.62, 0.80, basin); // deeper water body
152
- wet = smoothstep(0.64, 0.70, basin); // crisp shoreline of the open water
153
- return -LAKE_CARVE_DEPTH * (0.45 * shoulder + 0.55 * bowl);
150
+ float shoulder = smoothstep(0.30, 0.74, basin); // gentle outwash apron (blends far into terrain)
151
+ float bowl = smoothstep(0.50, 0.80, basin); // deeper basin starts earlier
152
+ wet = smoothstep(0.50, 0.70, basin); // softer shoreline transition
153
+ return -LAKE_CARVE_DEPTH * (0.65 * shoulder + 0.35 * bowl); // more apron, gentler banks
154
154
  }
155
155
  float lakeCarveM(vec3 dir){ float w; return lakeCarveM(dir, w); }
156
156
  // River and canyon are the SAME incision algorithm (a 4-octave ridged 1-abs(snoise3) network),
@@ -187,13 +187,13 @@ float inciseRidgeField(vec3 d, float baseFreq, float freqMul){
187
187
  return sum / norm; // ->1 on the channel network
188
188
  }
189
189
  const float RIVER_INCISE_DEPTH = 120.0; // metres at the channel thalweg
190
- float riverRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir), 320.0, 2.03); }
190
+ float riverRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir), 40.0, 2.03); }
191
191
  float riverCarveM(vec3 dir, out float wet){
192
192
  float ridge = riverRidgeField(dir);
193
- float valley = smoothstep(0.78, 0.94, ridge); // wide eroded valley sides
194
- float thalweg = smoothstep(0.88, 0.96, ridge); // deep channel core
195
- wet = smoothstep(0.90, 0.94, ridge); // flowing-water line
196
- return -RIVER_INCISE_DEPTH * (0.5 * valley + 0.5 * thalweg);
193
+ float valley = smoothstep(0.30, 0.94, ridge); // gentle eroded valley sides, start wider
194
+ float thalweg = smoothstep(0.75, 0.96, ridge); // deep channel core
195
+ wet = smoothstep(0.78, 0.94, ridge); // flowing-water line
196
+ return -RIVER_INCISE_DEPTH * (0.7 * valley + 0.3 * thalweg); // more valley, gentler banking
197
197
  }
198
198
  float riverCarveM(vec3 dir){ float w; return riverCarveM(dir, w); }
199
199
  const float CANYON_INCISE_DEPTH = 1400.0; // metres at the gorge floor (user 2026-06-02: deepen+widen so canyons visibly sculpt the elevation; was 480m = invisible vs multi-km relief)
@@ -206,7 +206,9 @@ uniform float uVertexAO; // per-vertex shading/AO strength lev
206
206
  // blended over the seabed). One program + a uniform flag = no second cold compile, no duplicated
207
207
  // per-frame uniform churn, and the branch is uniform-coherent (free on the GPU).
208
208
  uniform float uIsWater; // 0 = terrain pass, 1 = water-surface pass
209
+ uniform float uUnderwater; // 0 = camera above water, 1 = camera below sea level
209
210
  uniform float uBeachTopM; // beach ceiling (m): below this, grass/snow yield to sand (window.__beachTop; 30 default)
211
+ uniform float uBeachShelfM; // land coastal-shelf top (m): h<this is eased (h*h/S) so the coast rises gently from the waterline = wide beach (window.__beachShelf; 300 default). GEOMETRY (composeHeight+vH).
210
212
  uniform float uHiFreqCut; // hi-freq elevation-noise attenuation, applied to ALL hi-freq sources:
211
213
  // broadShapeM/MD fine octaves (o>=6) + vtxDisplace micro-relief
212
214
  // (window.__hiFreqCut; default 0.25 = the user's 4x reduction, 2026-06-06)
@@ -219,7 +221,7 @@ uniform float uMtnBandWide; // widen mtn=smoothstep(16.8,18.6,ele
219
221
  uniform float uClimateRelief; // widen wetLowFlat(0.66,0.9,humid) + coldFlat(0.18,0.34,temp) reliefMul gates
220
222
  uniform float uIsleWide; // widen isleZone seaBias gates (50,350)+(900,1600) -> (30,600)+(600,2200)
221
223
  uniform float uCarveWide; // widen the river/canyon/lake/dune CLIMATE gates so carve depth fades in over a wide span
222
- float canyonRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir) + vec3(13.7, -4.2, 8.9), 380.0, 2.07); } // phase offset matches FS canyonMask
224
+ float canyonRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir) + vec3(13.7, -4.2, 8.9), 96.0, 2.07); } // phase offset matches FS canyonMask. baseFreq 48->96 (user 2026-06-14: double canyon frequency = 2x finer/denser gorge network, more visible at the deck). Shared VS carve + FS mask -> congruent by construction.
223
225
  // CANYON cross-section now reads as a CANYON, not a V-notch: STEEP WALLS + a FLAT FLOOR.
224
226
  // wall = a sharp smoothstep band -> the carve drops fast over a narrow ridge interval (the cliff
225
227
  // walls), instead of the old gentle bench+gorge blend that made shallow V-troughs.
@@ -232,8 +234,8 @@ float canyonCarveM(vec3 dir, out float depth){
232
234
  // narrow ridge slice -> thin hairline gorges. Widen so the canyon reads as a BROAD gorge: a wide
233
235
  // eroded rim shoulder, a steep but visible wall, and a wide flat floor that the gorge bottoms out on.
234
236
  float bench = smoothstep(0.62, 0.78, ridge); // wide eroded rim shoulder
235
- float wall = smoothstep(0.78, 0.86, ridge); // STEEP cliff wall (wider band)
236
- float floorF= smoothstep(0.86, 0.93, ridge); // reach the flat gorge floor, then clamp
237
+ float wall = smoothstep(0.70, 0.86, ridge); // STEEP cliff wall (wider band)
238
+ float floorF= smoothstep(0.80, 0.93, ridge); // reach the flat gorge floor, then clamp
237
239
  depth = max(wall, floorF);
238
240
  // 0.18 rim shoulder + 0.82 wall-to-floor; floorF clamps so the bottom is flat (no infinite V).
239
241
  float profile = 0.18 * bench + 0.82 * max(wall, floorF);
@@ -246,13 +248,16 @@ float canyonCarveM(vec3 dir, out float depth){
246
248
  // 100m scale the low-altitude camera sees. Pure world-dir (LOD-invariant); the mesh resolves them
247
249
  // at maxLevel 16 (~7m cells). Each gully octave = a thinned ridged field, depth scaled to its wl.
248
250
  vec3 dn = normalize(dir);
249
- float g1 = inciseRidgeField(dn + vec3(5.1, 8.3, -2.7), 3500.0, 2.11); // ~10km tributaries
250
- float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2), 14000.0, 2.05); // ~2.5km gullies
251
- // HIGH-FREQ fractal depth x0.65 (user 2026-06-11 'leave the low frequency canyons deep, only
252
- // reduce the high frequency fractals, bring it back 30%' -- halved, then +30% restored): the
253
- // main 100km gorge network keeps full CANYON_INCISE_DEPTH; only these tributary octaves shrink.
254
- carve += -104.0 * dmul * smoothstep(0.78, 0.93, g1); // ~10km tributaries (160m * 0.65)
255
- carve += -39.0 * dmul * smoothstep(0.80, 0.95, g2); // ~2.5km gullies (60m * 0.65)
251
+ float g1 = inciseRidgeField(dn + vec3(5.1, 8.3, -2.7), 219.0, 2.11); // ~40km tributaries (was 875->438)
252
+ float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2), 875.0, 2.05); // ~10km gullies (was 3500->1750)
253
+ float g3 = inciseRidgeField(dn + vec3(2.2, -4.1, 8.8), 1750.0, 2.02); // ~1.2km branching ravines (deck scale)
254
+ // HIGH-FREQ fractal depth (user 2026-06-11 'leave the low frequency canyons deep, only reduce the
255
+ // high frequency fractals'). DEEPENED + sharpened 2026-06-14 (user: mountains need visible canyons):
256
+ // the main 100km gorge network keeps full CANYON_INCISE_DEPTH; the tributary octaves incise deeper
257
+ // with narrower walls so ravines read at the deck. g3 subdivides the bigger gullies at maxLevel.
258
+ carve += -450.0 * dmul * smoothstep(0.66, 0.90, g1); // ~10km tributaries (deeper, narrower)
259
+ carve += -200.0 * dmul * smoothstep(0.70, 0.92, g2); // ~2.5km gullies
260
+ carve += -90.0 * dmul * smoothstep(0.72, 0.93, g3); // ~1.2km branching ravines
256
261
  return carve;
257
262
  }
258
263
  float canyonCarveM(vec3 dir){ float dd; return canyonCarveM(dir, dd); }
@@ -338,10 +343,10 @@ float duneFieldM(vec3 dir, out float crest){
338
343
  highp float broadShapeLowM(vec3 dir){ // W7: metres (~13000) + freq (~49152) accumulators are highp islands
339
344
  if (hasHpf == 0) return 0.0;
340
345
  vec3 d = normalize(dir);
341
- highp float amp = 6500.0, freq = 3.0, sum = 0.0;
346
+ highp float amp = 6500.0, freq = 0.75, sum = 0.0;
342
347
  int blOcts = (uBroadLowOcts > 0) ? uBroadLowOcts : 8;
343
348
  for (int o=0; o<blOcts; o++){ sum += amp * snoise3(d*freq); amp *= (o < 6 ? 0.66 : 0.82); freq *= 2.0; }
344
- return sum - 900.0;
349
+ return sum - 500.0;
345
350
  }
346
351
  // ---- SHARED micro-relief helpers (MOVED to the common preamble, THC-Normal W1, so composeHeight()
347
352
  // in the VS/PROBE region can call them). vtxDetail = the micro-relief A/B global; vnoise2
@@ -382,13 +387,14 @@ highp vec2 faceWarp(highp vec2 p){ return defRadius * tan((p / defRadius) * 0.78
382
387
  // PERLIN-EVERYWHERE lever -- shared by the composeHeight elevation term (VS/PROBE) and the FS albedo
383
388
  // overlay, so it must be declared in ALL stages (outside the VS/PROBE guard below).
384
389
  uniform float uDetailOverlay; // amplitude lever (user-tuned 6; 0 = off; __detailOverlay)
390
+ uniform float uGrid; // interior cells per tile edge (GRID=16); for mesh-based vertex normals
385
391
  uniform float uNrmStepM; // lit-normal FD step in metres (150); uniform-fed to defeat FXC constant folding
386
392
  #if defined(_VERTEX_) || defined(_PROBE_)
387
393
  highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: returns metres (~13000) -> highp
388
394
  if (hasHpf == 0) return 0.0;
389
395
  float mtnAmp = 1.0; // mountain-amplitude (was a window.__mtnAmp uniform; inlined at neutral 1.0)
390
396
  vec3 d = normalize(dir);
391
- highp float amp = 6500.0, freq = 3.0, sum = 0.0; // W7 highp ISLAND: amp/freq(~49152)/sum(~13000) overflow fp16
397
+ highp float amp = 6500.0, freq = 0.75, sum = 0.0; // W7 highp ISLAND: amp/freq(~49152)/sum(~13000) overflow fp16
392
398
  int octMax = (uOctMax > 0) ? uOctMax : 12; // runtime bound (FXC unroll-defeat); 12 when unset
393
399
  for (int o=0; o<octMax; o++){ // W9: 14->12 octaves, drop the finest 2 (wavelengths <2km) UNCONDITIONALLY
394
400
  // FINE-OCTAVE 5x FREQ (user 2026-06-06: the high-freq normals-affecting GROUND bump should be 5x
@@ -417,19 +423,8 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
417
423
  sum += amp * nn;
418
424
  amp *= (o < 6 ? 0.66 : 0.80); freq *= 2.0; // macro decay 0.66; fine-octave decay 0.74->0.80 (widen, 2026-06-08)
419
425
  }
420
- // LOW-ELEVATION COMPRESSION CURVE (gamma 2.5): power-curve the positive land height only so
421
- // low/mid land sinks toward a flatter base while peaks keep their range; sea sign untouched.
422
- {
423
- const float HREF = 6500.0, GAMMA = 2.5;
424
- highp float e0 = sum - 900.0; // W7: metres
425
- if (e0 > 0.0) {
426
- highp float ec = HREF * pow(min(e0, HREF) / HREF, GAMMA);
427
- if (e0 > HREF) ec += (e0 - HREF);
428
- sum = 900.0 + ec;
429
- }
430
- }
431
426
  // PEAK-LIFT, MOUNTAIN-BELT GATED: lift the high end so ridges become tall peaks; plains flat.
432
- highp float hi = max(sum - 900.0, 0.0); // W7: metres
427
+ highp float hi = max(sum - 500.0, 0.0); // W7: metres
433
428
  float peak = smoothstep(1200.0, 5000.0, hi);
434
429
  float liftK = 0.06 + 0.16 * clamp(reliefMul, 0.0, 1.7); // 2x LESS elevated (user 2026-06-10: halved 0.12+0.32 -> 0.06+0.16)
435
430
  sum += hi * peak * liftK;
@@ -439,8 +434,8 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
439
434
  if (belt > 0.0 && hi > 0.0) {
440
435
  // (massif envelope freqs restored to 4.0/7.0 -- they set continental belt PLACEMENT, not the visible
441
436
  // ridge spacing; the 3x-wider knob is the o>=6 fine band above, not this envelope.)
442
- float e0 = snoise3(d * 4.0 + vec3(11.0, 3.0, 7.0)) * 0.5 + 0.5;
443
- float e1 = snoise3(d * 7.0 + vec3(2.0, 9.0, 4.0)) * 0.5 + 0.5;
437
+ float e0 = snoise3(d * 2.0 + vec3(11.0, 3.0, 7.0)) * 0.5 + 0.5;
438
+ float e1 = snoise3(d * 3.5 + vec3(2.0, 9.0, 4.0)) * 0.5 + 0.5;
444
439
  float modu = 0.7 + 0.3 * clamp(e0 * 0.7 + e1 * 0.3, 0.0, 1.0);
445
440
  float landGate = smoothstep(0.0, 800.0, hi);
446
441
  float base = belt * landGate * modu;
@@ -452,14 +447,14 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
452
447
  // conflated the braided TEXTURE patches (rockSlope breakup noise, since deleted) with real slope;
453
448
  // with the braids, the dark raw-photo far field, and the UDN normal-frame bug all fixed, the
454
449
  // original steep terrain stands.
455
- highp float pf = 200.0; float pa = 1.0, ps = 0.0, pn = 0.0; // W7: pf (~4100) feeds the noise lattice -> highp
450
+ highp float pf = 50.0; float pa = 1.0, ps = 0.0, pn = 0.0; // W7: pf feeds the noise lattice -> highp. pf 100->50 (user 2026-06-14: halve the mountain frequency = 2x wider peak massifs)
456
451
  int pkOcts = (uPeakOcts > 0) ? uPeakOcts : 3;
457
452
  for (int o = 0; o < pkOcts; o++) {
458
453
  ps += pa * (1.0 - abs(snoise3(d * pf + vec3(3.3, 7.7, 1.1))));
459
454
  pn += pa; pa *= 0.65; pf *= 2.13;
460
455
  }
461
456
  float crest = ps / pn;
462
- sum += base * pow(crest, 4.5) * 8400.0 * mtnAmp; // 2x LESS elevated (user 2026-06-10: halved 16800 -> 8400); sharper eroded tips (pow 4.5) kept
457
+ sum += base * pow(crest, 4.5) * 8400.0 * mtnAmp; // pow 4.5: sharp steep peaks that engage the rock-face slope gate (3.5 rounded them flat -- user 2026-06-14 regression revert)
463
458
  }
464
459
  }
465
460
  // SOFT ELEVATION CEILING (mob-w8-ceiling 2026-06-08: 'peaks come to points, no flat plateau'). Even at
@@ -469,13 +464,13 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
469
464
  // asymptoting to a ceiling -- and lift CMAX to 15000 so the asymptote sits well above any real peak. With
470
465
  // the wider knee the high band no longer collapses, so the *1.15 scale-up crutch is DROPPED.
471
466
  {
472
- highp float e = sum - 900.0; // W7: metres
473
- // GLOBAL ELEVATION SCALE 0.65 (user 2026-06-10). 0.343 over-squashed the field (45.9% land <500m, p50
467
+ highp float e = sum - 500.0; // W7: metres
468
+ // GLOBAL ELEVATION SCALE 0.85 (user 2026-06-13).
474
469
  // 723m, relief only ~4km -> mountains invisible/clamped-looking, NOT a top clip). 0.65 expands the
475
470
  // range so mountains stand out: p50 ~1370m, p99 ~8920m, MAX ~11600m, relief ~7.5km. Scales the WHOLE
476
471
  // relief uniformly (base octaves + belt massif + peaks) on the excess-over-900. Max 11.6km still well
477
472
  // under the ceiling (CK 6500 / CMAX 42000, knee /26000) so no clip.
478
- e *= 0.65;
473
+ e *= 0.85;
479
474
  // CEILING RAISED + KNEE WIDENED for the 4x mountaintops (user 2026-06-09: 'must not hit the ceiling
480
475
  // anywhere, run under the normal max + elevate the maximum'). MEASURED 4x-hf+4x-mtn pre-ceiling max
481
476
  // = 28550m (p99.9 21457m); CMAX 42000 sits 1.47x above it = no peak ever approaches the asymptote, and
@@ -548,9 +543,10 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
548
543
  // amplitude than the base bump (ea0 5m vs 8m) per 'a little less intensity'. 4-octave ridged-leaning fBm
549
544
  // for erosive gully/ridgeline shape. Added INSIDE vtxDisplace so it shares the LOD-invariant, seam-safe,
550
545
  // face-local path (no per-patch step) and is picked up by the composeHeight central-diff lit normal.
551
- float mtnGate = smoothstep(0.55, 1.0, rugged); // ramps in over the mountain belt (rugged ~ elevAmp)
546
+ float mtnGate = smoothstep(0.30, 0.70, rugged); // ramps in as soon as the massif lifts so ALL mountains get erosive perlin (user 2026-06-14); was (0.40,0.85)
547
+ float eros = 0.0; // mountain erosive relief, kept SEPARATE from the uHiFreqCut trim (see return)
552
548
  if (mtnGate > 0.0) {
553
- float ea = 2.5, ewl = 2880.0, esumF = 0.0, esumR = 0.0; // user 2026-06-09: HALF the erosion elevation (5->2.5m) + 3x WIDER again (960->2880m)
549
+ float ea = 8.0, ewl = 1440.0, esumF = 0.0, esumR = 0.0; // ea 4->8 + ewl 1920->1440: stronger, sharper rugged ravine relief so mountains read ROCKY (engage the rockSlope gate), not round/smooth (user 2026-06-14). Decoupled from uHiFreqCut so it lands full-strength.
554
550
  int veOcts = (uVtxErodeOcts > 0) ? uVtxErodeOcts : 4;
555
551
  for (int o = 0; o < veOcts; o++) {
556
552
  float en = vnoise2(fp / ewl);
@@ -559,10 +555,13 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
559
555
  esumR += ea * (er * 2.0 - 1.0);
560
556
  ea *= 0.55; ewl *= 0.5;
561
557
  }
562
- sum += mix(esumF, esumR * 0.9, 0.6) * mtnGate; // ridged-leaning erosive relief, mountain-gated
558
+ eros = mix(esumF, esumR * 0.9, 0.6) * mtnGate; // ridged-leaning erosive relief, mountain-gated
563
559
  }
564
560
  float ruggedAmp = clamp(rugged, 0.4, 1.15);
565
- return sum * vtxDetail * ruggedAmp * uHiFreqCut; // 4x high-freq cut (uHiFreqCut default 0.25)
561
+ // DECOUPLE (user 2026-06-14, workflow-found root cause): the everywhere-bump `sum` still rides the
562
+ // uHiFreqCut altitude-blotch trim (0.25), but the mountain `eros` does NOT -- it was being silently
563
+ // quartered (6.0->1.5m), which is why mountains read as smooth grass. eros now lands full-strength.
564
+ return (sum * uHiFreqCut + eros) * vtxDetail * ruggedAmp;
566
565
  }
567
566
 
568
567
  // PERLIN-EVERYWHERE detail fbm (user 2026-06-10): ONE 3-octave value fbm shared by the FS albedo
@@ -570,7 +569,7 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
570
569
  // relief variation correlate, reading as one landform). World-dir keyed, seam-safe.
571
570
  highp float detailFbm(vec3 dir) {
572
571
  float ov = 0.0, oa = 0.0;
573
- float fq = 150.0, am = 1.0;
572
+ float fq = 75.0, am = 1.0;
574
573
  int dfOcts = (uDetailFbmOcts > 0) ? uDetailFbmOcts : 3;
575
574
  for (int o = 0; o < dfOcts; o++) {
576
575
  ov += am * snoise3(dir * fq + vec3(float(o) * 7.3));
@@ -601,7 +600,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
601
600
  // latitude/moisture gradients instead of a per-cell contour (wrxo0rr7a coldFlat/wetLowFlat 80m+).
602
601
  float wetLowFlat = smoothstep(mix(0.66, 0.50, uClimateRelief), mix(0.9, 1.0, uClimateRelief), bHum) * (1.0 - mtn);
603
602
  float coldFlat = (1.0 - smoothstep(mix(0.18, 0.05, uClimateRelief), mix(0.34, 0.45, uClimateRelief), bTemp));
604
- float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.15, 1.7);
603
+ float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.40, 1.7);
605
604
  float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0);
606
605
  // ISLAND-TYPE VARIETY (pure fn of world dir) -- mirror of the VS main isle block.
607
606
  // uIsleWide=1 spreads the seaBias double-gate so the volcanic/atoll remix ramps in (wrxo0rr7a 2000m).
@@ -625,6 +624,16 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
625
624
  if (h < 0.0) {
626
625
  highp float d = -h;
627
626
  h = -(min(d, 500.0) * 0.24 + max(d - 500.0, 0.0) * 1.19);
627
+ h = max(h, -11000.0); // cap depth at Mariana Trench (~11km)
628
+ } else {
629
+ // LAND COASTAL SHELF (user 2026-06-14: 'beaches not wide enough'): the underwater shelf above
630
+ // gives a gentle seabed; mirror it on the LAND side so the coast rises GENTLY from the waterline
631
+ // = a wide beach. h*h/S is flat at the waterline (derivative 0) and identity at h=S, so high land
632
+ // is unchanged (no drowning) and only the low coastal band is stretched horizontally. Pure fn of
633
+ // the field -> seam-safe + the collision probe shares it. GUARD: a stale/unset uniform (0) would
634
+ // disable the shelf -> default 600 so it always applies. window.__beachShelf dials S live.
635
+ highp float bShelf = uBeachShelfM > 1.0 ? uBeachShelfM : 600.0;
636
+ if (h < bShelf) h = h * h / bShelf;
628
637
  }
629
638
  // displacement now continues UNDERWATER (the old land-only gate served the flat-clamped ocean,
630
639
  // gone since 026d530): the seabed carries the same micro-relief as land = realistic continuation.
@@ -633,17 +642,19 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
633
642
  // the FS albedo overlay shows, as real relief (~30m per lever unit -> ~180m at the user-tuned 6).
634
643
  // Shore-gated (fades in over the first 250m of land) so the coastline and the flat water planes are
635
644
  // untouched and no noise islets pop offshore. The VS FD lit-normal picks it up automatically.
636
- h += detailFbm(dir0) * uDetailOverlay * 30.0 * smoothstep(0.0, 250.0, h);
637
- // FLAT-AREA INTEREST NOISE (user 2026-06-12): gentle rolling relief on the least noisy
638
- // anchorpoints (low reliefMul = flat plains, non-mtn, non-wet, non-cold). Fades to zero
639
- // by reliefMul ~0.5 so mountains are unaffected.
645
+ h += detailFbm(dir0) * uDetailOverlay * 30.0 * step(0.0, h);
646
+ // FLAT-AREA VALLEY NETWORKS + LAKES (user 2026-06-13): incised valley systems in low-relief
647
+ // plains. Replaces the old noise bumps with a ridge-field valley network for connected
648
+ // linear depressions and lakes that fill the valley bottoms. Fades to zero by reliefMul ~0.5.
640
649
  float flatGate = max(0.0, 1.0 - reliefMul * 2.0);
641
- h += snoise3(dir0 * 1400.0) * flatGate * uDetailOverlay * 35.0;
650
+ float valleyV = 1.0 - inciseRidgeField(dir0, 31.0, 2.0);
651
+ float valleyVal = smoothstep(0.25, 0.80, valleyV) * flatGate * uDetailOverlay * 24.0;
652
+ h -= valleyVal;
642
653
  // LAKE CARVE + flat-water plane
643
654
  float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
644
- // uCarveWide=1 widens the carve CLIMATE gates so the gorge/lake/dune depth fades in over a wide
645
- // anchor span instead of snapping along a thin climate contour (wrxo0rr7a carve cluster 90-1400m).
646
- float lakeGate = smoothstep(mix(0.60, 0.50, uCarveWide), mix(0.85, 0.95, uCarveWide), hpf0.a) * step(0.0, h);
655
+ // Flat-area lakes: lower humidity gate so plains valleys fill with lakes
656
+ float lakeGateLo = mix(0.60, 0.50, uCarveWide) - 0.12 * flatGate;
657
+ float lakeGate = smoothstep(lakeGateLo, mix(0.85, 0.95, uCarveWide), hpf0.a) * step(0.0, h);
647
658
  float lakeCarveV = lakeCarveRaw * lakeGate;
648
659
  h += lakeCarveV;
649
660
  float lakeWet = lakeWetV * lakeGate;
@@ -651,8 +662,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
651
662
  // RIVER + CANYON incision (clamped so coastal gorges never punch fake inland seas)
652
663
  float riverWet = smoothstep(mix(0.30, 0.20, uCarveWide), mix(0.55, 0.65, uCarveWide), hpf0.a) * smoothstep(mix(0.20, 0.12, uCarveWide), mix(0.34, 0.46, uCarveWide), hpf0.b);
653
664
  float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, h);
654
- float canyonArid = (1.0 - smoothstep(mix(0.40, 0.30, uCarveWide), mix(0.58, 0.68, uCarveWide), hpf0.a)) * smoothstep(mix(0.38, 0.28, uCarveWide), mix(0.56, 0.66, uCarveWide), hpf0.b) * smoothstep(60.0, 200.0, h);
655
- float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, h);
665
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * step(0.0, h);
656
666
  float inciseTot = riverCarveV + canyonCarveV;
657
667
  // min(...,0): the floor term goes POSITIVE for any h below -60 and was LIFTING the entire ocean
658
668
  // floor to exactly -60m -- the whole seabed was a uniform pan (root of 'depth under the water
@@ -661,7 +671,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
661
671
  inciseTot = max(inciseTot, min(-60.0 - h, 0.0)); // land: h + inciseTot >= -60m; ocean: untouched
662
672
  h += inciseTot;
663
673
  // CLIFF TERRACING (mesa/butte benches) -- after carves so canyon walls + risers compose
664
- float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, h, cliffFaceMask) * canyonArid * step(0.0, h);
674
+ float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, h, cliffFaceMask) * step(0.0, h);
665
675
  h += cliffCarveV;
666
676
  // FLAT RIVER WATER
667
677
  float riverWetLine = riverWetMask * riverWet * step(0.0, h);
@@ -793,7 +803,7 @@ void main() {
793
803
  float mtn = smoothstep(16.8, 18.6, bAmp); // mountain-belt weight 0..1
794
804
  float wetLowFlat = smoothstep(0.66, 0.9, bHum) * (1.0 - mtn); // swamp/wetland -> flat
795
805
  float coldFlat = (1.0 - smoothstep(0.18, 0.34, bTemp)); // tundra/ice -> flat
796
- float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.15, 1.7);
806
+ float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.40, 1.7);
797
807
  float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0); // ridged crests only in mountain belts
798
808
  // ISLAND-TYPE VARIETY: small offshore swells get a per-region type (volcanic cone / low atoll)
799
809
  // from a world-dir noise so islands are not all scaled continents. Pure fn of world dir.
@@ -816,13 +826,24 @@ void main() {
816
826
  if (vH < 0.0) {
817
827
  highp float dSea = -vH;
818
828
  vH = -(min(dSea, 500.0) * 0.24 + max(dSea - 500.0, 0.0) * 1.19);
829
+ vH = max(vH, -11000.0); // cap depth at Mariana Trench (~11km)
830
+ } else {
831
+ highp float bShelf = uBeachShelfM > 1.0 ? uBeachShelfM : 600.0; // LAND COASTAL SHELF -- mirror composeHeight exactly (wide beach, user 2026-06-14); guard stale/unset uniform
832
+ if (vH < bShelf) vH = vH * vH / bShelf;
819
833
  }
820
834
  vH += vDisp;
835
+ vH += detailFbm(dir0) * uDetailOverlay * 30.0 * step(0.0, vH);
836
+ // FLAT-AREA VALLEY NETWORKS + LAKES (user 2026-06-13): incised valley systems in low-relief
837
+ // plains -- must mirror composeHeight for FS material consistency.
838
+ float flatGate = max(0.0, 1.0 - reliefMul * 2.0);
839
+ float valleyV = 1.0 - inciseRidgeField(dir0, 31.0, 2.0);
840
+ float valleyVal = smoothstep(0.25, 0.80, valleyV) * flatGate * uDetailOverlay * 24.0;
841
+ vH -= valleyVal;
821
842
  // LAKE CARVE: carve a real basin into the elevation on WET LAND so lakes are part of the
822
- // fractal (no fade-in). Gated to land (vH>0) + wet regions (humid) so it does not pit deserts
823
- // or deepen the ocean. World-dir field -> identical at every LOD.
843
+ // fractal (no fade-in). Flat-area gate widened so plains valleys fill with lakes.
824
844
  float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
825
- float lakeGate = smoothstep(0.60, 0.85, hpf0.a) * step(0.0, vH);
845
+ float lakeGateLo = 0.60 - 0.12 * flatGate;
846
+ float lakeGate = smoothstep(lakeGateLo, 0.85, hpf0.a) * step(0.0, vH);
826
847
  float lakeCarveV = lakeCarveRaw * lakeGate;
827
848
  vH += lakeCarveV;
828
849
  // FLAT WATER: inside the wet core the surface must read as flat water (user: 'water should be
@@ -846,8 +867,7 @@ void main() {
846
867
  // by the small depths + land gate; coastline hypso re-witnessed in incision-hypso-landfrac-gate).
847
868
  float riverWet = smoothstep(0.30, 0.55, hpf0.a) * smoothstep(0.20, 0.34, hpf0.b); // moist, not frozen
848
869
  float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, vH);
849
- float canyonArid = (1.0 - smoothstep(0.40, 0.58, hpf0.a)) * smoothstep(0.38, 0.56, hpf0.b) * smoothstep(60.0, 200.0, vH);
850
- float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, vH);
870
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * step(0.0, vH);
851
871
  // NO-SUB-SEA-COAST GUARD (user 2026-06-02 deepen+widen): with the deepened canyon (-1400m + gullies)
852
872
  // a gorge on 200m coastal land would punch vH to ~-1200m = fake inland seas. Clamp the TOTAL incision
853
873
  // so post-carve land bottoms out at a small floor (-60m: a gorge may reach near sea level but never
@@ -861,7 +881,7 @@ void main() {
861
881
  // arid badlands look). The snap delta is added to vH; cliffFaceMask (->1 on a riser face) goes to
862
882
  // the FS for strata banding + steep-rock material. Applied AFTER carves so canyon walls + cliff
863
883
  // risers compose. step(0,vH) keeps it on land.
864
- float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, vH, cliffFaceMask) * canyonArid * step(0.0, vH);
884
+ float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, vH, cliffFaceMask) * step(0.0, vH);
865
885
  vH += cliffCarveV;
866
886
  // FLAT RIVER WATER (user: 'rivers/lakes should NOT be bumpy'): like lakes, the river thalweg
867
887
  // surface must read as flat water, not the bumpy micro-relief floor. Pin vH down to the channel
@@ -919,22 +939,48 @@ void main() {
919
939
  // ALL THREE taps through ONE runtime-bounded loop forces FXC to emit a SINGLE composeHeight instance,
920
940
  // so whatever approximations it picks cancel exactly in the differences. uNrmStepM>0.0 keeps the
921
941
  // bound non-constant (same defeat class as uOctMax).
922
- highp vec3 uzFD = dir0;
923
- vec3 refAxisFD = (abs(uzFD.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
924
- vec3 uxFD = normalize(cross(refAxisFD, uzFD));
925
- vec3 uyFD = cross(uzFD, uxFD);
926
- highp float nStepFD = (uNrmStepM > 0.0) ? uNrmStepM : 150.0;
927
- highp float hFD0 = 0.0, hFD1 = 0.0, hFD2 = 0.0;
942
+ // VERTEX NORMALS: interior vertices use the mesh-based edge cross product (ordinary
943
+ // face normals). Tile-edge vertices use the tangent-frame finite difference so both
944
+ // sides of a seam share the same dir0/tangent frame -> consistent normals, no row artifact.
945
+ highp float hN0 = 0.0;
946
+ highp vec3 vN = dir0;
928
947
  if (uIsWater < 0.5) {
929
- int fdIters = (uNrmStepM >= 0.0) ? 3 : 1; // always 3; non-constant so FXC cannot unroll/split
948
+ hN0 = composeHeight(dir0, faceLocal, defOffset.z); // center = the geometry height h
949
+ // VERTEX NORMAL = CENTRAL DIFFERENCE in PARAMETRIC MESH SPACE over the FULL composeHeight (2026-06-14
950
+ // jagged-normal fix). Two earlier methods both jagged: (a) interior FORWARD mesh-cell cross product
951
+ // = each vertex got its forward triangle's FACE normal (faceted) at a vertex-spacing step (noisy);
952
+ // (b) the tangent-frame FD passed the SAME faceLocal to every tap, so vtxDisplace CANCELLED in the
953
+ // differences -> the normal ignored the bumps the geometry has -> smooth normal on a bumpy mesh =
954
+ // facets show through. THIS evaluates the full field (incl vtxDisplace) at +/-du/+/-dv in parametric
955
+ // space (faceWarp gives BOTH the dir and faceLocal at each offset, so vtxDisplace varies correctly)
956
+ // and takes a CENTRAL (symmetric) world-space cross product = smooth AND matches the displaced
957
+ // surface. ONE formula for every vert (no interior/edge split = no discontinuity); seam-safe because
958
+ // adjacent same-LOD tiles sample the identical world offsets (the field is a pure fn of world pos).
959
+ // NORMAL SMOOTHING (user 2026-06-14: 'angular normals = no vertex smoothing'): low-pass the
960
+ // per-vertex normal by taking the central difference over a fixed ~METRIC step (uNrmStepM, ~300m)
961
+ // instead of ~1 mesh cell. A cell-sized step samples the high-freq erosion/canyon relief so the
962
+ // normal swings sharply vertex-to-vertex = angular; a fixed larger step averages it = adjacent
963
+ // verts vary smoothly = smooth shading, at any GRID. duP = stepM / tile-span (defOffset.z), clamped
964
+ // so it never drops below the mesh cell (would re-alias) nor exceeds ~1/3 the tile.
965
+ highp float nStepM = (uNrmStepM > 0.0) ? uNrmStepM : 300.0;
966
+ highp float duP = clamp(nStepM / max(defOffset.z, 1.0), 1.0 / ((uGrid > 0.0) ? uGrid : 16.0), 0.34);
967
+ highp float hPU = 0.0, hMU = 0.0, hPV = 0.0, hMV = 0.0;
968
+ highp vec3 dPU = dir0, dMU = dir0, dPV = dir0, dMV = dir0;
969
+ int fdIters = (uGrid >= 0.0) ? 4 : 1; // 4 offset taps; runtime-bounded (FXC unroll-defeat, see uOctMax)
930
970
  for (int i = 0; i < fdIters; i++) {
931
- highp vec3 dd = (i == 0) ? dir0
932
- : normalize(uzFD + ((i == 1) ? uxFD : uyFD) * (nStepFD / defRadius));
933
- highp float hi = composeHeight(dd, faceLocal, defOffset.z);
934
- if (i == 0) hFD0 = hi; else if (i == 1) hFD1 = hi; else hFD2 = hi;
971
+ highp vec2 off = (i == 0) ? vec2(duP, 0.0) : (i == 1) ? vec2(-duP, 0.0) : (i == 2) ? vec2(0.0, duP) : vec2(0.0, -duP);
972
+ highp vec2 fl = faceWarp((vertex.xy + off) * defOffset.z + defOffset.xy);
973
+ highp vec3 dd = normalize(defLocalToWorld * vec3(fl, defRadius));
974
+ highp float hh = composeHeight(dd, fl, defOffset.z);
975
+ if (i == 0) { hPU = hh; dPU = dd; } else if (i == 1) { hMU = hh; dMU = dd; }
976
+ else if (i == 2) { hPV = hh; dPV = dd; } else { hMV = hh; dMV = dd; }
935
977
  }
978
+ highp vec3 wPU = dPU * (defRadius + hPU), wMU = dMU * (defRadius + hMU);
979
+ highp vec3 wPV = dPV * (defRadius + hPV), wMV = dMV * (defRadius + hMV);
980
+ vN = normalize(cross(wPU - wMU, wPV - wMV));
981
+ if (dot(vN, dir0) < 0.0) vN = -vN; // keep it outward (terrain has no overhangs)
936
982
  }
937
- highp float h = hFD0;
983
+ highp float h = hN0;
938
984
  // OCEAN TOP = A SEPARATE, ELEVATION-BASED SURFACE (user 2026-06-10: 'terrain should extend into
939
985
  // the ocean, the ocean top separate and elevation based'). composeHeight keeps carrying the TRUE
940
986
  // signed bathymetry (vH -> the FS depth tint/Beer-Lambert keys on it), but the RENDERED surface
@@ -948,34 +994,8 @@ void main() {
948
994
  // and shades it as the animated ocean, alpha-blended over the seabed (depth test keeps it
949
995
  // behind land).
950
996
  highp float hR = (uIsWater > 0.5) ? 0.0 : h;
951
- // W8 ANALYTIC LIT NORMAL (P1 data-first): the normal is a property of the height FIELD, so derive it
952
- // from the field, not from per-pixel screen derivatives. FIXED-WORLD-STEP central difference of
953
- // composeHeight in the radial tangent frame -- STABLE (does NOT shrink with distance/per-cell -> no
954
- // shimmer, see :769), full landform relief at ALL distances, no fade. Mirror of the proven nBand
955
- // pattern but the FULL composeHeight (not macro-only broadShapeLowM). Offset ONLY dir0 (the world-dir
956
- // gradient = the seam-safe landform relief); hold faceLocal/tileM fixed so the tile-local vtxDisplace
957
- // term cancels in the difference (it carries no world-continuous gradient and would only add jitter).
958
- vec3 uzN = dir0; // dir0 is already normalized; radial up
959
- vec3 refAxisN = (abs(uzN.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
960
- vec3 uxN = normalize(cross(refAxisN, uzN));
961
- vec3 uyN = cross(uzN, uxN);
962
- // FXC FOLD-DEFEAT (2026-06-12, same disease as the uOctMax unroll fix): with a LITERAL 150.0 the
963
- // d3d11/FXC translator constant-folds/reassociates the tiny FD offset (150/R = 2.4e-5) feeding
964
- // normalize(uzN + uxN*eps) -- the perturbed-direction taps then diverge wildly from h on FLAT
965
- // ground (witnessed: probe slope 0.000 + flat wireframe mesh under whole regions the FS painted
966
- // as rock; the FD normal claimed steep in noise-keyed PATCHES = 'rock without slope angle').
967
- // A uniform-fed step denies FXC the constant, exactly like uOctMax denies it the loop bound.
968
- highp float nStep = (uNrmStepM > 0.0) ? uNrmStepM : 150.0; // STABLE world step (m): > finest vtxDisplace octave (~20m, no alias),
969
- // < carve band (~100m+, resolves canyons/cliffs). NOT pxWorld-scaled.
970
- if (uIsWater > 0.5) {
971
- vNrm = uzN; // water surface: radial normal; waves perturb it per-pixel in the FS (skips 2 composeHeight taps)
972
- } else {
973
- // taps come from the SINGLE-INSTANCE FD loop above (hFD0/1/2) -- never call composeHeight at
974
- // separate call sites for the FD again; per-callsite FXC codegen divergence was the AMD root.
975
- float dHduN = float(hFD1 - h) / nStep;
976
- float dHdvN = float(hFD2 - h) / nStep;
977
- vNrm = normalize(uxN * (-dHduN) + uyN * (-dHdvN) + uzN); // n = normalize([-dz/du,-dz/dv,1]) in the (ux,uy,uz) frame
978
- }
997
+ // VERTEX NORMAL: central-difference (computed above) for land; radial for water.
998
+ vNrm = (uIsWater > 0.5) ? dir0 : vN;
979
999
  // DIRECT per-vertex sphere projection (replaces the old corner-blend deform, which
980
1000
  // bilinearly interpolated 4 deformed corners -> FLAT quad interior -> faceted at high GRID).
981
1001
  // dir0 is THIS vertex's world direction (faceWarp'd, defLocalToWorld-mapped); place it on the
@@ -1015,8 +1035,8 @@ void main() {
1015
1035
  // = metres the water plane sits above the pre-flatten carved floor, >0 ONLY inside the true open
1016
1036
  // water; the FS gates ALL inland-water shading on this so blue == the flat water, banks stay land.
1017
1037
  vWaterDepth = max(waterPlane - floorBefore, 0.0) * step(0.001, haveWater);
1018
- vCanyonDep = canyonDepMask * canyonArid * step(0.0, vH);
1019
- vCliffFace = cliffFaceMask * canyonArid * step(0.0, vH);
1038
+ vCanyonDep = canyonDepMask * step(0.0, vH);
1039
+ vCliffFace = cliffFaceMask * step(0.0, vH);
1020
1040
  vDuneCrest = duneCrest * duneSand;
1021
1041
  vGrid = vertex.xy; // parametric mesh-cell coord for the wireframe overlay
1022
1042
  vLevel = defOffset.w; // quad LOD level -> FS patches view
@@ -1156,13 +1176,9 @@ vec3 terrainAlbedo(float h, float slope, float rockSlope, highp vec3 worldPos) {
1156
1176
  } else {
1157
1177
  c = mix(bcShore, bcLowland, smoothstep(0.0, bandEdgesLo.x, h));
1158
1178
  c = mix(c, bcGrass, smoothstep(bandEdgesLo.x, bandEdgesLo.y, h));
1159
- // ROCK IS VERTICALITY-DRIVEN, NOT HEIGHT-SPLATTED (user 2026-06-03: 'rock face based on
1160
- // verticality not splatted directly on albedo'). The old height-band rock mix
1161
- // (bcRock by bandEdgesHi) painted tan rock flat across every high plateau/summit regardless
1162
- // of slope, so flat high ground read as rock. REMOVED. High flat ground now keeps its biome
1163
- // (and snow by height below); ONLY the slope/verticality term and the steep cliff term turn
1164
- // rock. Snow still bands by altitude (a flat snowfield is physically correct).
1165
- c = mix(c, bcSnow, smoothstep(snowEdges.x, snowEdges.y, h));
1179
+ float bandWarp = snoise3(normalize(worldPos) * 400.0) * 500.0;
1180
+ c = mix(c, bcRock, smoothstep(bandEdgesHi.x + bandWarp, bandEdgesHi.y + bandWarp, h));
1181
+ c = mix(c, bcSnow, smoothstep(snowEdges.x + bandWarp, snowEdges.y + bandWarp, h));
1166
1182
  c = mix(c, bcRock, smoothstep(slopeRock.x, slopeRock.y, rockSlope) * step(0.0, h));
1167
1183
  // OLD PROCEDURAL GREY ROCKFACE DELETED (max-speed sweep 2026-06-10, user 'replace the original
1168
1184
  // rock completely'): the photo-rock splat owns steep faces; the 3-tap grey fBm fallback is gone.
@@ -1280,8 +1296,6 @@ float canyonMask(vec3 worldPos, float h, float temp, float humid, float px, out
1280
1296
  depth = 0.0;
1281
1297
  // gentle elevation gate: canyons want SOME relief above the lowland, not only rare high
1282
1298
  // plateaus (h>250 made them NEVER appear -- witnessed canyonFieldFrac 0). Fade in over 60..200m.
1283
- float elevGate = smoothstep(60.0, 200.0, h);
1284
- if (elevGate <= 0.0) return 0.0;
1285
1299
  // ONE FRACTAL (user 2026-06-02: 'canyon field shows two different resolutions, expected the same
1286
1300
  // fractal'). The FS used its OWN inline loop over worldPos/terrainR (= dir*(1+h/R), elevation-
1287
1301
  // SHIFTED off the VS sample) -> a different field/position/resolution than the VS carve. Now call
@@ -1291,11 +1305,9 @@ float canyonMask(vec3 worldPos, float h, float temp, float humid, float px, out
1291
1305
  float wid = clamp(px * 0.0006, 0.0, 0.03); // narrower than rivers
1292
1306
  float line = smoothstep(0.875 - wid, 0.94, ridge); // ~river-density threshold so they appear
1293
1307
  depth = smoothstep(0.875, 0.95, ridge); // deeper toward the channel centre
1294
- float aridGate = (1.0 - smoothstep(0.40, 0.58, humid)); // DRY (deserts/steppe), opposite of rivers
1295
- float warmGate = smoothstep(0.38, 0.56, temp); // warm arid plateaus
1296
1308
  // NO altFade (user: canyon field fading instead of integrated). wid grows with px -> >=1px AA at
1297
1309
  // every distance without vanishing; the canyon network is permanent, not a distance overlay.
1298
- return line * aridGate * warmGate * elevGate;
1310
+ return line * step(0.0, h);
1299
1311
  }
1300
1312
  #endif // biomeClassColor/riverMask/canyonMask: DEBUGVIEW-only (called solely from displayMode blocks) -- excluded from render FS cold-compile (FS-2, workflow w4y1bnrqc)
1301
1313
 
@@ -1338,7 +1350,8 @@ vec3 terrainAlbedoClimate(float h, float slope, float rockSlope, float temp, flo
1338
1350
  // must stop and become sand, which continues under the water'): below uBeachTopM the biome/
1339
1351
  // grass/snow color yields to shore sand (the same bcShore the underwater bed starts from, so the
1340
1352
  // material is continuous through the waterline). Steep coastal cliffs keep their rock.
1341
- float beachM = (1.0 - smoothstep(uBeachTopM * 0.6, uBeachTopM, h))
1353
+ // WIDENED TRANSITION (2026-06-13): 0.6->0.3 softens the grass-sand boundary from 12m to 21m.
1354
+ float beachM = (1.0 - smoothstep(uBeachTopM * 0.3, uBeachTopM, h))
1342
1355
  * (1.0 - smoothstep(slopeRock.x, slopeRock.y, rockSlope));
1343
1356
  c = mix(c, bcShore, beachM);
1344
1357
  // INLAND WATER -- COLOUR GATED BY THE CARVE FIELD (alignment + no-fade fix, browser-2705/2699).
@@ -1380,7 +1393,7 @@ vec3 terrainAlbedoClimate(float h, float slope, float rockSlope, float temp, flo
1380
1393
  {
1381
1394
  highp vec3 od = normalize(worldPos);
1382
1395
  float ov = 0.0, oa = 0.0;
1383
- float fq = 150.0, am = 1.0; // octaves: ~42km / 8.5km / 1.7km features
1396
+ float fq = 75.0, am = 1.0; // octaves: ~84km / 17km / 3.4km features (halved to match VS detailFbm)
1384
1397
  int fdOcts = (uFSDetailOcts > 0) ? uFSDetailOcts : 3;
1385
1398
  for (int o = 0; o < fdOcts; o++) {
1386
1399
  float wl = 40000000.0 / fq; // feature wavelength (m) ~ 2*pi*R / fq
@@ -1452,6 +1465,34 @@ void main() {
1452
1465
  // none of the terrain material/atmosphere work below runs for water fragments.
1453
1466
  if (uIsWater > 0.5) {
1454
1467
  if (vH > 1.0) discard; // surface under land: depth test culls it anyway; discard kills shoreline shimmer
1468
+ // UNDERWATER WATER SURFACE (camera below sea level): render the surface from below as
1469
+ // opaque deep blue with Gerstner wave animation. No sky reflection, no Fresnel (TIR from
1470
+ // below = mostly opaque deep water), no Beer-Lambert (there is no seabed above the camera).
1471
+ if (uUnderwater > 0.5) {
1472
+ highp vec3 wOriginW = floor(camWorld / 1024.0) * 1024.0;
1473
+ highp vec2 wpW = vec2(dot(vWorld - wOriginW, ux), dot(vWorld - wOriginW, uy));
1474
+ vec2 slopeW = oceanWaveSlope(wpW, oceanTime);
1475
+ highp float wDistW = length(camWorld - vWorld);
1476
+ slopeW *= clamp(1.0 - wDistW / 4000.0, 0.0, 1.0);
1477
+ vec3 wn = normalize(uz - ux * slopeW.x - uy * slopeW.y);
1478
+ vec3 viewW = normalize(camWorld - vWorld);
1479
+ float ndl = max(dot(wn, sunDir), 0.0);
1480
+ // Sunlight attenuated through the water column; deep blue ambient
1481
+ float depthAtten = exp(-max(0.0, terrainR - length(camWorld)) * 0.0005);
1482
+ vec3 sunUnder = vec3(1.0, 0.6, 0.3) * depthAtten * ndl;
1483
+ vec3 deepBlue = vec3(0.005, 0.06, 0.18);
1484
+ vec3 waveBright = vec3(0.0, 0.02, 0.06) * length(slopeW);
1485
+ vec3 wcol = deepBlue + sunUnder * 0.4 + waveBright;
1486
+ float macroMuW = dot(uz, sunDir);
1487
+ float dayShadeW = mix(uNightFloor, 1.0, smoothstep(-uTermWidth, uTermWidth, macroMuW));
1488
+ vec3 cW = wcol * dayShadeW * uExposure;
1489
+ vec3 mappedW = clamp((cW * (2.51 * cW + 0.03)) / (cW * (2.43 * cW + 0.59) + 0.14), 0.0, 1.0);
1490
+ float lumW = dot(mappedW, vec3(0.2126, 0.7152, 0.0722));
1491
+ mappedW = mix(vec3(lumW), mappedW, uLookSat);
1492
+ mappedW = clamp((mappedW - 0.5) * uLookContrast + 0.5, 0.0, 1.0);
1493
+ fragColor = vec4(pow(mappedW, vec3(1.0 / 2.2)), 1.0);
1494
+ return;
1495
+ }
1455
1496
  highp float depthM = max(-vH, 0.0);
1456
1497
  highp vec3 wOriginW = floor(camWorld / 1024.0) * 1024.0; // snapped anchor (fp32 wave-phase fix, same as the old branch)
1457
1498
  highp vec2 wpW = vec2(dot(vWorld - wOriginW, ux), dot(vWorld - wOriginW, uy));
@@ -1511,7 +1552,7 @@ void main() {
1511
1552
  // (which made fine relief go missing). It is a continuous function of dir0 only -- adjacent fragments
1512
1553
  // get the linearly-interpolated VS value, so it is smooth by construction (no dFdx/dFdy, no fwidth).
1513
1554
  // uFlatNormal kept as a cheap escape hatch: pure sphere normal (uz) for A/B isolation.
1514
- vec3 n = (uFlatNormal > 0.5) ? uz : normalize(vNrm);
1555
+ vec3 n = (uFlatNormal > 0.5) ? uz : normalize(mix(vNrm, uz, 0.05));
1515
1556
 
1516
1557
  // GPU-TIMER VS-ISOLATION: a cheap measurement frame short-circuits the whole per-pixel shade
1517
1558
  // here (uses vNrmPV + vWorld so the VS outputs are not dead-code-eliminated) -> the timed cheap
@@ -1633,21 +1674,50 @@ void main() {
1633
1674
  float dryHot = smoothstep(0.60, 0.85, 1.0 - climate.w) * smoothstep(0.42, 0.62, climate.z);
1634
1675
  // BEACH (user 2026-06-10 'the beaches should be sand'): the shore profile eases over the
1635
1676
  // first ~60m of elevation, so low gentle land near sea level splats sand regardless of climate.
1636
- float beach = (1.0 - smoothstep(10.0, 80.0, vH)) * (1.0 - smoothstep(0.22, 0.42, slope));
1677
+ // NOISE-VARIED THRESHOLDS (2026-06-13): vTexWarp.x adds an irregular wavy line so the sand
1678
+ // doesn't follow a strict elevation contour — reads as a natural beach, not a cut.
1679
+ // SHARED biome-band warp pattern (user 2026-06-14: the SAME warping on snow/rock bands AND the
1680
+ // beach->land crossover). 2-oct world-dir noise, 1/4 freq (~13km + ~5km waves). Computed once
1681
+ // here; the snow/rock bandWarp below reuses warpN. highp dir for the lattice precision.
1682
+ highp vec3 bwDir = normalize(vWorld);
1683
+ float warpN = snoise3(bwDir * 3325.0) + 0.5 * snoise3(bwDir * 7750.0); // ~ +/-1.5
1684
+ // BEACH sand gate tied to uBeachTopM (so the sand TEXTURE scales with the wide beach, not a
1685
+ // hardcoded 80m strip) + the shared warp on its LAND edge so the beach->grass line is irregular.
1686
+ float beachW = warpN * uBeachTopM * 0.30; // warp amplitude scales with the beach band height
1687
+ float beach = (1.0 - smoothstep(uBeachTopM * 0.12 + beachW, uBeachTopM + beachW, vH))
1688
+ * (1.0 - smoothstep(0.15, 0.42, slope));
1689
+ // SAND BLEED (2026-06-13): patchy sand spills above the main beach line, modulated by VS
1690
+ // warp noise so the edge reads as wind-blown pockets, not a strict elevation cut. At peak it
1691
+ // adds ~0.3 sand weight far above the beach, creating a natural dappled transition.
1692
+ float sandBleed = max(0.0, vTexWarp.y) * 0.35 * max(0.0, 1.0 - smoothstep(30.0, 200.0, vH));
1637
1693
  // SAND REGIONS SUPPRESS ROCK (user 2026-06-10 'rock being used instead of sand'): in
1638
1694
  // deserts/dunes/beaches sand drapes moderate slopes; rock only wins on genuinely steep faces
1639
1695
  // there (gate shifted toward 0.5-0.7 inside sand regions instead of slopeRock 0.28-0.55).
1640
- float sandRegion = clamp(max(max(dryHot, beach), smoothstep(0.0, 0.25, vDuneCrest)), 0.0, 1.0);
1696
+ float sandRegion = clamp(max(max(dryHot, max(beach, sandBleed)), smoothstep(0.0, 0.25, vDuneCrest)), 0.0, 1.0);
1641
1697
  // SPLAT ROCK GATE DECOUPLED from the macro slopeRock (user 2026-06-11 'a lot of grass turning
1642
1698
  // into rocky patches again'): the user-calibrated global soft blend slopeRock [-0.6,1] puts
1643
1699
  // ~37% ROCK LAYER weight on perfectly flat ground, and the displacement-sharpened top-2
1644
1700
  // crossfade then flips whole displacement blobs to the rock PHOTO on grassland. The macro
1645
1701
  // color blend keeps the calibrated tone; the rock TEXTURE layer needs real slope: floor the
1646
1702
  // splat gate at 0.18 so flat/gentle land splats grass, rock starts on genuine slopes.
1647
- float srLo = max(slopeRock.x, 0.18), srHi = max(slopeRock.y, srLo + 0.2);
1648
- float wRock = smoothstep(mix(srLo, 0.50, sandRegion), mix(srHi, 0.70, sandRegion), rockSlope);
1649
- float snowHi = smoothstep(snowEdges.x, snowEdges.y, vH);
1650
- float snowCold = (1.0 - smoothstep(0.30, 0.75, climate.z)) * smoothstep(snowEdges.x * 0.5, snowEdges.x, vH);
1703
+ // WIDENED TRANSITION (2026-06-13): srLo+0.2 -> srLo+0.4 so rock/grass and rock/sand/snow
1704
+ // boundaries have a wider blend band the slope gradient between materials is no longer
1705
+ // a ~0.2-unit hard step but a ~0.4-unit gradual fade.
1706
+ float srLo = max(slopeRock.x, 0.05), srHi = max(slopeRock.y, srLo + 0.35); // 0.18->0.10->0.05: rock slope gate MORE SENSITIVE so even gentle slopes (~18deg+) pick up as rock (user 2026-06-14, repeated). Stays above truly-flat (rockSlope~0).
1707
+ float wRockSlope = smoothstep(mix(srLo, 0.50, sandRegion), mix(srHi, 0.70, sandRegion), rockSlope);
1708
+ // WARPED BIOME BAND EDGES (user 2026-06-14: the snow/rock lines were STRAIGHT horizontal contours
1709
+ // viewed side-on; the vTexWarp domain warp was too low-freq (>1.8km waves = ~constant over one
1710
+ // mountain) so it had no visible effect). Use a dedicated 2-octave WORLD-DIR noise at mountain
1711
+ // scale (~1.3-3km waves) so EVERY elevation-keyed band wobbles +/-~700m vertically over a few km
1712
+ // = irregular natural lines, not level contours. World-dir keyed (seam-safe). Applied to snow,
1713
+ // the rock band, snowCold, and the beach below. highp dir (high-freq lattice needs the precision).
1714
+ float bandWarp = warpN * 450.0; // reuse the shared warpN (defined at the beach gate above); +/-~700m undulation
1715
+ float snowHi = smoothstep(snowEdges.x + bandWarp, snowEdges.y + bandWarp, vH);
1716
+ // ROCK BAND leading up to the snow (user 2026-06-14): a rocky belt ~0.4-2.2km below the snow
1717
+ // line so high mountains show rock between alpine grass and snow, not grass straight to snow.
1718
+ float rockBand = smoothstep(snowEdges.x - 2200.0 + bandWarp, snowEdges.x - 400.0 + bandWarp, vH) * (1.0 - snowHi);
1719
+ float wRock = max(wRockSlope, rockBand);
1720
+ float snowCold = (1.0 - smoothstep(0.30, 0.75, climate.z)) * smoothstep(snowEdges.x * 0.5 + bandWarp, snowEdges.x + bandWarp, vH);
1651
1721
  // POLAR/ICE-BIOME SNOW (user 2026-06-10 'put the snow texture on the snow'): the ICE biome
1652
1722
  // whitens cold lowland (biomeColor gate 1-smoothstep(0.10,0.18,tempEff)) at ANY elevation, but
1653
1723
  // wSnow was elevation-gated only -- polar snowfields were splatting the GRASS layer. Match the
@@ -1657,13 +1727,14 @@ void main() {
1657
1727
  - 0.18 * (abs(lat2) / 1.5708) * uBiomeBandBias, 0.0, 1.0);
1658
1728
  float iceClimate = (1.0 - smoothstep(0.10, 0.20, tempEff2)) * step(0.0, vH); // no snow layer on the seabed
1659
1729
  float wSnow = clamp(snowHi + 0.7 * snowCold + iceClimate, 0.0, 1.0) * (1.0 - 0.6 * wRock);
1660
- float wSand = sandRegion * (1.0 - wRock) * (1.0 - wSnow) * (1.0 - smoothstep(0.40, 0.60, slope));
1730
+ float wSand = sandRegion * (1.0 - wRock) * (1.0 - wSnow) * (1.0 - smoothstep(0.30, 0.70, slope));
1661
1731
  float wGrass = max(1.0 - wRock - wSnow - wSand, 0.0);
1662
1732
  vec4 w4 = vec4(wGrass, wRock, wSand, wSnow); // layers 0..3 = grass,rock,sand,snow
1663
1733
  // BEACH BAND + UNDERWATER in one mask (user 2026-06-11): sand owns everything below the
1664
1734
  // beach ceiling uBeachTopM -- grass/snow weight folds into sand continuously through h=0 to
1665
1735
  // the seabed, so no vegetation can ever splat at or below the waterline (structural).
1666
- float uwM = 1.0 - smoothstep(uBeachTopM * 0.6, uBeachTopM, vH);
1736
+ // WIDENED (2026-06-13): 0.6->0.3 to match the macro beach transition, softer fade-out.
1737
+ float uwM = 1.0 - smoothstep(uBeachTopM * 0.3, uBeachTopM, vH);
1667
1738
  w4.z += (w4.x + w4.w) * uwM; w4.x *= 1.0 - uwM; w4.w *= 1.0 - uwM;
1668
1739
  w4 /= (w4.x + w4.y + w4.z + w4.w + 1e-4);
1669
1740
  // top-2 layer pick: 4-way blending would cost 24 taps; the gates are spatially near-exclusive,
@@ -1705,7 +1776,10 @@ void main() {
1705
1776
  // displacement term 0.8 -> 0.3 (user 2026-06-11 'still see bowls of rock texture'):
1706
1777
  // 0.8 still let the displacement photo's bowl-shaped blobs flip whole patches to rock
1707
1778
  // inside the transition band; 0.3 only feathers the seam edge.
1708
- float bSharp = clamp((bAB * 2.0 - 1.0) * 3.0 + (albA.a - albB.a) * 0.3 + 0.5, 0.0, 1.0);
1779
+ // SOFTENED (2026-06-13): transition sharpening reduced 3->1 so grass/rock/sand/snow
1780
+ // boundaries hold a natural blend band instead of a hard die-cut line. The 1.0 slope
1781
+ // still prefers the dominant layer but leaves a visible transition zone.
1782
+ float bSharp = clamp((bAB * 2.0 - 1.0) * 1.0 + (albA.a - albB.a) * 0.3 + 0.5, 0.0, 1.0);
1709
1783
  texAlb = mix(albB, albA, bSharp);
1710
1784
  texNrm = mix(nrmB, nrmA, bSharp);
1711
1785
  }
@@ -1821,7 +1895,16 @@ void main() {
1821
1895
  // GENTLE-SLOPE RELIEF SHADING (user 2026-06-10 'even gentle slopes shaded so terrain elevation
1822
1896
  // is obvious'): exaggerate the tangential tilt of the LIT normal only -- the material gates keep
1823
1897
  // the true geometric n, so placement is unchanged; lighting contrast on rolling ground increases.
1824
- if (vH > -2.0 && uReliefShade > 1.0) nLit = normalize(uz + (nLit - uz) * uReliefShade);
1898
+ if (vH > -2.0 && uReliefShade > 1.0) {
1899
+ // Relief exaggeration of the LIT normal's tangential tilt. The old SCREEN-SPACE dFdx(vH) term was
1900
+ // REMOVED (user 2026-06-14 'jagged normals, still there on the normals view'): dFdx/dFdy of the
1901
+ // INTERPOLATED vH varying is CONSTANT PER TRIANGLE and discontinuous at every triangle edge =
1902
+ // hard facets baked straight into the lit normal -- the jaggedness root, not the vertex normal.
1903
+ // It was a workaround for when vNrm ~= uz on gentle ground; the central-difference vNrm now
1904
+ // captures gentle slopes (real gradient), so (nLit-uz) is non-zero on real slopes and the plain
1905
+ // exaggeration is legible without the faceted screen-space hack.
1906
+ nLit = normalize(uz + (nLit - uz) * uReliefShade);
1907
+ }
1825
1908
  // photo-texture detail normal at its calibrated amplitude, OUTSIDE the relief exaggeration.
1826
1909
  // ADDED in world space (the old `- ux*dn.x - uy*dn.y` subtracted an already-negated Sobel
1827
1910
  // normal in a frame the triplanar RG was never expressed in -- the fundamental both-at-once
@@ -1840,7 +1923,18 @@ void main() {
1840
1923
  // blended over this seabed. Sea ice still keys the seabed albedo near the poles.
1841
1924
 
1842
1925
  vec3 skyIrr;
1843
- vec3 sunIrr = atm_sunSkyIrradiance(pAtm, nAtm, sunDir, skyIrr);
1926
+ vec3 sunIrr;
1927
+ if (uUnderwater > 0.5) {
1928
+ // Underwater terrain (seabed): sun attenuated through water column, blue ambient, no
1929
+ // atmospheric Rayleigh/Mie scattering. Sun colour shifts toward green-blue with depth.
1930
+ float depth = max(0.0, terrainR - length(camWorld));
1931
+ float atten = exp(-depth * 0.0004);
1932
+ float ndl = max(dot(nAtm, sunDir), 0.0);
1933
+ sunIrr = vec3(1.0, 0.65, 0.35) * atten * ndl * 0.7;
1934
+ skyIrr = vec3(0.02, 0.07, 0.18);
1935
+ } else {
1936
+ sunIrr = atm_sunSkyIrradiance(pAtm, nAtm, sunDir, skyIrr);
1937
+ }
1844
1938
  // The Rayleigh sky irradiance is strongly blue; at full strength it tints the lit
1845
1939
  // CONTINENTS blue-grey from orbit (witnessed: lit mean [83,95,112] vs warm albedo
1846
1940
  // [90,83,74] at 2000km -- the blue is the sky-irradiance term, NOT aerial inscatter).
@@ -1987,6 +2081,20 @@ void main() {
1987
2081
  color = mix(lit, hazed, apGate * uHazeMul); // uHazeMul lever (2026-06-10 'pale hazy': full-strength haze milked the midground)
1988
2082
  }
1989
2083
  }
2084
+ // UNDERWATER FOG (camera below sea level): replace the air-based Aerial Perspective with
2085
+ // absorption/scattering through water. Fog colour deepens with camera depth so the seabed
2086
+ // fades to blue-green with distance, red absorbed first. Applied only to terrain (uIsWater<0.5).
2087
+ if (uUnderwater > 0.5 && uIsWater < 0.5) {
2088
+ highp vec3 camA = atmPos(camWorld, terrainR);
2089
+ highp vec3 segKm = pAtm - camA;
2090
+ highp float dKm = length(segKm);
2091
+ float depth = max(0.0, terrainR - length(camWorld));
2092
+ // Water absorption coefficients (per km): red attenuates fastest, blue slowest.
2093
+ vec3 absorb = vec3(4.0, 0.8, 0.3) * (1.0 + depth * 0.0003);
2094
+ vec3 uwTrans = exp(-absorb * dKm);
2095
+ vec3 uwFog = vec3(0.002, 0.06, 0.16) + vec3(0.0, 0.02, 0.04) * depth / 1000.0;
2096
+ color = mix(color * uwTrans + uwFog * (1.0 - uwTrans), uwFog, smoothstep(50.0, 500.0, dKm * 1000.0));
2097
+ }
1990
2098
  // RIVERS post-lighting (witnessed browser-2115/2118: the river-blue in ALBEDO is multiplied
1991
2099
  // by the warm sun irradiance (sunIrr.b is low) so land rivers lose their blue in the lit
1992
2100
  // result -- 15k blue albedo px -> 0 lit px, even with the sun HIGH (sunDotUp 0.85). Ocean
@@ -26,7 +26,7 @@ const DEFAULTS = {
26
26
  fsNormal: 0, // window.__fsNormal (cross-dFdx FS normal, diagnostic, default off)
27
27
  },
28
28
  lod: {
29
- splitFactor: null, // window.__splitFactor (null = orchestrator altitude ramp)
29
+ splitFactor: 0.28, // window.__splitFactor (null = orchestrator altitude ramp). 0.4->0.28 (user 2026-06-14: ~600 quads at the deck)
30
30
  },
31
31
  geometry: {
32
32
  elevEdgeInset: 0.5, // window.__elevEdgeInset (mesh-edge elevation sample inset; gl-render)