mapspinner 0.1.33 → 0.1.35
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 +16 -6
- package/server.js +5 -3
- package/src/gl-render.js +91 -34
- package/src/planet-orchestrator.js +44 -11
- package/src/shaders/terrain.glsl +186 -132
- 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); });
|
|
@@ -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; //
|
|
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
|
-
|
|
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; }
|
|
@@ -566,6 +569,13 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
566
569
|
const vbo=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,vbo); gl.bufferData(gl.ARRAY_BUFFER,verts,gl.STATIC_DRAW);
|
|
567
570
|
const ibo=gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
|
|
568
571
|
const instBuf=gl.createBuffer(); // per-instance [ox,oy,l,level,face] (filled per frame in render())
|
|
572
|
+
// DATA-CONTINUITY CACHE (2026-06-14): terrain + water get their OWN persistent instance buffers so
|
|
573
|
+
// neither clobbers the other (the shared-buffer clobber forced a re-upload every frame and was the
|
|
574
|
+
// root of the prior 'water drawn as terrain' regression). On a STATIC frame (same quads array object)
|
|
575
|
+
// the instance data is identical -> skip the Float32Array build + bufferData + water dedup Set-loop
|
|
576
|
+
// and just rebind+draw. Pure CPU/GC win (GPU is vertex-bound, the upload is off the critical path).
|
|
577
|
+
const instBufWater=gl.createBuffer();
|
|
578
|
+
let _instQuadsRef=null, _instWaterRef=null, _instWaterN=0;
|
|
569
579
|
|
|
570
580
|
// per-face local->world (cube face -> sphere local frame). Column-major mat3 packed
|
|
571
581
|
// into a Float32Array(9). Matches localToWorld3 convention:
|
|
@@ -630,9 +640,17 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
630
640
|
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
|
631
641
|
const camDist = Math.hypot(cam.eye[0], cam.eye[1], cam.eye[2]);
|
|
632
642
|
const alt = Math.max(0.0, camDist - R);
|
|
643
|
+
const altAboveTerrain = Math.max(0.001, alt - R * (cam.surfElev || 0));
|
|
633
644
|
const horizon = Math.sqrt(Math.max(0.0, camDist*camDist - R*R));
|
|
634
|
-
|
|
635
|
-
|
|
645
|
+
// MATCH render()'s near exactly (2026-06-14 jank fix): the cull frustum must use the SAME near
|
|
646
|
+
// as the draw frustum, else behind-limb/screen-AABB culling diverges from what is actually drawn
|
|
647
|
+
// at the deck (cull near was max(*0.1,0.1) while render used the <2m 0.05 branch).
|
|
648
|
+
const near = altAboveTerrain < 2.0 ? 0.05 : Math.max(altAboveTerrain * 0.1, 0.05);
|
|
649
|
+
// FAR PLANE: horizon distance tracks the visible ground edge; blends toward camDist
|
|
650
|
+
// above 500km for orbital views so the full planet is visible.
|
|
651
|
+
const _fBlend = Math.min(1.0, Math.max(0.0, (alt - 500000.0) / 4500000.0));
|
|
652
|
+
const farGround = Math.max(horizon, alt * 8.0);
|
|
653
|
+
const far = farGround * (1.0 - _fBlend) + camDist * _fBlend;
|
|
636
654
|
const proj = M4.perspective(cam.fovy||0.785, aspect, near, far);
|
|
637
655
|
const eye = cam.eye;
|
|
638
656
|
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 +661,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
643
661
|
// fp32 cancellation at ground level (eye~=world), garbaging the projection and blanking the
|
|
644
662
|
// footprint. Subtracting in JS doubles first keeps the cull's projection precise near ground.
|
|
645
663
|
const viewProjNoEye = M4.mul(proj, viewRel);
|
|
646
|
-
return { viewProjRel, viewProjNoEye, eye, near, far, proj };
|
|
664
|
+
return { viewProjRel, viewProjNoEye, eye, near, far, proj, viewRel };
|
|
647
665
|
}
|
|
648
666
|
|
|
649
667
|
// Render a set of quads. quads: [{quad, face, elevLayer, normalLayer}], cam: {eye, center, up, fovy}
|
|
@@ -652,16 +670,18 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
652
670
|
// ADAPTIVE near/far (altitude-tied). A fixed near=1 / far=R*8 (~5e7) at a 50km eye
|
|
653
671
|
// pushed ALL near-surface geometry to NDC z~=1 (the far-plane limit), collapsing depth
|
|
654
672
|
// 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
|
|
673
|
+
// survived. Tie the planes to altitude: near = alt*0.1 (naturally scales from 1m at
|
|
674
|
+
// deck to 1200km at orbit), far = horizon distance blended toward camDist above 500km
|
|
675
|
+
// for orbital views. From space the far widens out to ~R*8, preserving the full-globe
|
|
659
676
|
// view. Clamped so near>=1 and far>near.
|
|
660
677
|
const camDist = Math.hypot(cam.eye[0], cam.eye[1], cam.eye[2]);
|
|
661
678
|
const alt = Math.max(0.0, camDist - R);
|
|
679
|
+
const altAboveTerrain = Math.max(0.001, alt - R * (cam.surfElev || 0));
|
|
662
680
|
const horizon = Math.sqrt(Math.max(0.0, camDist*camDist - R*R));
|
|
663
|
-
const near = Math.max(
|
|
664
|
-
const
|
|
681
|
+
const near = altAboveTerrain < 2.0 ? 0.05 : Math.max(altAboveTerrain * 0.1, 0.05);
|
|
682
|
+
const _fBlend = Math.min(1.0, Math.max(0.0, (alt - 500000.0) / 4500000.0));
|
|
683
|
+
const farGround = Math.max(horizon, alt * 8.0);
|
|
684
|
+
const far = farGround * (1.0 - _fBlend) + camDist * _fBlend;
|
|
665
685
|
const proj = M4.perspective(cam.fovy||0.785, aspect, near, far);
|
|
666
686
|
const view = M4.lookAt(cam.eye, cam.center, cam.up||[0,1,0]);
|
|
667
687
|
const viewProj = M4.mul(proj, view);
|
|
@@ -696,8 +716,28 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
696
716
|
gl.viewport(0,0,gl.drawingBufferWidth,gl.drawingBufferHeight);
|
|
697
717
|
gl.clearColor(0.0,0.0,0.0,1); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
|
|
698
718
|
|
|
699
|
-
// SKY/ATMOSPHERE PASS
|
|
700
|
-
//
|
|
719
|
+
// SKY/ATMOSPHERE PASS: atmospheric limb/halo behind the terrain. Fades in below
|
|
720
|
+
// 100km (full at surface, transparent above 100km). Depth off so terrain overdraws.
|
|
721
|
+
{
|
|
722
|
+
const skyFade = Math.max(0.0, 1.0 - camAlt / 100000.0);
|
|
723
|
+
if (skyFade > 0.001) {
|
|
724
|
+
gl.useProgram(skyProg);
|
|
725
|
+
gl.uniformMatrix3fv(SU('camRot'), false, new Float32Array([
|
|
726
|
+
_cm.viewRel[0], _cm.viewRel[4], _cm.viewRel[8],
|
|
727
|
+
_cm.viewRel[1], _cm.viewRel[5], _cm.viewRel[9],
|
|
728
|
+
_cm.viewRel[2], _cm.viewRel[6], _cm.viewRel[10]
|
|
729
|
+
]));
|
|
730
|
+
gl.uniform2f(SU('projDiag'), _cm.proj[0], _cm.proj[5]);
|
|
731
|
+
gl.uniform3f(SU('skyCamWorld'), eye[0], eye[1], eye[2]);
|
|
732
|
+
gl.uniform3f(SU('skySunDir'), sunDir[0], sunDir[1], sunDir[2]);
|
|
733
|
+
gl.uniform1f(SU('skyR'), R);
|
|
734
|
+
gl.uniform1f(SU('uSkyFade'), skyFade);
|
|
735
|
+
gl.disable(gl.DEPTH_TEST);
|
|
736
|
+
gl.bindVertexArray(skyVao);
|
|
737
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
738
|
+
gl.enable(gl.DEPTH_TEST);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
701
741
|
|
|
702
742
|
gl.enable(gl.DEPTH_TEST);
|
|
703
743
|
// Back-face culling: the globe's FAR (back) hemisphere quads were drawing over the
|
|
@@ -766,6 +806,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
766
806
|
// literals so the look is unchanged until the user dials a window global.
|
|
767
807
|
const _g = (n,d)=> (typeof window!=='undefined' && window['__'+n]!=null) ? +window['__'+n] : d;
|
|
768
808
|
gl.uniform1f(U('canyonDepthMul'), _g('canyonDepth', 1.0));
|
|
809
|
+
gl.uniform1f(U('uBeachShelfM'), _g('beachShelf', 2400.0)); // land coastal shelf (geometry): h<S eased h*h/S = wide beach
|
|
769
810
|
gl.uniform1f(U('uHiFreqCut'), _g('hiFreqCut', 0.25)); // 0.5->0.25 (2026-06-10 'blotchy' -- see setComposeHeightUniforms)
|
|
770
811
|
gl.uniform1f(U('uVertexAO'), _g('vertexAO', 1.0)); // per-vertex shading/AO strength (DEFECT 2, 2026-06-06)
|
|
771
812
|
gl.uniform1f(U('cliffAmt'), _g('cliffAmt', 1.0));
|
|
@@ -844,7 +885,7 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
844
885
|
gl.uniform1f(U('oceanAmp'), (oc.oceanAmplitude != null) ? oc.oceanAmplitude : 1.0);
|
|
845
886
|
gl.uniform1f(U('oceanChoppy'), (oc.oceanChoppiness != null) ? oc.oceanChoppiness : 0.5);
|
|
846
887
|
gl.uniform1f(U('oceanFoam'), (oc.oceanFoam != null) ? oc.oceanFoam : 0.5);
|
|
847
|
-
gl.uniform1f(U('uBeachTopM'), _g('beachTop',
|
|
888
|
+
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
889
|
|
|
849
890
|
// SINGLE INSTANCED DRAW: the deform params that were per-quad uniforms (ox,oy,l,level + face)
|
|
850
891
|
// are now PER-INSTANCE attributes. Build one interleaved instance buffer [ox,oy,l,level,face]
|
|
@@ -858,14 +899,19 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
858
899
|
// h = composeHeight(dir) for each vertex.
|
|
859
900
|
const FLOATS = 5; // [ox,oy,l,level,face]
|
|
860
901
|
if (n > 0) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
inst[i*FLOATS+0] = q.ox; inst[i*FLOATS+1] = q.oy; inst[i*FLOATS+2] = q.l; inst[i*FLOATS+3] = q.level;
|
|
865
|
-
inst[i*FLOATS+4] = quads[i].face;
|
|
866
|
-
}
|
|
902
|
+
// STATIC-FRAME SKIP: only rebuild + re-upload the instance data when the quad SET changed (the
|
|
903
|
+
// orchestrator passes the SAME quads array object on a static camera, a fresh array on rebuild).
|
|
904
|
+
const _dirty = (quads !== _instQuadsRef);
|
|
867
905
|
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
868
|
-
|
|
906
|
+
if (_dirty) {
|
|
907
|
+
const inst = new Float32Array(n * FLOATS);
|
|
908
|
+
for (let i = 0; i < n; i++) {
|
|
909
|
+
const q = quads[i].quad;
|
|
910
|
+
inst[i*FLOATS+0] = q.ox; inst[i*FLOATS+1] = q.oy; inst[i*FLOATS+2] = q.l; inst[i*FLOATS+3] = q.level;
|
|
911
|
+
inst[i*FLOATS+4] = quads[i].face;
|
|
912
|
+
}
|
|
913
|
+
gl.bufferData(gl.ARRAY_BUFFER, inst, gl.DYNAMIC_DRAW);
|
|
914
|
+
}
|
|
869
915
|
const STRIDE = FLOATS * 4;
|
|
870
916
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1); // iOffset
|
|
871
917
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1); // iFace
|
|
@@ -892,17 +938,24 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
892
938
|
// (~1.6km) sag ~5cm, far under any visible bathymetry, still ~16-64x fewer water verts
|
|
893
939
|
// than the deep terrain leaves.
|
|
894
940
|
const WCAP = 9;
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
941
|
+
// OWN persistent buffer (instBufWater) + static-frame skip: on an unchanged quad set, reuse the
|
|
942
|
+
// cached water instances (skip the dedup Set-loop + Float32Array + bufferData). Separate buffer
|
|
943
|
+
// means the terrain pass never clobbers it (the prior water-as-terrain regression root).
|
|
944
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instBufWater);
|
|
945
|
+
if (_dirty || quads !== _instWaterRef) {
|
|
946
|
+
const seen = new Set(); const wl = [];
|
|
947
|
+
for (let i = 0; i < n; i++) {
|
|
948
|
+
const q = quads[i].quad; let ox = q.ox, oy = q.oy, l = q.l, lv = q.level;
|
|
949
|
+
if (lv > WCAP) { const A = l * Math.pow(2, lv - WCAP); ox = Math.floor(ox / A) * A; oy = Math.floor(oy / A) * A; l = A; lv = WCAP; }
|
|
950
|
+
const key = quads[i].face + ':' + ox + ':' + oy + ':' + l;
|
|
951
|
+
if (seen.has(key)) continue; seen.add(key);
|
|
952
|
+
wl.push(ox, oy, l, lv, quads[i].face);
|
|
953
|
+
}
|
|
954
|
+
_instWaterN = wl.length / FLOATS;
|
|
955
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
|
|
956
|
+
_instWaterRef = quads;
|
|
902
957
|
}
|
|
903
|
-
const wn =
|
|
904
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
|
|
905
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
|
|
958
|
+
const wn = _instWaterN;
|
|
906
959
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1);
|
|
907
960
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1);
|
|
908
961
|
if (_uw) {
|
|
@@ -920,11 +973,15 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
920
973
|
gl.disable(gl.BLEND);
|
|
921
974
|
if (typeof window !== 'undefined') window.__lastWaterQuads = wn;
|
|
922
975
|
}
|
|
976
|
+
_instQuadsRef = quads; // mark this quad set uploaded; next frame with the same array skips the rebuild
|
|
977
|
+
if (typeof window !== 'undefined') window.__instUploads = (window.__instUploads | 0) + (_dirty ? 1 : 0);
|
|
923
978
|
}
|
|
924
979
|
if (typeof window !== 'undefined') window.__lastDrawCalls = (n > 0) ? 2 : 0;
|
|
925
|
-
return
|
|
980
|
+
return 0; // glError is checked via checkGlError() once per frame after quadtree (CPU/GPU pipelining)
|
|
926
981
|
}
|
|
927
982
|
|
|
983
|
+
function checkGlError() { return gl.getError(); }
|
|
984
|
+
|
|
928
985
|
// ---- DEBUG PROBE: replicate the VS clip-space transform on the CPU for a quad's 4
|
|
929
986
|
// corners (vertex.xy in {0,1}^2) so we can see which quads project off-screen.
|
|
930
987
|
function probe(quads, cam) {
|
|
@@ -956,5 +1013,5 @@ export async function initMapspinnerRender(gl, opts = {}) {
|
|
|
956
1013
|
return out;
|
|
957
1014
|
}
|
|
958
1015
|
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 };
|
|
1016
|
+
return { get prog(){ return prog; }, render, checkGlError, probe, sampleGroundM, cullMatrix, recompile, setHpf, GRID, indexCount: indices.length, M4 };
|
|
960
1017
|
}
|
|
@@ -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)
|
|
@@ -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),
|
|
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.
|
|
237
|
-
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
|
|
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),
|
|
251
|
-
float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2),
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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 =
|
|
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 -
|
|
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 =
|
|
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 -
|
|
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 *
|
|
444
|
-
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;
|
|
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 =
|
|
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; //
|
|
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 -
|
|
474
|
-
// GLOBAL ELEVATION SCALE 0.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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 *
|
|
638
|
-
// FLAT-AREA
|
|
639
|
-
//
|
|
640
|
-
// 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.
|
|
641
649
|
float flatGate = max(0.0, 1.0 - reliefMul * 2.0);
|
|
642
|
-
|
|
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
|
-
//
|
|
646
|
-
|
|
647
|
-
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);
|
|
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
|
|
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) *
|
|
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.
|
|
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).
|
|
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
|
|
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
|
|
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) *
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
highp
|
|
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
|
-
|
|
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
|
|
933
|
-
|
|
934
|
-
highp
|
|
935
|
-
|
|
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 =
|
|
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
|
-
//
|
|
953
|
-
|
|
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 *
|
|
1020
|
-
vCliffFace = cliffFaceMask *
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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 *
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
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);
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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:
|
|
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)
|