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 +1 -1
- package/planet.html +23 -15
- package/server.js +5 -3
- package/src/gl-render.js +65 -20
- package/src/planet-orchestrator.js +44 -11
- package/src/shaders/terrain.glsl +241 -133
- package/src/terrain-gen-controls.js +1 -1
package/package.json
CHANGED
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=
|
|
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.
|
|
109
|
-
return sum-
|
|
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)) ?
|
|
481
|
-
:
|
|
482
|
-
const altSurfElev = Math.
|
|
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 =
|
|
535
|
+
renderedM = gpuM + 1.0; // +1m margin for sub-vertex micro-relief (vtxDisplace peaks exceed 2m in rugged terrain)
|
|
526
536
|
} else {
|
|
527
|
-
renderedM =
|
|
537
|
+
renderedM = jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2]) + 3.0; // mirror fallback (CPU under-predicts)
|
|
528
538
|
}
|
|
529
|
-
// cap
|
|
530
|
-
|
|
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 +
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 ||
|
|
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',
|
|
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
|
-
|
|
635
|
-
|
|
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
|
|
656
|
-
//
|
|
657
|
-
//
|
|
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(
|
|
664
|
-
const
|
|
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
|
|
700
|
-
//
|
|
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',
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
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 =
|
|
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
|
-
&&
|
|
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 =
|
|
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
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
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.
|
package/src/shaders/terrain.glsl
CHANGED
|
@@ -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.
|
|
151
|
-
float bowl = smoothstep(0.
|
|
152
|
-
wet = smoothstep(0.
|
|
153
|
-
return -LAKE_CARVE_DEPTH * (0.
|
|
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),
|
|
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.
|
|
194
|
-
float thalweg = smoothstep(0.
|
|
195
|
-
wet = smoothstep(0.
|
|
196
|
-
return -RIVER_INCISE_DEPTH * (0.
|
|
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),
|
|
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.
|
|
236
|
-
float floorF= smoothstep(0.
|
|
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),
|
|
250
|
-
float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2),
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
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 =
|
|
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 -
|
|
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 =
|
|
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 -
|
|
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 *
|
|
443
|
-
float e1 = snoise3(d *
|
|
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 =
|
|
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; //
|
|
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 -
|
|
473
|
-
// GLOBAL ELEVATION SCALE 0.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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 *
|
|
637
|
-
// FLAT-AREA
|
|
638
|
-
//
|
|
639
|
-
// by reliefMul ~0.5
|
|
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
|
-
|
|
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
|
-
//
|
|
645
|
-
|
|
646
|
-
float lakeGate = smoothstep(
|
|
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
|
|
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) *
|
|
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.
|
|
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).
|
|
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
|
|
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
|
|
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) *
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
highp
|
|
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
|
-
|
|
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
|
|
932
|
-
|
|
933
|
-
highp
|
|
934
|
-
|
|
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 =
|
|
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
|
-
//
|
|
952
|
-
|
|
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 *
|
|
1019
|
-
vCliffFace = cliffFaceMask *
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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 *
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
float
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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:
|
|
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)
|