mapspinner 0.1.33 → 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.33",
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); });
@@ -522,7 +532,7 @@ 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 = gpuM; // GPU-exact rendered height (may be negative = seabed)
535
+ renderedM = gpuM + 1.0; // +1m margin for sub-vertex micro-relief (vtxDisplace peaks exceed 2m in rugged terrain)
526
536
  } else {
527
537
  renderedM = jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2]) + 3.0; // mirror fallback (CPU under-predicts)
528
538
  }
@@ -569,7 +579,7 @@ function frameLoop(){
569
579
  // at 2 by a diagnostic turned a dropdown pick of 1 into 2|1=3 (invalid mode -> silent lit) = the
570
580
  // 'render menu does nothing' report. Explicit precedence: override wins when set, else the menu.
571
581
  const _dmEff = (window.__displayMode != null ? window.__displayMode : cam.displayMode) | 0;
572
- 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);
573
583
  window.__webglFrame = fr;
574
584
  // First rendered frame: tear down the loading overlay (compile finished, terrain is on screen).
575
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]
@@ -922,9 +956,11 @@ export async function initMapspinnerRender(gl, opts = {}) {
922
956
  }
923
957
  }
924
958
  if (typeof window !== 'undefined') window.__lastDrawCalls = (n > 0) ? 2 : 0;
925
- return gl.getError();
959
+ return 0; // glError is checked via checkGlError() once per frame after quadtree (CPU/GPU pipelining)
926
960
  }
927
961
 
962
+ function checkGlError() { return gl.getError(); }
963
+
928
964
  // ---- DEBUG PROBE: replicate the VS clip-space transform on the CPU for a quad's 4
929
965
  // corners (vertex.xy in {0,1}^2) so we can see which quads project off-screen.
930
966
  function probe(quads, cam) {
@@ -956,5 +992,5 @@ export async function initMapspinnerRender(gl, opts = {}) {
956
992
  return out;
957
993
  }
958
994
  function setHpf(tex, res, tex2) { _hpfTex = tex; _hpfRes = res|0; _hpfTex2 = tex2 || null; } // tex2 = RG8(temp,humid) pack (W12)
959
- 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 };
960
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)
@@ -208,6 +208,7 @@ uniform float uVertexAO; // per-vertex shading/AO strength lev
208
208
  uniform float uIsWater; // 0 = terrain pass, 1 = water-surface pass
209
209
  uniform float uUnderwater; // 0 = camera above water, 1 = camera below sea level
210
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).
211
212
  uniform float uHiFreqCut; // hi-freq elevation-noise attenuation, applied to ALL hi-freq sources:
212
213
  // broadShapeM/MD fine octaves (o>=6) + vtxDisplace micro-relief
213
214
  // (window.__hiFreqCut; default 0.25 = the user's 4x reduction, 2026-06-06)
@@ -220,7 +221,7 @@ uniform float uMtnBandWide; // widen mtn=smoothstep(16.8,18.6,ele
220
221
  uniform float uClimateRelief; // widen wetLowFlat(0.66,0.9,humid) + coldFlat(0.18,0.34,temp) reliefMul gates
221
222
  uniform float uIsleWide; // widen isleZone seaBias gates (50,350)+(900,1600) -> (30,600)+(600,2200)
222
223
  uniform float uCarveWide; // widen the river/canyon/lake/dune CLIMATE gates so carve depth fades in over a wide span
223
- 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.
224
225
  // CANYON cross-section now reads as a CANYON, not a V-notch: STEEP WALLS + a FLAT FLOOR.
225
226
  // wall = a sharp smoothstep band -> the carve drops fast over a narrow ridge interval (the cliff
226
227
  // walls), instead of the old gentle bench+gorge blend that made shallow V-troughs.
@@ -233,8 +234,8 @@ float canyonCarveM(vec3 dir, out float depth){
233
234
  // narrow ridge slice -> thin hairline gorges. Widen so the canyon reads as a BROAD gorge: a wide
234
235
  // eroded rim shoulder, a steep but visible wall, and a wide flat floor that the gorge bottoms out on.
235
236
  float bench = smoothstep(0.62, 0.78, ridge); // wide eroded rim shoulder
236
- float wall = smoothstep(0.78, 0.86, ridge); // STEEP cliff wall (wider band)
237
- 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
238
239
  depth = max(wall, floorF);
239
240
  // 0.18 rim shoulder + 0.82 wall-to-floor; floorF clamps so the bottom is flat (no infinite V).
240
241
  float profile = 0.18 * bench + 0.82 * max(wall, floorF);
@@ -247,13 +248,16 @@ float canyonCarveM(vec3 dir, out float depth){
247
248
  // 100m scale the low-altitude camera sees. Pure world-dir (LOD-invariant); the mesh resolves them
248
249
  // at maxLevel 16 (~7m cells). Each gully octave = a thinned ridged field, depth scaled to its wl.
249
250
  vec3 dn = normalize(dir);
250
- float g1 = inciseRidgeField(dn + vec3(5.1, 8.3, -2.7), 3500.0, 2.11); // ~10km tributaries
251
- float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2), 14000.0, 2.05); // ~2.5km gullies
252
- // HIGH-FREQ fractal depth x0.65 (user 2026-06-11 'leave the low frequency canyons deep, only
253
- // reduce the high frequency fractals, bring it back 30%' -- halved, then +30% restored): the
254
- // main 100km gorge network keeps full CANYON_INCISE_DEPTH; only these tributary octaves shrink.
255
- carve += -104.0 * dmul * smoothstep(0.78, 0.93, g1); // ~10km tributaries (160m * 0.65)
256
- 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
257
261
  return carve;
258
262
  }
259
263
  float canyonCarveM(vec3 dir){ float dd; return canyonCarveM(dir, dd); }
@@ -339,10 +343,10 @@ float duneFieldM(vec3 dir, out float crest){
339
343
  highp float broadShapeLowM(vec3 dir){ // W7: metres (~13000) + freq (~49152) accumulators are highp islands
340
344
  if (hasHpf == 0) return 0.0;
341
345
  vec3 d = normalize(dir);
342
- highp float amp = 6500.0, freq = 3.0, sum = 0.0;
346
+ highp float amp = 6500.0, freq = 0.75, sum = 0.0;
343
347
  int blOcts = (uBroadLowOcts > 0) ? uBroadLowOcts : 8;
344
348
  for (int o=0; o<blOcts; o++){ sum += amp * snoise3(d*freq); amp *= (o < 6 ? 0.66 : 0.82); freq *= 2.0; }
345
- return sum - 900.0;
349
+ return sum - 500.0;
346
350
  }
347
351
  // ---- SHARED micro-relief helpers (MOVED to the common preamble, THC-Normal W1, so composeHeight()
348
352
  // in the VS/PROBE region can call them). vtxDetail = the micro-relief A/B global; vnoise2
@@ -383,13 +387,14 @@ highp vec2 faceWarp(highp vec2 p){ return defRadius * tan((p / defRadius) * 0.78
383
387
  // PERLIN-EVERYWHERE lever -- shared by the composeHeight elevation term (VS/PROBE) and the FS albedo
384
388
  // overlay, so it must be declared in ALL stages (outside the VS/PROBE guard below).
385
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
386
391
  uniform float uNrmStepM; // lit-normal FD step in metres (150); uniform-fed to defeat FXC constant folding
387
392
  #if defined(_VERTEX_) || defined(_PROBE_)
388
393
  highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: returns metres (~13000) -> highp
389
394
  if (hasHpf == 0) return 0.0;
390
395
  float mtnAmp = 1.0; // mountain-amplitude (was a window.__mtnAmp uniform; inlined at neutral 1.0)
391
396
  vec3 d = normalize(dir);
392
- 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
393
398
  int octMax = (uOctMax > 0) ? uOctMax : 12; // runtime bound (FXC unroll-defeat); 12 when unset
394
399
  for (int o=0; o<octMax; o++){ // W9: 14->12 octaves, drop the finest 2 (wavelengths <2km) UNCONDITIONALLY
395
400
  // FINE-OCTAVE 5x FREQ (user 2026-06-06: the high-freq normals-affecting GROUND bump should be 5x
@@ -418,19 +423,8 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
418
423
  sum += amp * nn;
419
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)
420
425
  }
421
- // LOW-ELEVATION COMPRESSION CURVE (gamma 2.5): power-curve the positive land height only so
422
- // low/mid land sinks toward a flatter base while peaks keep their range; sea sign untouched.
423
- {
424
- const float HREF = 6500.0, GAMMA = 2.5;
425
- highp float e0 = sum - 900.0; // W7: metres
426
- if (e0 > 0.0) {
427
- highp float ec = HREF * pow(min(e0, HREF) / HREF, GAMMA);
428
- if (e0 > HREF) ec += (e0 - HREF);
429
- sum = 900.0 + ec;
430
- }
431
- }
432
426
  // PEAK-LIFT, MOUNTAIN-BELT GATED: lift the high end so ridges become tall peaks; plains flat.
433
- highp float hi = max(sum - 900.0, 0.0); // W7: metres
427
+ highp float hi = max(sum - 500.0, 0.0); // W7: metres
434
428
  float peak = smoothstep(1200.0, 5000.0, hi);
435
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)
436
430
  sum += hi * peak * liftK;
@@ -440,8 +434,8 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
440
434
  if (belt > 0.0 && hi > 0.0) {
441
435
  // (massif envelope freqs restored to 4.0/7.0 -- they set continental belt PLACEMENT, not the visible
442
436
  // ridge spacing; the 3x-wider knob is the o>=6 fine band above, not this envelope.)
443
- float e0 = snoise3(d * 4.0 + vec3(11.0, 3.0, 7.0)) * 0.5 + 0.5;
444
- 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;
445
439
  float modu = 0.7 + 0.3 * clamp(e0 * 0.7 + e1 * 0.3, 0.0, 1.0);
446
440
  float landGate = smoothstep(0.0, 800.0, hi);
447
441
  float base = belt * landGate * modu;
@@ -453,14 +447,14 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
453
447
  // conflated the braided TEXTURE patches (rockSlope breakup noise, since deleted) with real slope;
454
448
  // with the braids, the dark raw-photo far field, and the UDN normal-frame bug all fixed, the
455
449
  // original steep terrain stands.
456
- 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)
457
451
  int pkOcts = (uPeakOcts > 0) ? uPeakOcts : 3;
458
452
  for (int o = 0; o < pkOcts; o++) {
459
453
  ps += pa * (1.0 - abs(snoise3(d * pf + vec3(3.3, 7.7, 1.1))));
460
454
  pn += pa; pa *= 0.65; pf *= 2.13;
461
455
  }
462
456
  float crest = ps / pn;
463
- 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)
464
458
  }
465
459
  }
466
460
  // SOFT ELEVATION CEILING (mob-w8-ceiling 2026-06-08: 'peaks come to points, no flat plateau'). Even at
@@ -470,13 +464,13 @@ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: ret
470
464
  // asymptoting to a ceiling -- and lift CMAX to 15000 so the asymptote sits well above any real peak. With
471
465
  // the wider knee the high band no longer collapses, so the *1.15 scale-up crutch is DROPPED.
472
466
  {
473
- highp float e = sum - 900.0; // W7: metres
474
- // 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).
475
469
  // 723m, relief only ~4km -> mountains invisible/clamped-looking, NOT a top clip). 0.65 expands the
476
470
  // range so mountains stand out: p50 ~1370m, p99 ~8920m, MAX ~11600m, relief ~7.5km. Scales the WHOLE
477
471
  // relief uniformly (base octaves + belt massif + peaks) on the excess-over-900. Max 11.6km still well
478
472
  // under the ceiling (CK 6500 / CMAX 42000, knee /26000) so no clip.
479
- e *= 0.65;
473
+ e *= 0.85;
480
474
  // CEILING RAISED + KNEE WIDENED for the 4x mountaintops (user 2026-06-09: 'must not hit the ceiling
481
475
  // anywhere, run under the normal max + elevate the maximum'). MEASURED 4x-hf+4x-mtn pre-ceiling max
482
476
  // = 28550m (p99.9 21457m); CMAX 42000 sits 1.47x above it = no peak ever approaches the asymptote, and
@@ -549,9 +543,10 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
549
543
  // amplitude than the base bump (ea0 5m vs 8m) per 'a little less intensity'. 4-octave ridged-leaning fBm
550
544
  // for erosive gully/ridgeline shape. Added INSIDE vtxDisplace so it shares the LOD-invariant, seam-safe,
551
545
  // face-local path (no per-patch step) and is picked up by the composeHeight central-diff lit normal.
552
- 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)
553
548
  if (mtnGate > 0.0) {
554
- 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.
555
550
  int veOcts = (uVtxErodeOcts > 0) ? uVtxErodeOcts : 4;
556
551
  for (int o = 0; o < veOcts; o++) {
557
552
  float en = vnoise2(fp / ewl);
@@ -560,10 +555,13 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
560
555
  esumR += ea * (er * 2.0 - 1.0);
561
556
  ea *= 0.55; ewl *= 0.5;
562
557
  }
563
- 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
564
559
  }
565
560
  float ruggedAmp = clamp(rugged, 0.4, 1.15);
566
- 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;
567
565
  }
568
566
 
569
567
  // PERLIN-EVERYWHERE detail fbm (user 2026-06-10): ONE 3-octave value fbm shared by the FS albedo
@@ -571,7 +569,7 @@ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-
571
569
  // relief variation correlate, reading as one landform). World-dir keyed, seam-safe.
572
570
  highp float detailFbm(vec3 dir) {
573
571
  float ov = 0.0, oa = 0.0;
574
- float fq = 150.0, am = 1.0;
572
+ float fq = 75.0, am = 1.0;
575
573
  int dfOcts = (uDetailFbmOcts > 0) ? uDetailFbmOcts : 3;
576
574
  for (int o = 0; o < dfOcts; o++) {
577
575
  ov += am * snoise3(dir * fq + vec3(float(o) * 7.3));
@@ -602,7 +600,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
602
600
  // latitude/moisture gradients instead of a per-cell contour (wrxo0rr7a coldFlat/wetLowFlat 80m+).
603
601
  float wetLowFlat = smoothstep(mix(0.66, 0.50, uClimateRelief), mix(0.9, 1.0, uClimateRelief), bHum) * (1.0 - mtn);
604
602
  float coldFlat = (1.0 - smoothstep(mix(0.18, 0.05, uClimateRelief), mix(0.34, 0.45, uClimateRelief), bTemp));
605
- 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);
606
604
  float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0);
607
605
  // ISLAND-TYPE VARIETY (pure fn of world dir) -- mirror of the VS main isle block.
608
606
  // uIsleWide=1 spreads the seaBias double-gate so the volcanic/atoll remix ramps in (wrxo0rr7a 2000m).
@@ -626,6 +624,16 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
626
624
  if (h < 0.0) {
627
625
  highp float d = -h;
628
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;
629
637
  }
630
638
  // displacement now continues UNDERWATER (the old land-only gate served the flat-clamped ocean,
631
639
  // gone since 026d530): the seabed carries the same micro-relief as land = realistic continuation.
@@ -634,17 +642,19 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
634
642
  // the FS albedo overlay shows, as real relief (~30m per lever unit -> ~180m at the user-tuned 6).
635
643
  // Shore-gated (fades in over the first 250m of land) so the coastline and the flat water planes are
636
644
  // untouched and no noise islets pop offshore. The VS FD lit-normal picks it up automatically.
637
- h += detailFbm(dir0) * uDetailOverlay * 30.0 * smoothstep(0.0, 250.0, h);
638
- // FLAT-AREA INTEREST NOISE (user 2026-06-12): gentle rolling relief on the least noisy
639
- // anchorpoints (low reliefMul = flat plains, non-mtn, non-wet, non-cold). Fades to zero
640
- // 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.
641
649
  float flatGate = max(0.0, 1.0 - reliefMul * 2.0);
642
- 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;
643
653
  // LAKE CARVE + flat-water plane
644
654
  float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
645
- // uCarveWide=1 widens the carve CLIMATE gates so the gorge/lake/dune depth fades in over a wide
646
- // anchor span instead of snapping along a thin climate contour (wrxo0rr7a carve cluster 90-1400m).
647
- 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);
648
658
  float lakeCarveV = lakeCarveRaw * lakeGate;
649
659
  h += lakeCarveV;
650
660
  float lakeWet = lakeWetV * lakeGate;
@@ -652,8 +662,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
652
662
  // RIVER + CANYON incision (clamped so coastal gorges never punch fake inland seas)
653
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);
654
664
  float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, h);
655
- 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);
656
- float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, h);
665
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * step(0.0, h);
657
666
  float inciseTot = riverCarveV + canyonCarveV;
658
667
  // min(...,0): the floor term goes POSITIVE for any h below -60 and was LIFTING the entire ocean
659
668
  // floor to exactly -60m -- the whole seabed was a uniform pan (root of 'depth under the water
@@ -662,7 +671,7 @@ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7
662
671
  inciseTot = max(inciseTot, min(-60.0 - h, 0.0)); // land: h + inciseTot >= -60m; ocean: untouched
663
672
  h += inciseTot;
664
673
  // CLIFF TERRACING (mesa/butte benches) -- after carves so canyon walls + risers compose
665
- 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);
666
675
  h += cliffCarveV;
667
676
  // FLAT RIVER WATER
668
677
  float riverWetLine = riverWetMask * riverWet * step(0.0, h);
@@ -794,7 +803,7 @@ void main() {
794
803
  float mtn = smoothstep(16.8, 18.6, bAmp); // mountain-belt weight 0..1
795
804
  float wetLowFlat = smoothstep(0.66, 0.9, bHum) * (1.0 - mtn); // swamp/wetland -> flat
796
805
  float coldFlat = (1.0 - smoothstep(0.18, 0.34, bTemp)); // tundra/ice -> flat
797
- 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);
798
807
  float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0); // ridged crests only in mountain belts
799
808
  // ISLAND-TYPE VARIETY: small offshore swells get a per-region type (volcanic cone / low atoll)
800
809
  // from a world-dir noise so islands are not all scaled continents. Pure fn of world dir.
@@ -817,13 +826,24 @@ void main() {
817
826
  if (vH < 0.0) {
818
827
  highp float dSea = -vH;
819
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;
820
833
  }
821
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;
822
842
  // LAKE CARVE: carve a real basin into the elevation on WET LAND so lakes are part of the
823
- // fractal (no fade-in). Gated to land (vH>0) + wet regions (humid) so it does not pit deserts
824
- // 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.
825
844
  float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
826
- 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);
827
847
  float lakeCarveV = lakeCarveRaw * lakeGate;
828
848
  vH += lakeCarveV;
829
849
  // FLAT WATER: inside the wet core the surface must read as flat water (user: 'water should be
@@ -847,8 +867,7 @@ void main() {
847
867
  // by the small depths + land gate; coastline hypso re-witnessed in incision-hypso-landfrac-gate).
848
868
  float riverWet = smoothstep(0.30, 0.55, hpf0.a) * smoothstep(0.20, 0.34, hpf0.b); // moist, not frozen
849
869
  float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, vH);
850
- float canyonArid = (1.0 - smoothstep(0.40, 0.58, hpf0.a)) * smoothstep(0.38, 0.56, hpf0.b) * smoothstep(60.0, 200.0, vH);
851
- float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, vH);
870
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * step(0.0, vH);
852
871
  // NO-SUB-SEA-COAST GUARD (user 2026-06-02 deepen+widen): with the deepened canyon (-1400m + gullies)
853
872
  // a gorge on 200m coastal land would punch vH to ~-1200m = fake inland seas. Clamp the TOTAL incision
854
873
  // so post-carve land bottoms out at a small floor (-60m: a gorge may reach near sea level but never
@@ -862,7 +881,7 @@ void main() {
862
881
  // arid badlands look). The snap delta is added to vH; cliffFaceMask (->1 on a riser face) goes to
863
882
  // the FS for strata banding + steep-rock material. Applied AFTER carves so canyon walls + cliff
864
883
  // risers compose. step(0,vH) keeps it on land.
865
- 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);
866
885
  vH += cliffCarveV;
867
886
  // FLAT RIVER WATER (user: 'rivers/lakes should NOT be bumpy'): like lakes, the river thalweg
868
887
  // surface must read as flat water, not the bumpy micro-relief floor. Pin vH down to the channel
@@ -920,22 +939,48 @@ void main() {
920
939
  // ALL THREE taps through ONE runtime-bounded loop forces FXC to emit a SINGLE composeHeight instance,
921
940
  // so whatever approximations it picks cancel exactly in the differences. uNrmStepM>0.0 keeps the
922
941
  // bound non-constant (same defeat class as uOctMax).
923
- highp vec3 uzFD = dir0;
924
- vec3 refAxisFD = (abs(uzFD.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
925
- vec3 uxFD = normalize(cross(refAxisFD, uzFD));
926
- vec3 uyFD = cross(uzFD, uxFD);
927
- highp float nStepFD = (uNrmStepM > 0.0) ? uNrmStepM : 150.0;
928
- 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;
929
947
  if (uIsWater < 0.5) {
930
- 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)
931
970
  for (int i = 0; i < fdIters; i++) {
932
- highp vec3 dd = (i == 0) ? dir0
933
- : normalize(uzFD + ((i == 1) ? uxFD : uyFD) * (nStepFD / defRadius));
934
- highp float hi = composeHeight(dd, faceLocal, defOffset.z);
935
- 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; }
936
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)
937
982
  }
938
- highp float h = hFD0;
983
+ highp float h = hN0;
939
984
  // OCEAN TOP = A SEPARATE, ELEVATION-BASED SURFACE (user 2026-06-10: 'terrain should extend into
940
985
  // the ocean, the ocean top separate and elevation based'). composeHeight keeps carrying the TRUE
941
986
  // signed bathymetry (vH -> the FS depth tint/Beer-Lambert keys on it), but the RENDERED surface
@@ -949,34 +994,8 @@ void main() {
949
994
  // and shades it as the animated ocean, alpha-blended over the seabed (depth test keeps it
950
995
  // behind land).
951
996
  highp float hR = (uIsWater > 0.5) ? 0.0 : h;
952
- // W8 ANALYTIC LIT NORMAL (P1 data-first): the normal is a property of the height FIELD, so derive it
953
- // from the field, not from per-pixel screen derivatives. FIXED-WORLD-STEP central difference of
954
- // composeHeight in the radial tangent frame -- STABLE (does NOT shrink with distance/per-cell -> no
955
- // shimmer, see :769), full landform relief at ALL distances, no fade. Mirror of the proven nBand
956
- // pattern but the FULL composeHeight (not macro-only broadShapeLowM). Offset ONLY dir0 (the world-dir
957
- // gradient = the seam-safe landform relief); hold faceLocal/tileM fixed so the tile-local vtxDisplace
958
- // term cancels in the difference (it carries no world-continuous gradient and would only add jitter).
959
- vec3 uzN = dir0; // dir0 is already normalized; radial up
960
- vec3 refAxisN = (abs(uzN.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
961
- vec3 uxN = normalize(cross(refAxisN, uzN));
962
- vec3 uyN = cross(uzN, uxN);
963
- // FXC FOLD-DEFEAT (2026-06-12, same disease as the uOctMax unroll fix): with a LITERAL 150.0 the
964
- // d3d11/FXC translator constant-folds/reassociates the tiny FD offset (150/R = 2.4e-5) feeding
965
- // normalize(uzN + uxN*eps) -- the perturbed-direction taps then diverge wildly from h on FLAT
966
- // ground (witnessed: probe slope 0.000 + flat wireframe mesh under whole regions the FS painted
967
- // as rock; the FD normal claimed steep in noise-keyed PATCHES = 'rock without slope angle').
968
- // A uniform-fed step denies FXC the constant, exactly like uOctMax denies it the loop bound.
969
- highp float nStep = (uNrmStepM > 0.0) ? uNrmStepM : 150.0; // STABLE world step (m): > finest vtxDisplace octave (~20m, no alias),
970
- // < carve band (~100m+, resolves canyons/cliffs). NOT pxWorld-scaled.
971
- if (uIsWater > 0.5) {
972
- vNrm = uzN; // water surface: radial normal; waves perturb it per-pixel in the FS (skips 2 composeHeight taps)
973
- } else {
974
- // taps come from the SINGLE-INSTANCE FD loop above (hFD0/1/2) -- never call composeHeight at
975
- // separate call sites for the FD again; per-callsite FXC codegen divergence was the AMD root.
976
- float dHduN = float(hFD1 - h) / nStep;
977
- float dHdvN = float(hFD2 - h) / nStep;
978
- vNrm = normalize(uxN * (-dHduN) + uyN * (-dHdvN) + uzN); // n = normalize([-dz/du,-dz/dv,1]) in the (ux,uy,uz) frame
979
- }
997
+ // VERTEX NORMAL: central-difference (computed above) for land; radial for water.
998
+ vNrm = (uIsWater > 0.5) ? dir0 : vN;
980
999
  // DIRECT per-vertex sphere projection (replaces the old corner-blend deform, which
981
1000
  // bilinearly interpolated 4 deformed corners -> FLAT quad interior -> faceted at high GRID).
982
1001
  // dir0 is THIS vertex's world direction (faceWarp'd, defLocalToWorld-mapped); place it on the
@@ -1016,8 +1035,8 @@ void main() {
1016
1035
  // = metres the water plane sits above the pre-flatten carved floor, >0 ONLY inside the true open
1017
1036
  // water; the FS gates ALL inland-water shading on this so blue == the flat water, banks stay land.
1018
1037
  vWaterDepth = max(waterPlane - floorBefore, 0.0) * step(0.001, haveWater);
1019
- vCanyonDep = canyonDepMask * canyonArid * step(0.0, vH);
1020
- vCliffFace = cliffFaceMask * canyonArid * step(0.0, vH);
1038
+ vCanyonDep = canyonDepMask * step(0.0, vH);
1039
+ vCliffFace = cliffFaceMask * step(0.0, vH);
1021
1040
  vDuneCrest = duneCrest * duneSand;
1022
1041
  vGrid = vertex.xy; // parametric mesh-cell coord for the wireframe overlay
1023
1042
  vLevel = defOffset.w; // quad LOD level -> FS patches view
@@ -1157,13 +1176,9 @@ vec3 terrainAlbedo(float h, float slope, float rockSlope, highp vec3 worldPos) {
1157
1176
  } else {
1158
1177
  c = mix(bcShore, bcLowland, smoothstep(0.0, bandEdgesLo.x, h));
1159
1178
  c = mix(c, bcGrass, smoothstep(bandEdgesLo.x, bandEdgesLo.y, h));
1160
- // ROCK IS VERTICALITY-DRIVEN, NOT HEIGHT-SPLATTED (user 2026-06-03: 'rock face based on
1161
- // verticality not splatted directly on albedo'). The old height-band rock mix
1162
- // (bcRock by bandEdgesHi) painted tan rock flat across every high plateau/summit regardless
1163
- // of slope, so flat high ground read as rock. REMOVED. High flat ground now keeps its biome
1164
- // (and snow by height below); ONLY the slope/verticality term and the steep cliff term turn
1165
- // rock. Snow still bands by altitude (a flat snowfield is physically correct).
1166
- 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));
1167
1182
  c = mix(c, bcRock, smoothstep(slopeRock.x, slopeRock.y, rockSlope) * step(0.0, h));
1168
1183
  // OLD PROCEDURAL GREY ROCKFACE DELETED (max-speed sweep 2026-06-10, user 'replace the original
1169
1184
  // rock completely'): the photo-rock splat owns steep faces; the 3-tap grey fBm fallback is gone.
@@ -1281,8 +1296,6 @@ float canyonMask(vec3 worldPos, float h, float temp, float humid, float px, out
1281
1296
  depth = 0.0;
1282
1297
  // gentle elevation gate: canyons want SOME relief above the lowland, not only rare high
1283
1298
  // plateaus (h>250 made them NEVER appear -- witnessed canyonFieldFrac 0). Fade in over 60..200m.
1284
- float elevGate = smoothstep(60.0, 200.0, h);
1285
- if (elevGate <= 0.0) return 0.0;
1286
1299
  // ONE FRACTAL (user 2026-06-02: 'canyon field shows two different resolutions, expected the same
1287
1300
  // fractal'). The FS used its OWN inline loop over worldPos/terrainR (= dir*(1+h/R), elevation-
1288
1301
  // SHIFTED off the VS sample) -> a different field/position/resolution than the VS carve. Now call
@@ -1292,11 +1305,9 @@ float canyonMask(vec3 worldPos, float h, float temp, float humid, float px, out
1292
1305
  float wid = clamp(px * 0.0006, 0.0, 0.03); // narrower than rivers
1293
1306
  float line = smoothstep(0.875 - wid, 0.94, ridge); // ~river-density threshold so they appear
1294
1307
  depth = smoothstep(0.875, 0.95, ridge); // deeper toward the channel centre
1295
- float aridGate = (1.0 - smoothstep(0.40, 0.58, humid)); // DRY (deserts/steppe), opposite of rivers
1296
- float warmGate = smoothstep(0.38, 0.56, temp); // warm arid plateaus
1297
1308
  // NO altFade (user: canyon field fading instead of integrated). wid grows with px -> >=1px AA at
1298
1309
  // every distance without vanishing; the canyon network is permanent, not a distance overlay.
1299
- return line * aridGate * warmGate * elevGate;
1310
+ return line * step(0.0, h);
1300
1311
  }
1301
1312
  #endif // biomeClassColor/riverMask/canyonMask: DEBUGVIEW-only (called solely from displayMode blocks) -- excluded from render FS cold-compile (FS-2, workflow w4y1bnrqc)
1302
1313
 
@@ -1339,7 +1350,8 @@ vec3 terrainAlbedoClimate(float h, float slope, float rockSlope, float temp, flo
1339
1350
  // must stop and become sand, which continues under the water'): below uBeachTopM the biome/
1340
1351
  // grass/snow color yields to shore sand (the same bcShore the underwater bed starts from, so the
1341
1352
  // material is continuous through the waterline). Steep coastal cliffs keep their rock.
1342
- 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))
1343
1355
  * (1.0 - smoothstep(slopeRock.x, slopeRock.y, rockSlope));
1344
1356
  c = mix(c, bcShore, beachM);
1345
1357
  // INLAND WATER -- COLOUR GATED BY THE CARVE FIELD (alignment + no-fade fix, browser-2705/2699).
@@ -1381,7 +1393,7 @@ vec3 terrainAlbedoClimate(float h, float slope, float rockSlope, float temp, flo
1381
1393
  {
1382
1394
  highp vec3 od = normalize(worldPos);
1383
1395
  float ov = 0.0, oa = 0.0;
1384
- 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)
1385
1397
  int fdOcts = (uFSDetailOcts > 0) ? uFSDetailOcts : 3;
1386
1398
  for (int o = 0; o < fdOcts; o++) {
1387
1399
  float wl = 40000000.0 / fq; // feature wavelength (m) ~ 2*pi*R / fq
@@ -1540,7 +1552,7 @@ void main() {
1540
1552
  // (which made fine relief go missing). It is a continuous function of dir0 only -- adjacent fragments
1541
1553
  // get the linearly-interpolated VS value, so it is smooth by construction (no dFdx/dFdy, no fwidth).
1542
1554
  // uFlatNormal kept as a cheap escape hatch: pure sphere normal (uz) for A/B isolation.
1543
- vec3 n = (uFlatNormal > 0.5) ? uz : normalize(vNrm);
1555
+ vec3 n = (uFlatNormal > 0.5) ? uz : normalize(mix(vNrm, uz, 0.05));
1544
1556
 
1545
1557
  // GPU-TIMER VS-ISOLATION: a cheap measurement frame short-circuits the whole per-pixel shade
1546
1558
  // here (uses vNrmPV + vWorld so the VS outputs are not dead-code-eliminated) -> the timed cheap
@@ -1662,21 +1674,50 @@ void main() {
1662
1674
  float dryHot = smoothstep(0.60, 0.85, 1.0 - climate.w) * smoothstep(0.42, 0.62, climate.z);
1663
1675
  // BEACH (user 2026-06-10 'the beaches should be sand'): the shore profile eases over the
1664
1676
  // first ~60m of elevation, so low gentle land near sea level splats sand regardless of climate.
1665
- 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));
1666
1693
  // SAND REGIONS SUPPRESS ROCK (user 2026-06-10 'rock being used instead of sand'): in
1667
1694
  // deserts/dunes/beaches sand drapes moderate slopes; rock only wins on genuinely steep faces
1668
1695
  // there (gate shifted toward 0.5-0.7 inside sand regions instead of slopeRock 0.28-0.55).
1669
- 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);
1670
1697
  // SPLAT ROCK GATE DECOUPLED from the macro slopeRock (user 2026-06-11 'a lot of grass turning
1671
1698
  // into rocky patches again'): the user-calibrated global soft blend slopeRock [-0.6,1] puts
1672
1699
  // ~37% ROCK LAYER weight on perfectly flat ground, and the displacement-sharpened top-2
1673
1700
  // crossfade then flips whole displacement blobs to the rock PHOTO on grassland. The macro
1674
1701
  // color blend keeps the calibrated tone; the rock TEXTURE layer needs real slope: floor the
1675
1702
  // splat gate at 0.18 so flat/gentle land splats grass, rock starts on genuine slopes.
1676
- float srLo = max(slopeRock.x, 0.18), srHi = max(slopeRock.y, srLo + 0.2);
1677
- float wRock = smoothstep(mix(srLo, 0.50, sandRegion), mix(srHi, 0.70, sandRegion), rockSlope);
1678
- float snowHi = smoothstep(snowEdges.x, snowEdges.y, vH);
1679
- 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);
1680
1721
  // POLAR/ICE-BIOME SNOW (user 2026-06-10 'put the snow texture on the snow'): the ICE biome
1681
1722
  // whitens cold lowland (biomeColor gate 1-smoothstep(0.10,0.18,tempEff)) at ANY elevation, but
1682
1723
  // wSnow was elevation-gated only -- polar snowfields were splatting the GRASS layer. Match the
@@ -1686,13 +1727,14 @@ void main() {
1686
1727
  - 0.18 * (abs(lat2) / 1.5708) * uBiomeBandBias, 0.0, 1.0);
1687
1728
  float iceClimate = (1.0 - smoothstep(0.10, 0.20, tempEff2)) * step(0.0, vH); // no snow layer on the seabed
1688
1729
  float wSnow = clamp(snowHi + 0.7 * snowCold + iceClimate, 0.0, 1.0) * (1.0 - 0.6 * wRock);
1689
- 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));
1690
1731
  float wGrass = max(1.0 - wRock - wSnow - wSand, 0.0);
1691
1732
  vec4 w4 = vec4(wGrass, wRock, wSand, wSnow); // layers 0..3 = grass,rock,sand,snow
1692
1733
  // BEACH BAND + UNDERWATER in one mask (user 2026-06-11): sand owns everything below the
1693
1734
  // beach ceiling uBeachTopM -- grass/snow weight folds into sand continuously through h=0 to
1694
1735
  // the seabed, so no vegetation can ever splat at or below the waterline (structural).
1695
- 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);
1696
1738
  w4.z += (w4.x + w4.w) * uwM; w4.x *= 1.0 - uwM; w4.w *= 1.0 - uwM;
1697
1739
  w4 /= (w4.x + w4.y + w4.z + w4.w + 1e-4);
1698
1740
  // top-2 layer pick: 4-way blending would cost 24 taps; the gates are spatially near-exclusive,
@@ -1734,7 +1776,10 @@ void main() {
1734
1776
  // displacement term 0.8 -> 0.3 (user 2026-06-11 'still see bowls of rock texture'):
1735
1777
  // 0.8 still let the displacement photo's bowl-shaped blobs flip whole patches to rock
1736
1778
  // inside the transition band; 0.3 only feathers the seam edge.
1737
- 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);
1738
1783
  texAlb = mix(albB, albA, bSharp);
1739
1784
  texNrm = mix(nrmB, nrmA, bSharp);
1740
1785
  }
@@ -1850,7 +1895,16 @@ void main() {
1850
1895
  // GENTLE-SLOPE RELIEF SHADING (user 2026-06-10 'even gentle slopes shaded so terrain elevation
1851
1896
  // is obvious'): exaggerate the tangential tilt of the LIT normal only -- the material gates keep
1852
1897
  // the true geometric n, so placement is unchanged; lighting contrast on rolling ground increases.
1853
- 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
+ }
1854
1908
  // photo-texture detail normal at its calibrated amplitude, OUTSIDE the relief exaggeration.
1855
1909
  // ADDED in world space (the old `- ux*dn.x - uy*dn.y` subtracted an already-negated Sobel
1856
1910
  // normal in a frame the triplanar RG was never expressed in -- the fundamental both-at-once
@@ -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)