mapspinner 0.1.1

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.
@@ -0,0 +1,2109 @@
1
+ // Proland authentic terrain render shader, ported to WebGL2 (#version 300 es) from
2
+ // vendor/proland-build/proland-git/terrain/examples/terrain2/terrainShader.glsl.
3
+ // VS: spherical deformation + CLOD blend (zf<->zc) of the wasm-driven elevation tiles.
4
+ // FS: sample normal + (placeholder) albedo, simple sun-lit Lambert with an ambient floor.
5
+ // Driven by the thin WebGL2 render layer (per-quad deformation uniforms + textureTile
6
+ // coords from the Proland wasm). No geometry/tess -> WebGL2-clean. The JS prepends
7
+ // #version + precision and compiles _VERTEX_ / _FRAGMENT_ separately.
8
+
9
+ // (Proland SamplerTile/textureTile atlas plumbing DELETED 2026-06-11 dead-code sweep: never set, never called.)
10
+
11
+ // --- DIRECT per-vertex sphere-projection uniforms (replaces the Proland corner-blend CLOD) ------
12
+ // SINGLE INSTANCED DRAW: defOffset (ox,oy,l,level) + the face frame are PER-INSTANCE now (one
13
+ // gl.drawElementsInstanced over the whole visible leaf set, no per-quad uniform churn). In the VS
14
+ // they come from instance attributes (iOffset + iFace); defRadius/defViewProjRel stay uniforms
15
+ // (same for every instance). The _PROBE_ collision program uses its own probeDir, not these.
16
+ // W7 highp ISLANDS: every world-scale quantity here is ~6.4e6 m and the projection cancels at that
17
+ // magnitude -- fp16 (mediump, range +-65504, ~3 sig digits) would shatter it. Declared highp under the
18
+ // mediump global default so the planet-scale fp32 cancellation fix (camera-relative vRel) stays intact.
19
+ uniform highp float defRadius; // R, sphere radius (~6.4e6 m)
20
+ uniform highp mat4 defViewProjNoEye; // proj*viewRel WITHOUT folded translate(-eye) -- for camera-relative VS pos
21
+ uniform highp vec3 defCamDir; // unit camera direction (eye/|eye|) -- vertex-jitter precision fix
22
+ uniform highp float defCamAlt; // camera altitude above the sphere (|eye|-R)
23
+
24
+ // HIERARCHICAL PARAMETER FIELD (HPF) continental texture (shared VS+FS): per-face 2D-arrays
25
+ // baked from anchor-field.js. W12 PACK (mob-w12): the old single RGBA32F (16B/texel) is split
26
+ // into TWO smaller textures -- hpfPool RG16F (R=seaBias[m], G=elevAmp; the two PRECISION-sensitive
27
+ // floats the geometry rides on) + hpfPool2 RG8 (R=temp, G=humid; both already [0,1] climate, 8-bit
28
+ // is ample). 4B+2B = 6B/texel vs 16B = 62% less HPF VRAM/bandwidth, with NO silhouette change
29
+ // (seaBias/elevAmp keep float precision; temp/humid quantize to 1/255, far below the biome
30
+ // soft-threshold widths). hpfSample DECODES both back into the same vec4 callers expect
31
+ // (seaBias, elevAmp, temp, humid) so every consumer (VS bias, vClimate, probe, heightbake) is
32
+ // unchanged. UNCONDITIONAL format (single version; no tier branch).
33
+ uniform sampler2DArray hpfPool; // RG16F: r=seaBias[m], g=elevAmp
34
+ uniform sampler2DArray hpfPool2; // RG8: r=temp[0,1], g=humid[0,1]
35
+ uniform int hasHpf;
36
+ uniform float uHpfInset; // HPF seam-inset sampler (window.__hpfInset; 0=centred bake default, 1=edge-inset seam->0). MUST match planet-orchestrator bakeFace fu mapping. Declared here (before hpfSample) so all stages see it.
37
+ // W11 FORMAT PROBE (NOT a quality tier): OES_texture_float_linear probed ONCE at init (gl-render.js
38
+ // sets this). 1 = the float/half-float atlas pools filter LINEAR in hardware -> the manual 4-tap
39
+ // bilinear in hpfSample collapses to a single hardware texture() call.
40
+ // 0 = the driver would silently fall back to NEAREST (the 'square steps / banding' regression) so the
41
+ // manual 4-tap is kept for correctness. Numerically equivalent either way -- a FORMAT-correctness
42
+ // branch, the one admissible runtime branch.
43
+ uniform int uFloatLinearOK;
44
+ // world unit dir -> cube face + uv[0,1] (matches anchor-field.js dirToFaceUV / FACE_FRAME).
45
+ void hpfFaceUV(vec3 d, out int face, out vec2 uv) {
46
+ vec3 a = abs(d);
47
+ float u, v, sc;
48
+ if (a.x >= a.y && a.x >= a.z) { sc = 1.0/a.x;
49
+ if (d.x > 0.0) { face = 0; u = -d.z*sc; } else { face = 1; u = d.z*sc; } v = d.y*sc; }
50
+ else if (a.y >= a.z) { sc = 1.0/a.y;
51
+ if (d.y > 0.0) { face = 2; v = -d.z*sc; } else { face = 3; v = d.z*sc; } u = d.x*sc; }
52
+ else { sc = 1.0/a.z;
53
+ if (d.z > 0.0) { face = 4; u = d.x*sc; } else { face = 5; u = -d.x*sc; } v = d.y*sc; }
54
+ uv = vec2(u*0.5 + 0.5, v*0.5 + 0.5);
55
+ }
56
+ highp vec4 hpfSample(vec3 dir) { // W7: R=seaBias is metres (~1600) -> highp so cbias keeps full precision
57
+ if (hasHpf == 0) return vec4(0.0, 1.0, 0.5, 0.5); // seaBias 0, elevAmp 1, temp/humid .5
58
+ int face; vec2 uv; hpfFaceUV(normalize(dir), face, uv);
59
+ // W11: when float-linear filters in hardware, one texture() per pool == the 4-tap result. The
60
+ // bake samples at texel centres so the centred (non-inset) hardware tap matches the manual one
61
+ // exactly; the inset bake keeps the manual taps (its tap centres are k/(sz-1), not the hardware
62
+ // (k+0.5)/sz grid) so it stays correct regardless.
63
+ // HARDWARE-TAP FAST PATH DISABLED for the crease fix: a single hardware texture() is HARDWARE
64
+ // BILINEAR = the SAME C0 texel-edge slope kink as raw manual weights, so on a float-linear GPU it
65
+ // bypassed the quintic-weight fix below and the seaBias crease persisted (user 2026-06-09). Always
66
+ // take the manual path so the quintic C2 weights apply to seaBias/elevAmp (geometry-critical). Cost:
67
+ // 4 taps vs 1, but only per-vertex in VS/PROBE (negligible; FS reads the interpolated varying).
68
+ // (uHpfInset>0.5 inset bake already used the manual path; this just unifies onto it always.)
69
+ // MANUAL BILINEAR (2026-06-06, user: 'elevation divided into squares -> square STEPS / STAIRS,
70
+ // should be smooth'). hpfSample drives vH = cbias + bShape (terrain.glsl ~743): cbias=seaBias and
71
+ // reliefMul/ridgeMul derive from this sample, so any quantization here steps the GEOMETRY in
72
+ // ~50km (128-texel/face) squares. The hpfPool texture is FLAGGED LINEAR (planet-orchestrator.js)
73
+ // but RGBA32F LINEAR filtering REQUIRES OES_texture_float_linear; if that extension is absent the
74
+ // driver SILENTLY falls back to NEAREST -> per-texel-cell constants -> the visible stairs (the
75
+ // orchestrator's own getExtension comment names this exact 'elevations look square from NEAREST'
76
+ // failure). Fix: bilinear in-shader so the field is smooth REGARDLESS of hardware float-linear --
77
+ // textureSize for the texel grid, 4 taps at the surrounding texel centres, 2D lerp. Tap coords are
78
+ // clamped to texel centres in [0.5/sz, 1-0.5/sz] = CLAMP_TO_EDGE equivalent, so this reproduces the
79
+ // intended hardware LINEAR exactly (no new cube-face seam) when the ext IS present, and fixes the
80
+ // steps when it is not.
81
+ vec2 sz = vec2(textureSize(hpfPool, 0).xy);
82
+ // SEAM-INSET MATCHED SAMPLER (hpf-seam-inset-bake, gated by uHpfInset to match the bake fu=x/(RES-1)).
83
+ // Inset: texel k sits at uv=k/(sz-1) (edge texels at 0 and 1), so t=uv*(sz-1) and the tap centres are
84
+ // k/(sz-1). Centred (default): texel k at uv=(k+0.5)/sz, t=uv*sz-0.5. Both must agree with the bake.
85
+ bool inset = uHpfInset > 0.5;
86
+ vec2 denom = inset ? (sz - 1.0) : sz;
87
+ vec2 t = inset ? (uv * denom) : (uv * sz - 0.5);
88
+ vec2 f = fract(t);
89
+ vec2 t0 = floor(t);
90
+ vec2 hb = 0.5 / sz; // half-texel in uv (clamp band, centred case)
91
+ vec2 c0 = inset ? (t0) / denom : clamp((t0 + 0.5) / sz, hb, 1.0 - hb);
92
+ vec2 c1 = inset ? (t0 + vec2(1.0, 0.0)) / denom : clamp((t0 + vec2(1.5, 0.5)) / sz, hb, 1.0 - hb);
93
+ vec2 c2 = inset ? (t0 + vec2(0.0, 1.0)) / denom : clamp((t0 + vec2(0.5, 1.5)) / sz, hb, 1.0 - hb);
94
+ vec2 c3 = inset ? (t0 + vec2(1.0, 1.0)) / denom : clamp((t0 + 1.5) / sz, hb, 1.0 - hb);
95
+ vec2 uv00 = clamp(c0, vec2(0.0), vec2(1.0));
96
+ vec2 uv10 = clamp(c1, vec2(0.0), vec2(1.0));
97
+ vec2 uv01 = clamp(c2, vec2(0.0), vec2(1.0));
98
+ vec2 uv11 = clamp(c3, vec2(0.0), vec2(1.0));
99
+ float ff = float(face);
100
+ // W12 DECODE: 4 bilinear taps from EACH packed texture, reassembled into the legacy
101
+ // vec4 (seaBias, elevAmp, temp, humid). Same uv/weights for both -> one bilinear field.
102
+ vec2 s00 = texture(hpfPool, vec3(uv00, ff)).rg; // (seaBias, elevAmp)
103
+ vec2 s10 = texture(hpfPool, vec3(uv10, ff)).rg;
104
+ vec2 s01 = texture(hpfPool, vec3(uv01, ff)).rg;
105
+ vec2 s11 = texture(hpfPool, vec3(uv11, ff)).rg;
106
+ // QUINTIC C2 bilinear weights (NOT raw f=fract(t)). Linear weights are C0 -> the interpolated
107
+ // seaBias/elevAmp SLOPE kinks at every texel-cell edge; seaBias is added RAW to h (cbias+bShape)
108
+ // so on a slope crossing a texel boundary that kink is a STRAIGHT ELEVATION CREASE (user 5 grids
109
+ // 2026-06-09: a vertical cliff at one grid column, 0.4-38m, all rows). Quintic w is value-identical
110
+ // at texel centres (w(0)=0,w(1)=1) so amplitude/field UNCHANGED -- only the inter-texel slope is C2.
111
+ // Same fix class as the vnoise2 quintic. SEAM-SAFE: the texel-centre clamp (above) is untouched.
112
+ vec2 w = f*f*f*(f*(f*6.0-15.0)+10.0);
113
+ vec2 se = mix(mix(s00, s10, w.x), mix(s01, s11, w.x), w.y);
114
+ vec2 t00 = texture(hpfPool2, vec3(uv00, ff)).rg; // (temp, humid)
115
+ vec2 t10 = texture(hpfPool2, vec3(uv10, ff)).rg;
116
+ vec2 t01 = texture(hpfPool2, vec3(uv01, ff)).rg;
117
+ vec2 t11 = texture(hpfPool2, vec3(uv11, ff)).rg;
118
+ vec2 th = mix(mix(t00, t10, w.x), mix(t01, t11, w.x), w.y); // same quintic C2 weights (temp/humid)
119
+ return vec4(se.x, se.y, th.x, th.y);
120
+ }
121
+
122
+ // 3D value noise of a world-space point (continuous everywhere on the sphere). SHARED by the VS
123
+ // (broadShapeM continuous-field shape) AND the FS (riverMask drainage network), so it must live
124
+ // in the common preamble -- NOT inside #ifdef _VERTEX_, else the FS sees no snoise3 and the
125
+ // fragment program fails to link ('snoise3: no matching overloaded function'), aborting renderer init.
126
+ // W7 highp ISLAND: snoise3/shash3 lattice inputs reach freq*dir ~1.4e4 (fine octaves) and rely on
127
+ // integer floor()/fract() cell precision -- fp16 (~3 digits) would collapse every cell to mush. ALL
128
+ // noise primitives + their P/p args are highp; the [-1,1] RESULT narrows back to the mediump default.
129
+ highp float shash3(highp vec3 p){ p=fract(p*0.3183099+vec3(0.1,0.2,0.3)); p+=dot(p,p.yzx+19.19); return fract((p.x+p.y)*p.z + (p.y+p.z)*p.x); }
130
+ float snoise3(highp vec3 P){ highp vec3 i=floor(P),f=fract(P); highp vec3 u=f*f*(3.0-2.0*f);
131
+ float n000=shash3(i),n100=shash3(i+vec3(1,0,0)),n010=shash3(i+vec3(0,1,0)),n110=shash3(i+vec3(1,1,0));
132
+ float n001=shash3(i+vec3(0,0,1)),n101=shash3(i+vec3(1,0,1)),n011=shash3(i+vec3(0,1,1)),n111=shash3(i+vec3(1,1,1));
133
+ float x00=mix(n000,n100,u.x),x10=mix(n010,n110,u.x),x01=mix(n001,n101,u.x),x11=mix(n011,n111,u.x);
134
+ return mix(mix(x00,x10,u.y),mix(x01,x11,u.y),u.z)*2.0-1.0; } // [-1,1]
135
+
136
+ // ANALYTIC-DERIVATIVE value noise (IQ): returns vec4(value[-1,1], d/dPx, d/dPy, d/dPz). The exact
137
+ // gradient of the SAME trilinear+smoothstep field as snoise3 above, so the geometry it shapes and the
138
+ // normal it lights are the one field with NO finite-difference step to alias the fine octaves -- this
139
+ // is the core of the one-comprehensive-system design (mem tv8-one-system-design-analytic-deriv-fbm):
140
+ // the lit normal becomes exact at every scale, killing the deck flat-clay (the 200m/2000m FD steps
141
+ // averaged the fine octaves out). du = derivative of the smoothstep weight 6f(1-f).
142
+ // (snoise3D analytic-derivative noise DELETED 2026-06-11 dead-code sweep: its sole consumer broadShapeMD was removed earlier.)
143
+
144
+ // ---- CONTENT CARVE FIELDS (SHARED VS+FS preamble). These cut lakes/rivers/canyons into the
145
+ // elevation (VS uses the depth) AND gate the water/rock colour (FS uses the wet/depth mask), so the
146
+ // colour sits EXACTLY in the carved depression at every LOD -- they MUST be visible to BOTH stages
147
+ // (defining them inside #ifdef _VERTEX_ made the FS fail to link: 'no matching overloaded function',
148
+ // the same class as the historical snoise3-in-VS-only bug). Pure fn of world dir -> seam-safe +
149
+ // LOD-invariant. Each has an EROSION profile: a wide graded shoulder/valley/bench that blends into
150
+ // terrain plus a deeper core, and returns a wet/depth mask the FS colours.
151
+ const float LAKE_CARVE_DEPTH = 90.0; // metres of bowl depth at basin centre
152
+ float lakeBasinField(vec3 dir){ return 0.5 + 0.5 * snoise3(dir * 55.0 + vec3(4.0, 9.0, 1.0)); }
153
+ float lakeCarveM(vec3 dir, out float wet){
154
+ float basin = lakeBasinField(dir);
155
+ float shoulder = smoothstep(0.50, 0.74, basin); // eroded outwash apron (blends into terrain)
156
+ float bowl = smoothstep(0.62, 0.80, basin); // deeper water body
157
+ wet = smoothstep(0.64, 0.70, basin); // crisp shoreline of the open water
158
+ return -LAKE_CARVE_DEPTH * (0.45 * shoulder + 0.55 * bowl);
159
+ }
160
+ float lakeCarveM(vec3 dir){ float w; return lakeCarveM(dir, w); }
161
+ // River and canyon are the SAME incision algorithm (a 4-octave ridged 1-abs(snoise3) network),
162
+ // differing only in base frequency, phase, depth, and threshold band. One shared field helper.
163
+ // `d` is already the sampling coordinate (normalized dir, optionally phase-shifted by the caller).
164
+ //
165
+ // AXIS-BIAS + ORGANIC FIX (user: 'canyons draw H/V lines, nothing in between; less predictable').
166
+ // snoise3 is VALUE noise on an integer lattice, so ridged 1-abs(snoise3) crests align to the x/y/z
167
+ // axes -> horizontal/vertical channel lines. Two fixes, both keeping it a pure world-dir field:
168
+ // ROTATE the sample domain per octave by a fixed CONSTANT 3D rotation (precomputed mat3, no
169
+ // per-call trig/normalize, no extra noise taps -- cheap) so each octave's lattice axes point
170
+ // differently -> the summed ridged crests run in EVERY direction, not just the x/y/z lattice H/V.
171
+ // The matrix rows are a fixed tilted basis (~no axis alignment); det ~= 1 so it doesn't drift scale.
172
+ const mat3 OCT_ROT = mat3( 0.80, 0.36, -0.48, -0.48, 0.86, -0.18, 0.36, 0.36, 0.86);
173
+ float inciseRidgeField(vec3 d, float baseFreq, float freqMul){
174
+ vec3 p = d;
175
+ float freq = baseFreq, amp = 1.0, sum = 0.0, norm = 0.0;
176
+ for (int o = 0; o < 3; o++){ // 4->3 octaves (max-speed sweep 2026-06-10: finest ~6km gullies sub-pixel at flight altitudes; -1 tap x4 calls x3 composeHeight/vertex)
177
+ sum += amp * (1.0 - abs(snoise3(p * freq)));
178
+ norm += amp; freq *= freqMul; amp *= 0.5; p = OCT_ROT * p; // rotate domain each octave
179
+ }
180
+ return sum / norm; // ->1 on the channel network
181
+ }
182
+ const float RIVER_INCISE_DEPTH = 120.0; // metres at the channel thalweg
183
+ float riverRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir), 320.0, 2.03); }
184
+ float riverCarveM(vec3 dir, out float wet){
185
+ float ridge = riverRidgeField(dir);
186
+ float valley = smoothstep(0.78, 0.94, ridge); // wide eroded valley sides
187
+ float thalweg = smoothstep(0.88, 0.96, ridge); // deep channel core
188
+ wet = smoothstep(0.90, 0.94, ridge); // flowing-water line
189
+ return -RIVER_INCISE_DEPTH * (0.5 * valley + 0.5 * thalweg);
190
+ }
191
+ float riverCarveM(vec3 dir){ float w; return riverCarveM(dir, w); }
192
+ 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)
193
+ uniform float canyonDepthMul; // LIVE canyon-depth lever (window.__canyonDepth; 1.0 default)
194
+ // W5: uNrmGain deleted (only fed the removed VS slopeGain).
195
+ uniform float uVertexAO; // per-vertex shading/AO strength lever (window.__vertexAO; 1.0 default)
196
+ // SEPARATE WATER SURFACE (user 2026-06-11 'the ocean should be a separate surface'): the same
197
+ // program draws TWO instanced passes -- terrain (uIsWater=0, true seabed geometry, land shading)
198
+ // then water (uIsWater=1, the same leaves pinned to sea level, animated ocean shading, alpha-
199
+ // blended over the seabed). One program + a uniform flag = no second cold compile, no duplicated
200
+ // per-frame uniform churn, and the branch is uniform-coherent (free on the GPU).
201
+ uniform float uIsWater; // 0 = terrain pass, 1 = water-surface pass
202
+ uniform float uBeachTopM; // beach ceiling (m): below this, grass/snow yield to sand (window.__beachTop; 30 default)
203
+ uniform float uHiFreqCut; // hi-freq elevation-noise attenuation, applied to ALL hi-freq sources:
204
+ // broadShapeM/MD fine octaves (o>=6) + vtxDisplace micro-relief
205
+ // (window.__hiFreqCut; default 0.25 = the user's 4x reduction, 2026-06-06)
206
+ // ANCHOR-STEP A/B TOGGLES (per-area stairstep root-hunt, workflow wrxo0rr7a). Each defaults to 0.0 =
207
+ // CURRENT behaviour; set window.__<name>=1 to WIDEN that anchor-keyed smoothstep band so the 0->1 swing
208
+ // spreads over more ground (the narrow-band-on-slow-anchor mechanism that snaps relief+normal "here and
209
+ // there" along anchor contours). The user binary-searches these live at the affected spot; the one that
210
+ // dissolves the step = the root. Once found, the winning widen is baked in unconditionally + toggle removed.
211
+ uniform float uMtnBandWide; // widen mtn=smoothstep(16.8,18.6,elevAmp) -> (14.5,19.5) [TOP SUSPECT: unlocks 2600m belt massif across a thin contour]
212
+ uniform float uClimateRelief; // widen wetLowFlat(0.66,0.9,humid) + coldFlat(0.18,0.34,temp) reliefMul gates
213
+ uniform float uIsleWide; // widen isleZone seaBias gates (50,350)+(900,1600) -> (30,600)+(600,2200)
214
+ uniform float uCarveWide; // widen the river/canyon/lake/dune CLIMATE gates so carve depth fades in over a wide span
215
+ float canyonRidgeField(vec3 dir){ return inciseRidgeField(normalize(dir) + vec3(13.7, -4.2, 8.9), 380.0, 2.07); } // phase offset matches FS canyonMask
216
+ // CANYON cross-section now reads as a CANYON, not a V-notch: STEEP WALLS + a FLAT FLOOR.
217
+ // wall = a sharp smoothstep band -> the carve drops fast over a narrow ridge interval (the cliff
218
+ // walls), instead of the old gentle bench+gorge blend that made shallow V-troughs.
219
+ // floor = clamped: once past the wall the depth saturates to the flat gorge bottom (a river runs
220
+ // there, not an ever-deepening point). bench keeps a soft eroded rim above the wall lip.
221
+ // Pure world-dir (LOD-invariant, seam-safe). depth out = 0 rim -> 1 floor (drives FS strata + AO).
222
+ float canyonCarveM(vec3 dir, out float depth){
223
+ float ridge = canyonRidgeField(dir);
224
+ // WIDENED bands (user 2026-06-02 deepen+widen): the old .78/.86/.905/.93 intervals were a very
225
+ // narrow ridge slice -> thin hairline gorges. Widen so the canyon reads as a BROAD gorge: a wide
226
+ // eroded rim shoulder, a steep but visible wall, and a wide flat floor that the gorge bottoms out on.
227
+ float bench = smoothstep(0.62, 0.78, ridge); // wide eroded rim shoulder
228
+ float wall = smoothstep(0.78, 0.86, ridge); // STEEP cliff wall (wider band)
229
+ float floorF= smoothstep(0.86, 0.93, ridge); // reach the flat gorge floor, then clamp
230
+ depth = max(wall, floorF);
231
+ // 0.18 rim shoulder + 0.82 wall-to-floor; floorF clamps so the bottom is flat (no infinite V).
232
+ float profile = 0.18 * bench + 0.82 * max(wall, floorF);
233
+ float dmul = canyonDepthMul > 0.0 ? canyonDepthMul : 1.0; // 0 = uniform unset (e.g. probe prog)
234
+ float carve = -CANYON_INCISE_DEPTH * dmul * profile;
235
+ // FINE TRIBUTARY GULLIES (user 2026-06-02: 'at 2m our canyons are <2m'). The main canyon network
236
+ // (freq 380 ~100km) is sparse + broad, so close up the ground has no canyon-scale erosion. Add a
237
+ // FRACTAL CONTINUATION: 2 finer ridged incision octaves (~10km + ~2km wavelength) carving shallower
238
+ // gullies/ravines (~55m + ~16m deep) so arid terrain shows branching erosion channels at the 10-
239
+ // 100m scale the low-altitude camera sees. Pure world-dir (LOD-invariant); the mesh resolves them
240
+ // at maxLevel 16 (~7m cells). Each gully octave = a thinned ridged field, depth scaled to its wl.
241
+ vec3 dn = normalize(dir);
242
+ float g1 = inciseRidgeField(dn + vec3(5.1, 8.3, -2.7), 3500.0, 2.11); // ~10km tributaries
243
+ float g2 = inciseRidgeField(dn + vec3(-7.4, 1.9, 6.2), 14000.0, 2.05); // ~2.5km gullies
244
+ // HIGH-FREQ fractal depth x0.65 (user 2026-06-11 'leave the low frequency canyons deep, only
245
+ // reduce the high frequency fractals, bring it back 30%' -- halved, then +30% restored): the
246
+ // main 100km gorge network keeps full CANYON_INCISE_DEPTH; only these tributary octaves shrink.
247
+ carve += -104.0 * dmul * smoothstep(0.78, 0.93, g1); // ~10km tributaries (160m * 0.65)
248
+ carve += -39.0 * dmul * smoothstep(0.80, 0.95, g2); // ~2.5km gullies (60m * 0.65)
249
+ return carve;
250
+ }
251
+ float canyonCarveM(vec3 dir){ float dd; return canyonCarveM(dir, dd); }
252
+
253
+ // CLIFFS AS REAL MESA LANDFORMS (user 2026-06-02: 'cliffs still patches of dots', 'we dont have
254
+ // realistic cliffs yet'). The height-QUANTIZATION terrace produced thin scattered contour risers =
255
+ // dots, NOT escarpments. REDESIGN: a cliff is a coherent raised PLATEAU (mesa) with a sharp RIM.
256
+ // mesaField(d) -> a smooth low-frequency field; where it exceeds a threshold the land is a mesa
257
+ // TOP, raised by a fixed height; the THRESHOLD CROSSING is a narrow band = the near-vertical cliff
258
+ // RIM, a CONNECTED line around the whole mesa (not speckle). Stacking two octaves gives mesas +
259
+ // buttes (smaller mesas on top). Pure world-dir, LOD-invariant, seam-safe. Returns metres to ADD;
260
+ // cliffOut ->1 on the rim band (drives FS strata + steep material) so cliffs are lit + textured.
261
+ const float MESA_HEIGHT = 1000.0; // metres a mesa top stands above the floor (user 2026-06-02 deepen+widen; was 380m = invisible vs multi-km relief)
262
+ uniform float cliffAmt; // LIVE cliff-strength lever (window.__cliffAmt; 1.0 default)
263
+ highp float broadShapeLowM(vec3 dir); // W7: forward decl (metres -> highp); atlas-backed slope helper for the region slope gate
264
+ // COHERENT BADLANDS REGION MASK: a LOW-FREQUENCY world-dir noise (freq 11 ~ continental patches)
265
+ // thresholded to a minority fraction -> WHERE mesa country occurs. Mesas exist only inside a region
266
+ // so cliffs cluster into badlands, never speckle the whole planet.
267
+ float badlandsRegion(vec3 d){
268
+ float r = snoise3(d * 11.0 + vec3(31.7, -12.3, 5.1)); // [-1,1], coherent ~continental patches
269
+ return smoothstep(0.30, 0.55, r); // top fraction -> 1 inside badlands
270
+ }
271
+ // Smooth mesa potential in [0,1]: a couple of mid-frequency octaves (freq ~55/130 -> mesas ~80km /
272
+ // ~35km across). Domain-warped so mesa outlines are organic (not blobby circles).
273
+ float mesaField(vec3 d){
274
+ vec3 w = d; w.xy += 0.10 * vec2(snoise3(d*40.0+5.0), snoise3(d*40.0+11.0)); // 3->2 warp taps (max-speed sweep): z-warp dropped, outlines stay organic
275
+ float a = snoise3(w * 55.0 + vec3(3.1, 7.7, 1.3)); // big mesas
276
+ float b = snoise3(w * 130.0 + vec3(9.4, 2.2, 6.6)); // buttes on top
277
+ return 0.5 + 0.5 * (0.7 * a + 0.3 * b); // [0,1]
278
+ }
279
+ highp float cliffTerraceM(vec3 dir, highp float h, out float cliffOut){ // W7: h is metres (highp); cliffOut mask stays mediump
280
+ cliffOut = 0.0;
281
+ if (h <= 0.0) return 0.0;
282
+ vec3 d = normalize(dir);
283
+ // REGION GATE: mesas only inside a coherent badlands region (no planet-wide speckle).
284
+ float region = badlandsRegion(d);
285
+ if (region <= 0.0) return 0.0;
286
+ // EXCLUDE steep mountains (a mesa is a flat-topped raised block, not a peak). Macro slope from
287
+ // broadShapeLowM over a fixed 1.2km step -> fade mesas out where the broad land is already steep.
288
+ const float e = 1200.0;
289
+ vec3 ux = normalize(cross(abs(d.y) < 0.99 ? vec3(0.0,1.0,0.0) : vec3(1.0,0.0,0.0), d));
290
+ highp float h0 = broadShapeLowM(d); // W7: metres, highp
291
+ highp float gx = (broadShapeLowM(normalize(d + ux * (e/defRadius))) - h0) / e;
292
+ float flatness = 1.0 - smoothstep(0.05, 0.16, abs(gx));
293
+ float gate = region * flatness;
294
+ if (gate <= 0.0) return 0.0;
295
+ // MESA STEP: raise the land where mesaField crosses THR over a NARROW band TH -> the band is the
296
+ // cliff RIM (a connected contour around the mesa), the inside is the raised flat-ish top. Two
297
+ // levels (top + a higher butte tier) so cliffs stack like real mesa country.
298
+ float m = mesaField(d);
299
+ const float THR = 0.56, TH = 0.07; // rim centre + half-width (widened for the taller 1000m escarpment -> readable slope, still a sharp rim)
300
+ float top = smoothstep(THR - TH, THR + TH, m); // 0 floor -> 1 mesa top
301
+ float butte= smoothstep(0.74 - TH, 0.74 + TH, m); // higher butte tier
302
+ float rise = (0.7 * top + 0.3 * butte) * MESA_HEIGHT; // metres raised
303
+ // rim mask: ->1 INSIDE either transition band (where the step is climbing = the steep face).
304
+ float rim = max(top * (1.0 - top), butte * (1.0 - butte)) * 4.0; // bell, peak 1 mid-rim
305
+ cliffOut = clamp(rim, 0.0, 1.0) * gate;
306
+ float camt = cliffAmt > 0.0 ? cliffAmt : 1.0; // 0 = uniform unset (e.g. probe prog)
307
+ return rise * camt * gate; // metres added (mesa top raised; rim = the cliff)
308
+ }
309
+
310
+ // DUNE FIELD (arid/desert anchors, ref: keaukraine/webgl-dunes). A rolling sand surface = large
311
+ // smooth low-frequency dunes with ASYMMETRIC crests (windward shallow, lee steep) + superimposed
312
+ // fine wind ripples. Pure world-dir field (LOD-invariant). Returns metres of dune relief (>=0,
313
+ // added on top of land) and `crest` in [0,1] (1 at a dune ridge -> the FS lightens sand there).
314
+ const float DUNE_AMP = 120.0; // metres of big-dune relief
315
+ float duneFieldM(vec3 dir, out float crest){
316
+ vec3 d = normalize(dir); // UNIT dir keeps freq*d bounded -> no planet-scale fp32 lattice break.
317
+ // BIG DUNES ~3km: low-freq smooth noise, asymmetric crest (pow sharpens the lee face).
318
+ float big = snoise3(d * 2000.0 + vec3(2.0, 7.0, 5.0)) * 0.5 + 0.5; // [0,1]
319
+ float dune = pow(big, 1.6); // windward-shallow/lee-steep
320
+ // MEDIUM dunes ~1km riding on the big ones. (The old ~150-220m wind RIPPLES were REMOVED: at
321
+ // that wavelength the mesh vertices undersample them -> shimmer/moire 'UV issue' + a wild FD
322
+ // normal, with no resolvable benefit. The rolling big+medium dunes are the dune read.)
323
+ float med = snoise3(d * 6000.0) * 0.5 + 0.5;
324
+ crest = smoothstep(0.55, 0.92, big);
325
+ return DUNE_AMP * dune + 40.0 * (med * dune);
326
+ }
327
+
328
+ // TERRAIN HEIGHT FIELD: one continuous world-dir fBm -- a finer LOD is a denser SAMPLE of the same
329
+ // field (LOD-invariant, seam-safe by construction).
330
+ // CHEAP 8-octave variant for the VS lit-normal finite difference (silhouette+macro slope only).
331
+ highp float broadShapeLowM(vec3 dir){ // W7: metres (~13000) + freq (~49152) accumulators are highp islands
332
+ if (hasHpf == 0) return 0.0;
333
+ vec3 d = normalize(dir);
334
+ highp float amp = 6500.0, freq = 3.0, sum = 0.0;
335
+ for (int o=0; o<8; o++){ sum += amp * snoise3(d*freq); amp *= (o < 6 ? 0.66 : 0.82); freq *= 2.0; }
336
+ return sum - 900.0;
337
+ }
338
+ // ---- SHARED micro-relief helpers (MOVED to the common preamble, THC-Normal W1, so composeHeight()
339
+ // in the VS/PROBE region can call them). vtxDetail = the micro-relief A/B global; vnoise2
340
+ // is the 2D value-noise vtxDisplace octaves sample; ruggedFromElevAmp + faceWarp map anchor->amplitude
341
+ // and face-local metres->warped metres. All pure, dependency-light (defRadius + smoothstep only).
342
+ uniform float vtxDetail; // micro-relief strength (1=on) -- kept as a global so it can be A/B'd
343
+ // W7 highp ISLAND: vnoise2 args are face-local metres / wavelength = fp(~6.4e6)/wl(~1m) -> ~6e6, far
344
+ // past fp16. The vtxDisplace micro-relief lattice needs full precision or the ground bump quantizes.
345
+ // W10 PRECISION: the old fract(p*443.897) hash overflowed fp32 integer-exactness at fine octaves
346
+ // (wl~10m -> p~6.4e5, *443.897 -> 2.84e8 >> 2^24=1.67e7), so fract() lost low bits and the corner
347
+ // hashes QUANTIZED to a few discrete values -> per-cell height plateaus = faint ~0.48m terracing on
348
+ // flat ground. Fix: hash the INTEGER lattice cell with a uint bit-mix (no large float, no fract).
349
+ // Pure function of the world-consistent integer cell index -> adjacent LODs and cube faces agree
350
+ // EXACTLY (no new seam). Amplitude/range UNCHANGED: still a uniform [0,1] per-cell value, same field
351
+ // statistics; this removes the quantization, it does not smooth. (p is the exact integer lattice
352
+ // index <2^24, so ivec2(p) is exact.)
353
+ highp float vhash(highp vec2 p){
354
+ uvec2 q = uvec2(ivec2(p)); // two's-complement bits of the exact integer cell index
355
+ uint h = q.x * 1597334677u + q.y * 3812015801u; // decorrelate the two axes
356
+ h ^= h >> 16; h *= 2654435769u; h ^= h >> 15; h *= 2246822519u; h ^= h >> 13; // bit avalanche
357
+ return float(h) * (1.0 / 4294967296.0); // [0,1)
358
+ }
359
+ // QUINTIC C2 interp (Perlin u=6t^5-15t^4+10t^3), NOT the C1 cubic smoothstep (3t^2-2t^3). The cubic's
360
+ // SECOND derivative is discontinuous at every lattice cell edge -> the noise SLOPE kinks at each ~wl cell
361
+ // boundary; on a gentle mountain flank (where the kink is not swamped by relief) that ~2.7m/10m slope spike
362
+ // every ~320m (vtxDisplace wl0) reads as a LOCAL stairstep in both elevation and the central-diff normal
363
+ // (user 2026-06-09 'only a few very local spots'). Quintic is C2 -> slope continuous across cells, NO kink,
364
+ // and amplitude UNCHANGED (this does not smooth the terrain, it removes the discontinuity in its derivative).
365
+ float vnoise2(highp vec2 p){ highp vec2 i=floor(p),f=fract(p); vec2 u=f*f*f*(f*(f*6.0-15.0)+10.0);
366
+ float a=vhash(i),b=vhash(i+vec2(1,0)),c=vhash(i+vec2(0,1)),d=vhash(i+vec2(1,1));
367
+ return mix(mix(a,b,u.x),mix(c,d,u.x),u.y)*2.0-1.0; } // [-1,1]
368
+ // (vnoise2D DELETED 2026-06-11 dead-code sweep: no callers since the VS gradient block was removed.)
369
+ // anchor elevAmp -> rugged multiplier (BAKED elevAmp range ~14.5-18.9).
370
+ float ruggedFromElevAmp(float elevAmp){ return mix(0.5, 1.8, smoothstep(15.0, 18.6, elevAmp)); }
371
+ // TANGENT-ADJUSTED cube->sphere warp: face-local metres -> warped metres (near-uniform cell area).
372
+ // Edge s=+-1 -> tan(+-pi/4)=+-1 (identity) => cross-face shared edges meet exactly (seam-safe).
373
+ highp vec2 faceWarp(highp vec2 p){ return defRadius * tan((p / defRadius) * 0.7853981634); } // W7: ~6.4e6 m result, highp
374
+ // PERLIN-EVERYWHERE lever -- shared by the composeHeight elevation term (VS/PROBE) and the FS albedo
375
+ // overlay, so it must be declared in ALL stages (outside the VS/PROBE guard below).
376
+ uniform float uDetailOverlay; // amplitude lever (user-tuned 6; 0 = off; __detailOverlay)
377
+ // FXC UNROLL-DEFEAT (2026-06-12, 'rocks everywhere + no normals on AMD default Chrome'): ANGLE's
378
+ // D3D11 backend runs FXC, which fully unrolls constant-bound loops and applies aggressive math
379
+ // reordering across the unrolled body -- the long-suspected mis-translation domain (vulkan on the
380
+ // SAME AMD GPU renders correctly; d3d11 does not). A runtime loop bound cannot be unrolled. The
381
+ // uniform is set to 12 by gl-render; the max(...) guard keeps an unset uniform (0) from flattening
382
+ // the planet -- worst case the loop still runs the literal octave count.
383
+ uniform int uOctMax; // broadShapeM octave count (12); runtime-bound to defeat FXC unrolling
384
+ uniform float uNrmStepM; // lit-normal FD step in metres (150); uniform-fed to defeat FXC constant folding
385
+ #if defined(_VERTEX_) || defined(_PROBE_)
386
+ highp float broadShapeM(vec3 dir, float reliefMul, float ridgeMul){ // W7: returns metres (~13000) -> highp
387
+ if (hasHpf == 0) return 0.0;
388
+ float mtnAmp = 1.0; // mountain-amplitude (was a window.__mtnAmp uniform; inlined at neutral 1.0)
389
+ vec3 d = normalize(dir);
390
+ highp float amp = 6500.0, freq = 3.0, sum = 0.0; // W7 highp ISLAND: amp/freq(~49152)/sum(~13000) overflow fp16
391
+ int octMax = (uOctMax > 0) ? uOctMax : 12; // runtime bound (FXC unroll-defeat); 12 when unset
392
+ for (int o=0; o<octMax; o++){ // W9: 14->12 octaves, drop the finest 2 (wavelengths <2km) UNCONDITIONALLY
393
+ // FINE-OCTAVE 5x FREQ (user 2026-06-06: the high-freq normals-affecting GROUND bump should be 5x
394
+ // SMALLER). The o>=6 fine octaves ARE that bump (they drive the VS lit normal via the broadShapeMD
395
+ // gradient bEx/bEy; vtxDisplace + rock dN are separate, confirmed by live __vtxDetail toggle). Sample
396
+ // the fine band at 5x frequency so the bump grain is 5x finer; the o<6 base octaves (silhouette +
397
+ // hypsometry, CLI-validated) keep freq untouched. MUST match broadShapeMD exactly (geometry+normal).
398
+ highp float sf = (o >= 6) ? freq * 0.667 : freq; // W7: freq island. fine/ridge band 3x WIDER (user 2026-06-10 'mountains noise still narrow, 3x wider'): *2.0 -> *0.667 = the visible o>=6 ridge texture (13-104km) widens 3x
399
+ float nn = snoise3(d*sf);
400
+ if (o >= 6) {
401
+ float r = (1.0 - abs(nn)) * 2.0 - 1.0; // rounded ridged crests for mountain belts
402
+ nn = mix(nn, r, ridgeMul * 0.8);
403
+ nn *= reliefMul; // per-biome amplitude on regional+fine octaves
404
+ nn *= 1.36; // FINE-OCTAVE INTENSITY -- 4x (user 2026-06-09 '4x our high
405
+ // frequency perlin noise intensity'): 0.34 -> 1.36, net
406
+ // 1.36 * uHiFreqCut 0.5 = 0.68 (was 0.17). The o>=6 fine
407
+ // octaves are the high-freq normals-affecting ground bump
408
+ // (user-confirmed 2026-06-06). uHiFreqCut(0.5) stays the
409
+ // live trim dial. Silhouette octaves o<6 untouched.
410
+ nn *= uHiFreqCut; // hi-freq cut, default WIDENED 0.25->0.5 (see gl-render):
411
+ // apply the SAME fine-octave attenuation that broadShapeMD uses, so the COLLISION/probe height
412
+ // (sampleGroundM runs this scalar broadShapeM) matches the RENDERED geometry (broadShapeMD). Before
413
+ // this, only broadShapeMD cut the fine band -> the collision height diverged from the surface by the
414
+ // fine-octave amount. uHiFreqCut default 0.25 = the user's 4x reduction of all hi-freq elevation noise.
415
+ }
416
+ sum += amp * nn;
417
+ 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)
418
+ }
419
+ // LOW-ELEVATION COMPRESSION CURVE (gamma 2.5): power-curve the positive land height only so
420
+ // low/mid land sinks toward a flatter base while peaks keep their range; sea sign untouched.
421
+ {
422
+ const float HREF = 6500.0, GAMMA = 2.5;
423
+ highp float e0 = sum - 900.0; // W7: metres
424
+ if (e0 > 0.0) {
425
+ highp float ec = HREF * pow(min(e0, HREF) / HREF, GAMMA);
426
+ if (e0 > HREF) ec += (e0 - HREF);
427
+ sum = 900.0 + ec;
428
+ }
429
+ }
430
+ // PEAK-LIFT, MOUNTAIN-BELT GATED: lift the high end so ridges become tall peaks; plains flat.
431
+ highp float hi = max(sum - 900.0, 0.0); // W7: metres
432
+ float peak = smoothstep(1200.0, 5000.0, hi);
433
+ 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)
434
+ sum += hi * peak * liftK;
435
+ // MOUNTAIN-BELT MASSIF: a low-freq range-scale envelope lifts the belt interior as a coherent
436
+ // landmass; the ridged mid octaves above supply the rugged peaks ON TOP of the raised base.
437
+ float belt = clamp((reliefMul - 0.45) / 1.25, 0.0, 1.0);
438
+ if (belt > 0.0 && hi > 0.0) {
439
+ // (massif envelope freqs restored to 4.0/7.0 -- they set continental belt PLACEMENT, not the visible
440
+ // ridge spacing; the 3x-wider knob is the o>=6 fine band above, not this envelope.)
441
+ float e0 = snoise3(d * 4.0 + vec3(11.0, 3.0, 7.0)) * 0.5 + 0.5;
442
+ float e1 = snoise3(d * 7.0 + vec3(2.0, 9.0, 4.0)) * 0.5 + 0.5;
443
+ float modu = 0.7 + 0.3 * clamp(e0 * 0.7 + e1 * 0.3, 0.0, 1.0);
444
+ float landGate = smoothstep(0.0, 800.0, hi);
445
+ float base = belt * landGate * modu;
446
+ sum += base * 5200.0 * mtnAmp; // 2x LESS elevated (user 2026-06-10: halved 10400 -> 5200)
447
+ // STEEP RUGGED PEAKS: a high-base-freq (~30km) ridged stack concentrated into steep crests.
448
+ if (base > 0.01) {
449
+ // pf 100 REVERTED to 200 (user 2026-06-11 'most of the terrain is flat now'): the halved peak
450
+ // frequency flattened the 4x mountains planet-wide. The 'slopes everywhere' diagnosis it served
451
+ // conflated the braided TEXTURE patches (rockSlope breakup noise, since deleted) with real slope;
452
+ // with the braids, the dark raw-photo far field, and the UDN normal-frame bug all fixed, the
453
+ // original steep terrain stands.
454
+ highp float pf = 200.0; float pa = 1.0, ps = 0.0, pn = 0.0; // W7: pf (~4100) feeds the noise lattice -> highp
455
+ for (int o = 0; o < 3; o++) { // 5->3 octaves (max-speed sweep: -6 taps/vertex in the belt; coarse 2 set the peak stance, decay 0.5->0.65 reweights)
456
+ ps += pa * (1.0 - abs(snoise3(d * pf + vec3(3.3, 7.7, 1.1))));
457
+ pn += pa; pa *= 0.65; pf *= 2.13;
458
+ }
459
+ float crest = ps / pn;
460
+ sum += base * pow(crest, 4.5) * 8400.0 * mtnAmp; // 2x LESS elevated (user 2026-06-10: halved 16800 -> 8400); sharper eroded tips (pow 4.5) kept
461
+ }
462
+ }
463
+ // SOFT ELEVATION CEILING (mob-w8-ceiling 2026-06-08: 'peaks come to points, no flat plateau'). Even at
464
+ // CK=6500/CMAX=13000 the tanh(x/(CMAX-CK)) knee was tight enough that the high band still rolled over onto
465
+ // a soft plateau and needed a *1.15 post-scale crutch. WIDEN THE KNEE: divide the excess by a fixed 8000m
466
+ // so the tanh stays in its near-linear region far longer -- peaks keep climbing to points instead of
467
+ // asymptoting to a ceiling -- and lift CMAX to 15000 so the asymptote sits well above any real peak. With
468
+ // the wider knee the high band no longer collapses, so the *1.15 scale-up crutch is DROPPED.
469
+ {
470
+ highp float e = sum - 900.0; // W7: metres
471
+ // GLOBAL ELEVATION SCALE 0.65 (user 2026-06-10). 0.343 over-squashed the field (45.9% land <500m, p50
472
+ // 723m, relief only ~4km -> mountains invisible/clamped-looking, NOT a top clip). 0.65 expands the
473
+ // range so mountains stand out: p50 ~1370m, p99 ~8920m, MAX ~11600m, relief ~7.5km. Scales the WHOLE
474
+ // relief uniformly (base octaves + belt massif + peaks) on the excess-over-900. Max 11.6km still well
475
+ // under the ceiling (CK 6500 / CMAX 42000, knee /26000) so no clip.
476
+ e *= 0.65;
477
+ // CEILING RAISED + KNEE WIDENED for the 4x mountaintops (user 2026-06-09: 'must not hit the ceiling
478
+ // anywhere, run under the normal max + elevate the maximum'). MEASURED 4x-hf+4x-mtn pre-ceiling max
479
+ // = 28550m (p99.9 21457m); CMAX 42000 sits 1.47x above it = no peak ever approaches the asymptote, and
480
+ // the knee /26000 keeps tanh near-LINEAR through the whole real range (at the 22050m excess top,
481
+ // tanh(0.85)=0.69 -> peaks come to POINTS, never the flat plateau the tight knee caused).
482
+ float CK = 6500.0, CMAX = 42000.0;
483
+ if (e > CK) { highp float x = e - CK; e = CK + (CMAX - CK) * tanh(x / 26000.0); }
484
+ return e;
485
+ }
486
+ }
487
+ // FD-GRADIENT VARIANT (FPS lever, measured: browser-9 low-alt VS=53ms/95% of frame). The lit-normal
488
+ // finite-difference in the VS samples broadShapeM at +/- a 2km world step; octaves whose wavelength is
489
+ // BELOW the FD step (~the finest 4 of the 14, wavelength < ~2km) cannot be resolved by a 2km FD -- they
490
+ // only add sub-step noise that smooths/aliases out. So this variant runs 10 (not 14) macro+mid octaves
491
+ // and a 3-octave (not 5) steep-peak stack, keeping the FULL massif/peak-lift/ceiling AMPLITUDE so the FD
492
+ // slope still tracks the real mountain geometry (the broadShapeLowM regression was dropping that
493
+ // amplitude, not the sub-step octaves). It is a GLOBALLY-FIXED world-dir function => LOD-invariant, no
494
+ // tile-edge seam (the refuted per-tile/LOD octave fade diverged at shared edges; this does not). Used
495
+ // ONLY for the two FD taps; the displaced HEIGHT still uses the full broadShapeM (full detail on the
496
+ // geometry, slightly-coarsened slope on the lit normal -- the FS dFdx normal carries the fine relief).
497
+ // ANALYTIC-DERIVATIVE broadShapeM: returns vec4(height, dHeight/dDir in WORLD-DIR space). Same field
498
+ // as broadShapeM, but every octave also accumulates its exact gradient via snoise3D, and the smooth
499
+ // post-terms (gamma compression, peak-lift, tanh ceiling) scale the gradient by their scalar
500
+ // derivative (chain rule). The FS builds the lit normal from this gradient instead of a finite
501
+ // difference -> exact relief shading at EVERY scale, no fine-octave aliasing (the deck flat-clay fix).
502
+ // The massif/steep-peak ridged terms contribute their dominant gradient; sub-tap detail rides the
503
+ // octave gradient. Returns the SAME height as broadShapeM (verified by construction).
504
+ // broadShapeFD (the reduced-octave FD-tap variant) REMOVED 2026-06-05: the analytic-derivative
505
+ // broadShapeMD replaced the 3-tap finite-difference lit-normal entirely, so the reduced-octave FD
506
+ // helper is dead. One comprehensive gradient source, net-smaller shader (one-system cleanup).
507
+ highp float broadShape(vec3 dir){ return broadShapeM(dir, 1.0, 0.0); } // W7: metres -> highp
508
+ highp float broadShape(vec3 dir, float tileM){ return broadShape(dir); }
509
+
510
+ // PER-VERTEX micro-relief (rolling-ground bump). MOVED into the shared VS/PROBE region
511
+ // (THC-Normal W1) so geometry (VS), collision (PROBE) and the height-bake all call the SAME field --
512
+ // no parallel re-derivation. fp = face-local warped metres (continuous across tiles), tileM = quad
513
+ // size for the Nyquist octave fade, rugged = anchor-driven amplitude. Pure value (no gradient).
514
+ float vtxDisplace(highp vec2 fp, float tileM, float rugged){ // W7: fp = face-local metres ~6.4e6 -> highp; fp/wl feeds the noise lattice
515
+ if (vtxDetail <= 0.0 || rugged <= 0.0) return 0.0;
516
+ float wl0 = 960.0; // coarsest fine octave wavelength (m) -- 3x WIDER / less frequent (user 2026-06-09:
517
+ // 'make this high frequency elevation noise 3x wider, its very nice but too small').
518
+ // Was 320m; 320*3=960. The noise the user sees on ALL land widens 3x (finest octave
519
+ // 30m vs 10m). The mountain-gated erosive copy below rides the same widened basis.
520
+ float amp0 = 11.0; // 8->11m: wider features carry a touch more amplitude to stay as visible as the old bump
521
+ float ridge = smoothstep(1.3, 1.75, rugged); // only the highest mountain belts fold to ridges
522
+ float sumF = 0.0, sumR = 0.0, a = amp0, wl = wl0;
523
+ // PER-PATCH STAIR-STEP FIX (user 'stair steps were per patch, not per vert'): the old Nyquist fade keyed
524
+ // cellM=tileM/16 / nyqWl on tileM = the LEAF SIZE (a per-patch constant). Two ADJACENT leaves at different
525
+ // LOD (tileM differs 2x) then weighted the octaves DIFFERENTLY, so their micro-relief disagreed at the
526
+ // shared edge by a constant -> a per-patch HEIGHT STEP at every LOD boundary. Make vtxDisplace a FIXED
527
+ // octave set, a PURE function of the world-pos fp with NO tileM dependence -> every leaf computes the
528
+ // IDENTICAL height at a given point = C0 across all LOD boundaries by construction (no per-patch step).
529
+ // A coarse leaf's 16-cell mesh simply under-samples the finest octaves (smooth interpolation, not a step);
530
+ // the amplitude is small (amp0 8m * uHiFreqCut 0.25 * ruggedAmp) so any residual aliasing is minor and
531
+ // far less visible than the per-patch steps it removes. (tileM kept in the signature for callers.)
532
+ for (int o=0; o<6; o++){
533
+ float n = vnoise2(fp / wl); // [-1,1]
534
+ sumF += a * n; // smooth fBm
535
+ float r = 1.0 - abs(n); r *= r; // ridged: sharp crest at n=0
536
+ sumR += a * (r * 2.0 - 1.0);
537
+ a *= 0.55; wl *= 0.5;
538
+ }
539
+ float sum = mix(sumF, sumR * 0.9, ridge);
540
+ // MOUNTAIN EROSIVE DETAIL (user 2026-06-09: 'take the high-freq elevation noise, make it 3x WIDER (less
541
+ // frequent) -- nice but too small -- and add it to all mountainous zones at a little less intensity as
542
+ // part of the mountain pattern, to simulate erosive detail'). SAME vnoise2(fp/wl) basis as the bump above
543
+ // (so it reads as the same noise the user likes), but wl0 = 3*320 = 960m (3x wider / lower frequency).
544
+ // Gated by `mtnGate` (the rugged/mountain signal) so it ONLY lands in mountainous zones, and at LOWER
545
+ // amplitude than the base bump (ea0 5m vs 8m) per 'a little less intensity'. 4-octave ridged-leaning fBm
546
+ // for erosive gully/ridgeline shape. Added INSIDE vtxDisplace so it shares the LOD-invariant, seam-safe,
547
+ // face-local path (no per-patch step) and is picked up by the composeHeight central-diff lit normal.
548
+ float mtnGate = smoothstep(0.55, 1.0, rugged); // ramps in over the mountain belt (rugged ~ elevAmp)
549
+ if (mtnGate > 0.0) {
550
+ float ea = 2.5, ewl = 2880.0, esumF = 0.0, esumR = 0.0; // user 2026-06-09: HALF the erosion elevation (5->2.5m) + 3x WIDER again (960->2880m)
551
+ for (int o = 0; o < 2; o++) { // 4->2 octaves (max-speed sweep: 720/360m fine erosion sub-vertex at GRID 16; coarse 2880/1440m carry the gully shape)
552
+ float en = vnoise2(fp / ewl);
553
+ esumF += ea * en;
554
+ float er = 1.0 - abs(en); er *= er;
555
+ esumR += ea * (er * 2.0 - 1.0);
556
+ ea *= 0.55; ewl *= 0.5;
557
+ }
558
+ sum += mix(esumF, esumR * 0.9, 0.6) * mtnGate; // ridged-leaning erosive relief, mountain-gated
559
+ }
560
+ float ruggedAmp = clamp(rugged, 0.4, 1.15);
561
+ return sum * vtxDetail * ruggedAmp * uHiFreqCut; // 4x high-freq cut (uHiFreqCut default 0.25)
562
+ }
563
+
564
+ // PERLIN-EVERYWHERE detail fbm (user 2026-06-10): ONE 3-octave value fbm shared by the FS albedo
565
+ // overlay and the composeHeight elevation term below (same field -> the brightness variation and the
566
+ // relief variation correlate, reading as one landform). World-dir keyed, seam-safe.
567
+ highp float detailFbm(vec3 dir) {
568
+ float ov = 0.0, oa = 0.0;
569
+ float fq = 150.0, am = 1.0; // octaves: ~42km / 8.5km (3rd 1.7km octave dropped, max-speed sweep: sub-pixel at altitude, -3 taps/vertex)
570
+ for (int o = 0; o < 2; o++) {
571
+ ov += am * snoise3(dir * fq + vec3(float(o) * 7.3));
572
+ oa += am;
573
+ fq *= 5.0; am *= 0.75;
574
+ }
575
+ return ov / oa;
576
+ }
577
+
578
+ // THC-Normal W1: the SINGLE composite height field. Lifts the inline vH accumulation out of the VS
579
+ // main() (was terrain.glsl ~801-902) into ONE value-only scalar so geometry (VS) and collision (PROBE)
580
+ // are derived from the EXACT same composition -- aligned by
581
+ // construction, no parallel mirror to drift. Returns the signed elevation h (metres) for dir0; the
582
+ // caller adds nothing (cbias is folded in here). dir0 = world dir of the sample (faceWarp'd),
583
+ // faceLocal = face-local warped metres (for vtxDisplace), tileM = quad size (Nyquist fade).
584
+ highp float composeHeight(vec3 dir0, highp vec2 faceLocal, float tileM){ // W7: faceLocal metres + returned h -> highp islands
585
+ vec4 hpf0 = hpfSample(dir0); // (seaBias=r, elevAmp=g, temp=b, humid=a)
586
+ highp float cbias = hpf0.r; // W7: seaBias metres
587
+ float rugged = ruggedFromElevAmp(hpf0.g);
588
+ float vDisp = vtxDisplace(faceLocal, tileM, rugged);
589
+ // per-biome relief from the anchor sample (mirror of the old VS main morphology)
590
+ float bTemp = hpf0.b, bHum = hpf0.a, bAmp = hpf0.g;
591
+ // mtn band: uMtnBandWide=1 spreads the 16.8->18.6 contour to 14.5->19.5 (covers the live elevAmp
592
+ // range ~14.5-18.9) so the belt-massif/reliefMul transition is a planet-wide gradient, not a thin
593
+ // iso-contour snapping a 2600m bulk lift on across gentle land (workflow wrxo0rr7a topSuspect, 2600m).
594
+ float mtn = smoothstep(mix(16.8, 14.5, uMtnBandWide), mix(18.6, 19.5, uMtnBandWide), bAmp);
595
+ // climate reliefMul gates: uClimateRelief=1 widens both bands so the cold/wet flattening fades over
596
+ // latitude/moisture gradients instead of a per-cell contour (wrxo0rr7a coldFlat/wetLowFlat 80m+).
597
+ float wetLowFlat = smoothstep(mix(0.66, 0.50, uClimateRelief), mix(0.9, 1.0, uClimateRelief), bHum) * (1.0 - mtn);
598
+ float coldFlat = (1.0 - smoothstep(mix(0.18, 0.05, uClimateRelief), mix(0.34, 0.45, uClimateRelief), bTemp));
599
+ float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.15, 1.7);
600
+ float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0);
601
+ // ISLAND-TYPE VARIETY (pure fn of world dir) -- mirror of the VS main isle block.
602
+ // uIsleWide=1 spreads the seaBias double-gate so the volcanic/atoll remix ramps in (wrxo0rr7a 2000m).
603
+ float isleZone = smoothstep(mix(50.0, 30.0, uIsleWide), mix(350.0, 600.0, uIsleWide), cbias)
604
+ * (1.0 - smoothstep(mix(900.0, 600.0, uIsleWide), mix(1600.0, 2200.0, uIsleWide), cbias));
605
+ if (isleZone > 0.0) {
606
+ float isleType = snoise3(dir0 * 9.0);
607
+ float volcanic = smoothstep(0.25, 0.7, isleType);
608
+ float atoll = smoothstep(0.25, 0.7, -isleType);
609
+ reliefMul = mix(reliefMul, mix(reliefMul, 1.6, volcanic) * (1.0 - 0.7 * atoll), isleZone);
610
+ ridgeMul = mix(ridgeMul, max(ridgeMul, 0.9 * volcanic), isleZone);
611
+ }
612
+ highp float bShape = broadShapeM(dir0, reliefMul, ridgeMul); // W7: metres
613
+ highp float h = cbias + bShape; // W7: composite elevation metres (~13000)
614
+ // REALISTIC BATHYMETRY (user 2026-06-11 'the depth under the water doesnt seem right -- the
615
+ // landscape should continue underwater realistically'): raw cbias+bShape plunges at land-relief
616
+ // gradients, so the seabed hit kilometre depths within sight of the beach. Real margins have a
617
+ // wide gently-sloped CONTINENTAL SHELF (0..~120m over tens of km), then a steeper continental
618
+ // SLOPE down to the abyssal plain (which keeps its raw depth). Monotone remap of h, pure fn of
619
+ // the field -> seam-safe, LOD-invariant, and the collision probe shares it by construction.
620
+ if (h < 0.0) {
621
+ highp float d = -h;
622
+ h = -(min(d, 500.0) * 0.24 + max(d - 500.0, 0.0) * 1.19);
623
+ }
624
+ // displacement now continues UNDERWATER (the old land-only gate served the flat-clamped ocean,
625
+ // gone since 026d530): the seabed carries the same micro-relief as land = realistic continuation.
626
+ h += vDisp;
627
+ // PERLIN-EVERYWHERE ELEVATION (user 2026-06-10 'it must also affect elevation'): the same detailFbm
628
+ // the FS albedo overlay shows, as real relief (~30m per lever unit -> ~180m at the user-tuned 6).
629
+ // Shore-gated (fades in over the first 250m of land) so the coastline and the flat water planes are
630
+ // untouched and no noise islets pop offshore. The VS FD lit-normal picks it up automatically.
631
+ h += detailFbm(dir0) * uDetailOverlay * 30.0 * smoothstep(0.0, 250.0, h);
632
+ // LAKE CARVE + flat-water plane
633
+ float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
634
+ // uCarveWide=1 widens the carve CLIMATE gates so the gorge/lake/dune depth fades in over a wide
635
+ // anchor span instead of snapping along a thin climate contour (wrxo0rr7a carve cluster 90-1400m).
636
+ float lakeGate = smoothstep(mix(0.60, 0.50, uCarveWide), mix(0.85, 0.95, uCarveWide), hpf0.a) * step(0.0, h);
637
+ float lakeCarveV = lakeCarveRaw * lakeGate;
638
+ h += lakeCarveV;
639
+ float lakeWet = lakeWetV * lakeGate;
640
+ if (lakeWet > 0.0) { float waterLevel = max(h, 0.0) - 25.0; h = mix(h, waterLevel, lakeWet); vDisp *= (1.0 - lakeWet); }
641
+ // RIVER + CANYON incision (clamped so coastal gorges never punch fake inland seas)
642
+ 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);
643
+ float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, h);
644
+ float canyonArid = (1.0 - smoothstep(mix(0.40, 0.30, uCarveWide), mix(0.58, 0.68, uCarveWide), hpf0.a)) * smoothstep(mix(0.38, 0.28, uCarveWide), mix(0.56, 0.66, uCarveWide), hpf0.b) * smoothstep(60.0, 200.0, h);
645
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, h);
646
+ float inciseTot = riverCarveV + canyonCarveV;
647
+ // min(...,0): the floor term goes POSITIVE for any h below -60 and was LIFTING the entire ocean
648
+ // floor to exactly -60m -- the whole seabed was a uniform pan (root of 'depth under the water
649
+ // doesnt seem right'; probe-witnessed minSeen -60 planet-wide). Carves still cannot punch land
650
+ // below -60; the ocean keeps its real bathymetry.
651
+ inciseTot = max(inciseTot, min(-60.0 - h, 0.0)); // land: h + inciseTot >= -60m; ocean: untouched
652
+ h += inciseTot;
653
+ // CLIFF TERRACING (mesa/butte benches) -- after carves so canyon walls + risers compose
654
+ float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, h, cliffFaceMask) * canyonArid * step(0.0, h);
655
+ h += cliffCarveV;
656
+ // FLAT RIVER WATER
657
+ float riverWetLine = riverWetMask * riverWet * step(0.0, h);
658
+ if (riverWetLine > 0.0) { float rWaterLevel = h - 20.0; h = mix(h, rWaterLevel, riverWetLine); vDisp *= (1.0 - riverWetLine); }
659
+ // DUNES on the low sand desert
660
+ float duneSand = smoothstep(mix(0.62, 0.50, uCarveWide), mix(0.85, 0.95, uCarveWide), 1.0 - hpf0.a) * smoothstep(mix(0.40, 0.30, uCarveWide), mix(0.58, 0.68, uCarveWide), hpf0.b) * (1.0 - smoothstep(40.0, 160.0, h));
661
+ float duneCrest; float duneV = duneFieldM(dir0, duneCrest) * duneSand * step(0.0, h);
662
+ h += duneV;
663
+ return h;
664
+ }
665
+ #endif // broadShapeM/broadShape/vtxDisplace/composeHeight: VS/PROBE (excluded from render FS, FS-1)
666
+
667
+ #ifdef _VERTEX_
668
+ layout(location=0) in vec3 vertex; // vertex.xy in [0,1] parametric quad coord
669
+ layout(location=1) in highp vec4 iOffset; // W7: PER-INSTANCE (ox, oy, l, level) face-local metres ~6.4e6 -> highp
670
+ layout(location=2) in float iFace; // PER-INSTANCE cube face index 0..5
671
+ out highp vec3 vWorld; // W7 highp ISLAND: absolute world pos ~6.4e6 m (FS lighting/atmosphere) -- fp16 would jitter it
672
+ out highp float vH; // W7: signed elevation (metres, ~13000) -- highp for the material ramp / strata / ocean depth
673
+ out highp vec3 vNrm; // W8: world-space per-vertex analytic normal (fixed-step central diff of the FULL composeHeight). The SOLE FS lit normal -- replaces the jittery cross(dFdx,dFdy). highp to match vWorld on the ~6.4e6 m planet.
674
+ out vec3 vTexWarp; // texture domain warp, computed ONCE in the VS (2026-06-12 'make warp as performant
675
+ // as possible': was 9 snoise3 PER PIXEL in the FS splat; the warp waves are >=1.8km
676
+ // so per-vertex evaluation + linear interpolation is visually identical at any GRID).
677
+ // The FS applies it EXACTLY ONCE (wt += vTexWarp * uTexWarp) -- single-application
678
+ // by construction, every texture layer inherits it through the shared wt.
679
+ // W5: vNrmPV/vMacroSlope/vShadeAO out-varyings deleted -- the lit normal, rock-gate slope and per-vertex
680
+ // AO are all derived in the FS from the Sobel normal now (THC sole path).
681
+ // UNIFIED-FIELD water/incision masks: computed ONCE per vertex from the same carve fields that cut
682
+ // the geometry, then INTERPOLATED to the FS. The FS no longer re-evaluates the sharp ridged carve
683
+ // fields per-pixel (that was a separate high-freq evaluation that aliased = the biome-localized
684
+ // moire the user reported). One field, sampled at the vertices, smooth in between.
685
+ out float vLakeWet; // lake open-water mask (carve basin)
686
+ out float vRiverWet; // river thalweg wet line
687
+ out float vWaterDepth; // metres the flat inland-water plane sits ABOVE the local terrain floor (>0 = submerged)
688
+ out float vCanyonDep; // canyon gorge depth [0,1]
689
+ out float vCliffFace; // cliff/escarpment riser face [0,1] (1 = steep terrace face)
690
+ out float vDuneCrest; // dune crest [0,1]
691
+ out float vLevel; // quad LOD level (per-instance iOffset.w) for the patches debug view
692
+ out vec2 vGrid; // per-quad parametric mesh coord [0,1] (for the wireframe overlay)
693
+ out vec4 vClimate; // (seaBias, elevAmp, temp, humid) sampled ONCE per vertex from the HPF
694
+ // texture + INTERPOLATED, so the FS never reads the HPF texture per-pixel
695
+ // (that per-pixel texture read showed the HPF texel grid as UV lines/moire
696
+ // up close, biome colours following the blocky cells -- user: 'definitely UV').
697
+
698
+ // cube face index -> face-local->world rotation (col0=U, col1=V, col2=center). MUST match the JS
699
+ // FACE_FRAME in planet-orchestrator.js + render localToWorld3(). Built per-instance from iFace so
700
+ // the whole visible leaf set draws in ONE instanced call.
701
+ mat3 faceFrame(float f){
702
+ int i = int(f + 0.5);
703
+ if (i==0) return mat3( 0.0,0.0,-1.0, 0.0,1.0,0.0, 1.0,0.0,0.0); // +X
704
+ if (i==1) return mat3( 0.0,0.0, 1.0, 0.0,1.0,0.0, -1.0,0.0,0.0); // -X
705
+ if (i==2) return mat3( 1.0,0.0,0.0, 0.0,0.0,-1.0, 0.0,1.0,0.0); // +Y
706
+ if (i==3) return mat3( 1.0,0.0,0.0, 0.0,0.0, 1.0, 0.0,-1.0,0.0); // -Y
707
+ if (i==4) return mat3( 1.0,0.0,0.0, 0.0,1.0,0.0, 0.0,0.0,1.0); // +Z
708
+ return mat3(-1.0,0.0,0.0, 0.0,1.0,0.0, 0.0,0.0,-1.0); // -Z
709
+ }
710
+
711
+ // continental elevation bias (meters) from the HPF (shared hpfSample), as a continuous
712
+ // function of world dir -> replaces the old hardcoded sin/cos lobe with the editable field.
713
+ highp float continentalBias(vec3 dir) { return hpfSample(dir).r; } // W7: R = seaBias (meters) -> highp
714
+
715
+ // ---- CONTINUOUS BROAD+MID SHAPE (LOD-uniformity fix, user-directive 2026-05-30). The Proland
716
+ // per-level g_noiseAmp cascade adds a DIFFERENT noise draw at each LOD, so consecutive levels
717
+ // look different (1500km vs 1200km "way different"). The fix: source the broad+regional SHAPE
718
+ // from ONE continuous fBm of WORLD DIRECTION, sampled the same at every LOD -> a finer level is
719
+ // a denser SAMPLE of the SAME field, divergence ~0 BY CONSTRUCTION (CLI continuous-field-proof
720
+ // meanDiv 0.02-0.08 vs cascade 0.52). Because it is a pure function of world dir it is:
721
+ // - LOD-invariant -> identical in zf and zc, so CLOD blend is automatically smooth
722
+ // - C0 across tile AND cube-face boundaries -> seamless by construction (no seam probe needed)
723
+ // The wasm cascade is demoted (g_noiseAmp L0-6 ~0) so it only adds fine sub-silhouette detail.
724
+ // (shash3/snoise3 are defined in the SHARED preamble above so the FS riverMask can use them too.)
725
+ // broad+mid fBm of unit world dir -> metres. ONE field sampled IDENTICALLY at every LOD: a finer
726
+ // LOD is a denser SAMPLE of the SAME function, so consecutive LODs are maximally similar BY
727
+ // CONSTRUCTION (CLI continuous-field-proof meanDivergence 0.02 vs cascade 0.52). Crucially there
728
+ // is NO tile-size/LOD term here -- a per-tile octave fade was tried and REFUTED live (it made the
729
+ // field itself LOD-DEPENDENT: 1500km faded out octaves a 1200km tile kept, so they diverged).
730
+ // 8 fixed octaves span continent->regional->local; baseFreq sets the coarsest continent
731
+ // wavelength; seaBias offset sets land fraction (~0.3). Validated regime A0 5000, gain 0.6.
732
+ // PER-BIOME RELIEF (reliefMul, ridgeMul): the continent BASE octaves (o0-5) are ALWAYS full so
733
+ // the land/sea silhouette + hypsometry + LOD-uniformity are untouched (they were CLI/browser
734
+ // validated). The REGIONAL+FINE octaves (o6-13) are scaled by reliefMul and folded toward RIDGES
735
+ // by ridgeMul, BOTH supplied per-point from the anchor biome (mountains: high relief + ridged;
736
+ // plains/meadow: low relief; desert: medium smooth; swamp/tundra: very low). This makes the
737
+ // SHAPE distinguish biomes (not just color) WITHOUT breaking the broad silhouette or LOD-safety
738
+ // (still a pure fn of world dir -> denser-sample-of-same-field, no per-LOD term).
739
+ // ---- PER-VERTEX micro-relief: the FINE CONTINUATION of the single terrain field, below the
740
+ // scale broadShapeM resolves at vertex rate. Keyed on ABSOLUTE world wavelengths (fixed metres),
741
+ // NOT tile size, so a finer LOD is a DENSER SAMPLE of the SAME fine field -> detail grows
742
+ // monotonically on approach and never "pops" between LODs (the old tileM-relative wavelength was
743
+ // a different draw per level, which popped, so it was disabled). Pure fn of face-local world
744
+ // position (continuous across tiles on a face -> seamless). Default ON: this is what gives a
745
+ // close-up patch real relief instead of sitting flat inside one broad feature.
746
+ // vtxDetail/vhash/vnoise2/vnoise2D MOVED to the common preamble (THC-Normal W1) so composeHeight() can
747
+ // call them; vtxDisplace MOVED to the shared VS/PROBE region. (were here.)
748
+ // anchor elevAmp -> rugged multiplier. MEASURED LIVE (browser-1887): rendered elevAmp clusters
749
+ // ~8.0-8.5 (NOT the full 7.5-10.5 band range), so the old smoothstep(7.8,9.6) damped EVERYWHERE
750
+ // to ~0.2 -> the whole planet read flat. Recalibrate to the actual distribution: map [7.9,8.7]
751
+ // so typical terrain gets meaningful relief (~0.5-1.8) and only genuinely-low belts soften,
752
+ // high belts amplify into mountains. Floor 0.5 keeps plains gently rolling, not dead-flat.
753
+ // elevAmp range is REALLY ~29-40 (browser-4626), not ~8 -- the old smoothstep(7.9,8.7) saturated to
754
+ // 1.8 everywhere (uniform rugged -> uniform bumpiness, no plains/mountain differentiation). Map the
755
+ // real range so plains (low elevAmp) are gently rolling and only the high tail is rugged.
756
+ // ruggedFromElevAmp + faceWarp MOVED to the common preamble (THC-Normal W1, shared by composeHeight).
757
+
758
+ void main() {
759
+ // PER-INSTANCE deform params (one instanced draw over the whole leaf set): the quad origin+size
760
+ // and its face frame come from the instance attributes, replacing the old per-quad uniforms.
761
+ highp vec4 defOffset = iOffset; // W7: face-local metres ~6.4e6 -> highp
762
+ mat3 defLocalToWorld = faceFrame(iFace); // face-local -> world rotation
763
+ // (elevation atlas removed -- terrain height is the GPU fractal broadShapeM, no tile sample.)
764
+ // world direction of this vertex (pre-deform) for the continental mask.
765
+ vec3 dir0 = normalize(defLocalToWorld * vec3(faceWarp(vertex.xy * defOffset.z + defOffset.xy), defRadius)); // W7: faceWarp/defRadius highp -> pre-normalize ~6.4e6 stays highp
766
+ highp vec4 hpf0 = hpfSample(dir0); // (seaBias=r, elevAmp=g, temp=b, humid=a) -- W7: R seaBias metres
767
+ highp float cbias = hpf0.r; // W7: seaBias metres
768
+ // ANCHOR-DRIVEN morphology: the anchor elevAmp belt decides whether this region is flat
769
+ // lowland (floodplain/meadow) or rugged mountain -- the anchor map is the primary spreader.
770
+ float rugged = ruggedFromElevAmp(hpf0.g);
771
+ // per-vertex micro-relief: unique height per vertex from world-continuous face-local pos
772
+ // (fixes the flat 2x2 atlas-texel blocks; gives sub-mesh-cell relief). Only on land.
773
+ highp vec2 faceLocal = faceWarp(vertex.xy * defOffset.z + defOffset.xy); // W7: warped metres ~6.4e6 -> highp
774
+ float vDisp = vtxDisplace(faceLocal, defOffset.z, rugged);
775
+ // CONTINUOUS broad+mid SHAPE (LOD-uniformity fix): one world-dir fBm sampled the SAME at
776
+ // every LOD -> no per-level shape divergence. PER-BIOME RELIEF: scale the regional+fine octaves
777
+ // (continent base untouched -> silhouette/hypso/LOD-uniformity preserved) by an anchor-derived
778
+ // reliefMul + ridgeMul so the SHAPE distinguishes biomes: mountains (high elevAmp) = strong +
779
+ // ridged; meadow/plains = gentle. All pure fn of world dir -> still seam-safe + LOD-invariant.
780
+ float bTemp = hpf0.b, bHum = hpf0.a, bAmp = hpf0.g;
781
+ // MOUNTAIN-BELT GATE on the BAKED elevAmp range (~14.5-18.9): the high tail (~17%) is the
782
+ // mountain belt; the rest is genuine plains so the mountain biome is a differentiated minority.
783
+ float mtn = smoothstep(16.8, 18.6, bAmp); // mountain-belt weight 0..1
784
+ float wetLowFlat = smoothstep(0.66, 0.9, bHum) * (1.0 - mtn); // swamp/wetland -> flat
785
+ float coldFlat = (1.0 - smoothstep(0.18, 0.34, bTemp)); // tundra/ice -> flat
786
+ float reliefMul = clamp(0.45 + 1.25 * mtn - 0.30 * wetLowFlat - 0.25 * coldFlat, 0.15, 1.7);
787
+ float ridgeMul = clamp(mtn * 1.1, 0.0, 1.0); // ridged crests only in mountain belts
788
+ // ISLAND-TYPE VARIETY: small offshore swells get a per-region type (volcanic cone / low atoll)
789
+ // from a world-dir noise so islands are not all scaled continents. Pure fn of world dir.
790
+ float isleZone = smoothstep(50.0, 350.0, cbias) * (1.0 - smoothstep(900.0, 1600.0, cbias));
791
+ if (isleZone > 0.0) {
792
+ float isleType = snoise3(dir0 * 9.0);
793
+ float volcanic = smoothstep(0.25, 0.7, isleType);
794
+ float atoll = smoothstep(0.25, 0.7, -isleType);
795
+ reliefMul = mix(reliefMul, mix(reliefMul, 1.6, volcanic) * (1.0 - 0.7 * atoll), isleZone);
796
+ ridgeMul = mix(ridgeMul, max(ridgeMul, 0.9 * volcanic), isleZone);
797
+ }
798
+ highp float bShape = broadShapeM(dir0, reliefMul, ridgeMul); // W7: metres
799
+ // ONE FRACTAL: the continuous world-dir field (bShape) + the continental swell (cbias) carry the
800
+ // entire silhouette+hypsometry. The old wasm upsample-and-add cascade (zfc.x) was REMOVED -- it
801
+ // was a SECOND shape source that diverged per-LOD (the detail-inversion root). bShape amplitude
802
+ // was retuned (A0 6500, off -900) so this single field hits Earth hypso without the cascade.
803
+ vH = cbias + bShape;
804
+ // shelf/slope bathymetry remap + underwater displacement -- MUST mirror composeHeight exactly
805
+ // (this is the FS-material running value; composeHeight is the geometry/probe height).
806
+ if (vH < 0.0) {
807
+ highp float dSea = -vH;
808
+ vH = -(min(dSea, 500.0) * 0.24 + max(dSea - 500.0, 0.0) * 1.19);
809
+ }
810
+ vH += vDisp;
811
+ // LAKE CARVE: carve a real basin into the elevation on WET LAND so lakes are part of the
812
+ // fractal (no fade-in). Gated to land (vH>0) + wet regions (humid) so it does not pit deserts
813
+ // or deepen the ocean. World-dir field -> identical at every LOD.
814
+ float lakeWetV; float lakeCarveRaw = lakeCarveM(dir0, lakeWetV);
815
+ float lakeGate = smoothstep(0.60, 0.85, hpf0.a) * step(0.0, vH);
816
+ float lakeCarveV = lakeCarveRaw * lakeGate;
817
+ vH += lakeCarveV;
818
+ // FLAT WATER: inside the wet core the surface must read as flat water (user: 'water should be
819
+ // flat'), not a noisy bowl floor. Pin vH toward a constant water level (a few m below the local
820
+ // rim) weighted by the wet mask, and suppress the micro-relief there. The lakeCarve shoulder
821
+ // already erodes/grades the surrounding terrain into the basin (the 'eroded margin').
822
+ float lakeWet = lakeWetV * lakeGate;
823
+ highp float waterPlane = 0.0; highp float floorBefore = 0.0; float haveWater = 0.0; // W7: waterPlane/floorBefore are metres (~13000); their small DIFFERENCE (vWaterDepth) needs highp to avoid cancellation
824
+ if (lakeWet > 0.0) {
825
+ highp float waterLevel = max(vH, 0.0) - 25.0; // flat plane just below the rim
826
+ floorBefore = vH; // carved bowl floor before flattening
827
+ vH = mix(vH, waterLevel, lakeWet);
828
+ waterPlane = waterLevel; haveWater = lakeWet;
829
+ vDisp *= (1.0 - lakeWet); // no micro-bumps on the water surface
830
+ }
831
+ // RIVER + CANYON INCISION into the elevation (user: content makes up part of the elevation as
832
+ // erosion works). Both are pure world-dir carves (LOD-invariant, no fade). River: wet/temperate
833
+ // land. Canyon: arid + elevated (h>~60m) land, opposite climate to rivers. Gated to land
834
+ // (step(0,vH)) so they never punch the ocean or create fake seas. The incision is bounded so it
835
+ // does not drive coastal land below sea level (clamp the post-carve vH floor handled implicitly
836
+ // by the small depths + land gate; coastline hypso re-witnessed in incision-hypso-landfrac-gate).
837
+ float riverWet = smoothstep(0.30, 0.55, hpf0.a) * smoothstep(0.20, 0.34, hpf0.b); // moist, not frozen
838
+ float riverWetMask; float riverCarveV = riverCarveM(dir0, riverWetMask) * riverWet * step(0.0, vH);
839
+ float canyonArid = (1.0 - smoothstep(0.40, 0.58, hpf0.a)) * smoothstep(0.38, 0.56, hpf0.b) * smoothstep(60.0, 200.0, vH);
840
+ float canyonDepMask; float canyonCarveV = canyonCarveM(dir0, canyonDepMask) * canyonArid * step(0.0, vH);
841
+ // NO-SUB-SEA-COAST GUARD (user 2026-06-02 deepen+widen): with the deepened canyon (-1400m + gullies)
842
+ // a gorge on 200m coastal land would punch vH to ~-1200m = fake inland seas. Clamp the TOTAL incision
843
+ // so post-carve land bottoms out at a small floor (-60m: a gorge may reach near sea level but never
844
+ // carves a deep basin below it). Bounded against the PRE-carve vH so deep inland canyons keep full
845
+ // depth while coastal ones are limited by their own available headroom.
846
+ float inciseTot = riverCarveV + canyonCarveV; // both negative (downcut)
847
+ inciseTot = max(inciseTot, min(-60.0 - vH, 0.0)); // land floor only -- ocean keeps real depth (see composeHeight note)
848
+ vH += inciseTot;
849
+ // CLIFF TERRACING: snap the arid+elevated land into flat benches with steep risers (mesa/butte
850
+ // cliff country). Gated by the SAME canyonArid mask so cliffs share canyon regions (a coherent
851
+ // arid badlands look). The snap delta is added to vH; cliffFaceMask (->1 on a riser face) goes to
852
+ // the FS for strata banding + steep-rock material. Applied AFTER carves so canyon walls + cliff
853
+ // risers compose. step(0,vH) keeps it on land.
854
+ float cliffFaceMask; float cliffCarveV = cliffTerraceM(dir0, vH, cliffFaceMask) * canyonArid * step(0.0, vH);
855
+ vH += cliffCarveV;
856
+ // FLAT RIVER WATER (user: 'rivers/lakes should NOT be bumpy'): like lakes, the river thalweg
857
+ // surface must read as flat water, not the bumpy micro-relief floor. Pin vH down to the channel
858
+ // water level + suppress the micro-displacement on the wet line. riverWetMask is the thalweg mask.
859
+ float riverWetLine = riverWetMask * riverWet * step(0.0, vH);
860
+ if (riverWetLine > 0.0) {
861
+ highp float rWaterLevel = vH - 20.0; // W7: metres
862
+ highp float rFloorBefore = vH;
863
+ vH = mix(vH, rWaterLevel, riverWetLine);
864
+ vDisp *= (1.0 - riverWetLine); // no micro-bumps on the river surface
865
+ // record the deeper/stronger of lake|river as the water surface for vWaterDepth
866
+ if (riverWetLine > haveWater) { waterPlane = rWaterLevel; floorBefore = rFloorBefore; haveWater = riverWetLine; }
867
+ }
868
+ // DUNES on the SAND DESERT (very dry + warm + LOW land): rolling dune relief replaces harsh rock
869
+ // here (ref: webgl-dunes). Gated opposite to canyons (which want elevated arid plateaus) -- dunes
870
+ // ride the low desert floor. World-dir field -> LOD-invariant.
871
+ float duneSand = smoothstep(0.62, 0.85, 1.0 - hpf0.a) * smoothstep(0.40, 0.58, hpf0.b) * (1.0 - smoothstep(40.0, 160.0, vH));
872
+ float duneCrest; float duneV = duneFieldM(dir0, duneCrest) * duneSand * step(0.0, vH);
873
+ vH += duneV;
874
+
875
+ // PER-VERTEX SEAMLESS NORMAL = (-dz/dx, -dz/dy, 1) in the tile tangent frame. After the
876
+ // one-fractal collapse the old cascade-atlas finite-difference term is gone (it was identically
877
+ // zero), so the lit normal is built purely from the micro-relief (dEx), broad-shape (bEx) and
878
+ // carve (rcEx) gradients below -- those ARE the one field.
879
+ // Include the per-vertex micro-displacement gradient so the new relief is LIT (not just
880
+ // geometrically displaced). MOIRE-ON-DESCENT FIX (user 2026-06-01h: fine green speckle/moire at
881
+ // closeup, GONE with vtxDetail off): the FD step was ONE MESH CELL (defOffset.z/24), which
882
+ // SHRINKS as tiles get fine on descent -> the micro-relief lit normal captured ever-higher-freq
883
+ // slope cell-to-cell = shimmer/moire (the same shrinking-step trap the broadShape FD warns of).
884
+ // Use a FIXED ~600m world step so the micro-relief SHADING reflects a stable slope at every
885
+ // altitude; the geometry still displaces per-vertex (vH), the normal just stops chasing the cell.
886
+ // W5: the per-vertex lit-normal gradient block (dEx/dEy from vtxDisplaceD; bEx/bEy/rcEx/rcEy/fEx/fEy
887
+ // from broadShapeMD + the cbias/carve central-differences) is DELETED -- it only fed the now-deleted
888
+ // vNrmPV/vMacroSlope assembly. THC's per-pixel Sobel is the sole lit normal. The carve VALUES (h) are
889
+ // computed above (~906-) and untouched; only their gradient finite-differences are gone.
890
+ // W5: the entire per-vertex normal/AO assembly (vMacroSlope, vShadeAO crease+micro AO, slopeGain, the
891
+ // vNrmPV true-gradient sum) is DELETED -- THC is the sole path, so the lit normal is the FS Sobel of
892
+ // heightPool and the rock-gate slope + per-vertex AO are recomputed in the FS from that Sobel normal.
893
+ // The gradient taps (dEx/dEy from vtxDisplaceD, bEx/bEy/rcEx/rcEy/fEx/fEy from broadShapeMD) that fed
894
+ // this block are removed upstream; only the SCALAR height fns (broadShapeM/vtxDisplace) remain, for the
895
+ // procedural h fallback when a leaf has no cached tile.
896
+
897
+ highp float R = defRadius; // W7: ~6.4e6 m -> highp
898
+ // ONE HEIGHT FUNCTION: every vertex gets the procedural composeHeight() (broadShapeM + cbias + carves
899
+ // + vtxDisplace, with lake/river water-plane flattening). vH (FS material/strata, computed above as the
900
+ // water-flattened running value) stays for the FS; the GEOMETRY height h is the single composeHeight.
901
+ // WATER GATE (perf sweep 2026-06-11): on the water pass the geometry height is pinned to sea level
902
+ // (hR=0 below) and the normal is radial (:914), so composeHeight's result is never consumed --
903
+ // skip the 12-oct eval for ~540 water instances/frame. Uniform-coherent branch, zero fidelity change.
904
+ // SINGLE-INSTANCE FD TAPS (2026-06-12, THE AMD ROOT -- witnessed by __flatNormal A/B: forcing the
905
+ // radial normal turned the whole 'rock patch' region back into smooth grass, so vNrm was the broken
906
+ // carrier). FXC inlines composeHeight per CALL SITE and optimizes each copy differently; the center
907
+ // and offset taps then disagree by tens of metres on FLAT ground -> fake slope -> rock material +
908
+ // slope-AO darkness + dead normals (d3d11-only; vulkan compiles one consistent version). Evaluating
909
+ // ALL THREE taps through ONE runtime-bounded loop forces FXC to emit a SINGLE composeHeight instance,
910
+ // so whatever approximations it picks cancel exactly in the differences. uNrmStepM>0.0 keeps the
911
+ // bound non-constant (same defeat class as uOctMax).
912
+ highp vec3 uzFD = dir0;
913
+ vec3 refAxisFD = (abs(uzFD.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
914
+ vec3 uxFD = normalize(cross(refAxisFD, uzFD));
915
+ vec3 uyFD = cross(uzFD, uxFD);
916
+ highp float nStepFD = (uNrmStepM > 0.0) ? uNrmStepM : 150.0;
917
+ highp float hFD0 = 0.0, hFD1 = 0.0, hFD2 = 0.0;
918
+ if (uIsWater < 0.5) {
919
+ int fdIters = (uNrmStepM >= 0.0) ? 3 : 1; // always 3; non-constant so FXC cannot unroll/split
920
+ for (int i = 0; i < fdIters; i++) {
921
+ highp vec3 dd = (i == 0) ? dir0
922
+ : normalize(uzFD + ((i == 1) ? uxFD : uyFD) * (nStepFD / defRadius));
923
+ highp float hi = composeHeight(dd, faceLocal, defOffset.z);
924
+ if (i == 0) hFD0 = hi; else if (i == 1) hFD1 = hi; else hFD2 = hi;
925
+ }
926
+ }
927
+ highp float h = hFD0;
928
+ // OCEAN TOP = A SEPARATE, ELEVATION-BASED SURFACE (user 2026-06-10: 'terrain should extend into
929
+ // the ocean, the ocean top separate and elevation based'). composeHeight keeps carrying the TRUE
930
+ // signed bathymetry (vH -> the FS depth tint/Beer-Lambert keys on it), but the RENDERED surface
931
+ // smooth-clamps to sea level: open water is a flat plane at R (proper waterline + horizon), the
932
+ // seafloor field lives on UNDER it as the depth signal. The smoothstep ramp (not a hard max)
933
+ // also fixes 'the terrain curve creates a hard line at the base': the beach profile eases
934
+ // tangentially into the waterline over the last ~60m instead of meeting it in a crease.
935
+ // SEPARATE WATER SURFACE (user 2026-06-11): the terrain pass renders the TRUE seabed (the old
936
+ // smoothstep(-60,60) sea-level clamp is GONE -- sand/rock bathymetry is real geometry now); the
937
+ // water pass (uIsWater=1, second instanced draw of the same leaves) pins this mesh to sea level
938
+ // and shades it as the animated ocean, alpha-blended over the seabed (depth test keeps it
939
+ // behind land).
940
+ highp float hR = (uIsWater > 0.5) ? 0.0 : h;
941
+ // W8 ANALYTIC LIT NORMAL (P1 data-first): the normal is a property of the height FIELD, so derive it
942
+ // from the field, not from per-pixel screen derivatives. FIXED-WORLD-STEP central difference of
943
+ // composeHeight in the radial tangent frame -- STABLE (does NOT shrink with distance/per-cell -> no
944
+ // shimmer, see :769), full landform relief at ALL distances, no fade. Mirror of the proven nBand
945
+ // pattern but the FULL composeHeight (not macro-only broadShapeLowM). Offset ONLY dir0 (the world-dir
946
+ // gradient = the seam-safe landform relief); hold faceLocal/tileM fixed so the tile-local vtxDisplace
947
+ // term cancels in the difference (it carries no world-continuous gradient and would only add jitter).
948
+ vec3 uzN = dir0; // dir0 is already normalized; radial up
949
+ vec3 refAxisN = (abs(uzN.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
950
+ vec3 uxN = normalize(cross(refAxisN, uzN));
951
+ vec3 uyN = cross(uzN, uxN);
952
+ // FXC FOLD-DEFEAT (2026-06-12, same disease as the uOctMax unroll fix): with a LITERAL 150.0 the
953
+ // d3d11/FXC translator constant-folds/reassociates the tiny FD offset (150/R = 2.4e-5) feeding
954
+ // normalize(uzN + uxN*eps) -- the perturbed-direction taps then diverge wildly from h on FLAT
955
+ // ground (witnessed: probe slope 0.000 + flat wireframe mesh under whole regions the FS painted
956
+ // as rock; the FD normal claimed steep in noise-keyed PATCHES = 'rock without slope angle').
957
+ // A uniform-fed step denies FXC the constant, exactly like uOctMax denies it the loop bound.
958
+ highp float nStep = (uNrmStepM > 0.0) ? uNrmStepM : 150.0; // STABLE world step (m): > finest vtxDisplace octave (~20m, no alias),
959
+ // < carve band (~100m+, resolves canyons/cliffs). NOT pxWorld-scaled.
960
+ if (uIsWater > 0.5) {
961
+ vNrm = uzN; // water surface: radial normal; waves perturb it per-pixel in the FS (skips 2 composeHeight taps)
962
+ } else {
963
+ // taps come from the SINGLE-INSTANCE FD loop above (hFD0/1/2) -- never call composeHeight at
964
+ // separate call sites for the FD again; per-callsite FXC codegen divergence was the AMD root.
965
+ float dHduN = float(hFD1 - h) / nStep;
966
+ float dHdvN = float(hFD2 - h) / nStep;
967
+ vNrm = normalize(uxN * (-dHduN) + uyN * (-dHdvN) + uzN); // n = normalize([-dz/du,-dz/dv,1]) in the (ux,uy,uz) frame
968
+ }
969
+ // DIRECT per-vertex sphere projection (replaces the Proland deformedCorners*alphaPrime blend,
970
+ // which bilinearly interpolated 4 deformed corners -> FLAT quad interior -> faceted at high GRID).
971
+ // dir0 is THIS vertex's world direction (faceWarp'd, defLocalToWorld-mapped); place it on the
972
+ // sphere at radius R+h and project. Every vertex curves -> round at any tessellation.
973
+ // SKIRT: outer-ring verts (vertex.z==1, xy clamped to the true edge) drop radially below the
974
+ // surface to form a near-vertical curtain that hides LOD T-junction cracks without painting a
975
+ // visible flat band (the old overlap ring's artifact). Depth scales with the tile size so the
976
+ // skirt always reaches below a coarser neighbor's surface. Hidden behind the surface from above.
977
+ // water pass: NO skirt (user 2026-06-11) -- the surface is an exact sphere at R, so adjacent
978
+ // LODs agree exactly and there are no T-junction cracks to hide; a skirt would only drape a
979
+ // visible curtain through the transparent shallows.
980
+ highp float skirt = (vertex.z > 0.5 && uIsWater < 0.5) ? max(defOffset.z * 0.12, 60.0) : 0.0; // W7: metres (tile-size scaled) -> highp
981
+ vWorld = dir0 * (R + hR - skirt); // ABSOLUTE world pos (RENDER height: ocean top flat) -> FS lighting/atmosphere
982
+ // TEXTURE DOMAIN WARP -- VS-side, HALVED frequencies (user 2026-06-12 'halve the warp frequency' +
983
+ // 'as performant as possible'): lattice 900/3600/14000 -> 450/1800/7000 (waves 2x larger: ~88km/22km/5.6km
984
+ // at the same +/-1.2/0.6/0.3-tile amplitudes). Moved out of the per-pixel FS (-9 snoise3/pixel); dir0 is
985
+ // the same normalize(vWorld) the FS used, so the field is identical, just vertex-sampled.
986
+ {
987
+ highp vec3 w0 = dir0 * 450.0, w1 = dir0 * 1800.0, w2 = dir0 * 7000.0;
988
+ vTexWarp = vec3(snoise3(w0), snoise3(w0 + vec3(7.3)), snoise3(w0 + vec3(23.9))) * 1.2
989
+ + vec3(snoise3(w1), snoise3(w1 + vec3(13.7)), snoise3(w1 + vec3(31.1))) * 0.6
990
+ + vec3(snoise3(w2), snoise3(w2 + vec3(5.1)), snoise3(w2 + vec3(17.9))) * 0.3;
991
+ }
992
+ // CAMERA-RELATIVE PROJECTION (vertex-jitter fix): forming dir0*(R+h) at ~6.4e6m rounds to ~0.5m in
993
+ // fp32 -> vertices quantize and JITTER as the camera moves (the same fp32 cancellation gl-render.js
994
+ // documents for the cull, which the per-vertex draw path did NOT avoid). Build the SMALL camera-
995
+ // relative position directly -- (dir0-defCamDir)*R is the lateral offset computed from a unit-vector
996
+ // difference (precise, no 6.4e6 intermediate), plus the small radial terms -- and project with
997
+ // defViewProjNoEye (no folded translate(-eye)). Result magnitude ~horizon scale, fp32-precise.
998
+ highp vec3 vRel = (dir0 - defCamDir) * R + dir0 * (hR - skirt) - defCamDir * defCamAlt; // W7 highp ISLAND: camera-relative projection (render height; planet-scale fp32 cancellation fix kept intact)
999
+ gl_Position = defViewProjNoEye * vec4(vRel, 1.0);
1000
+ // UNIFIED carve masks -> FS (interpolated; the FS no longer re-evaluates the sharp carve fields
1001
+ // per-pixel). riverWetMask/canyonDepMask/duneCrest are the out-params captured above; lakeWetV
1002
+ // from the lake-carve block. Gated by the SAME climate masks the geometry used.
1003
+ vLakeWet = lakeWetV * lakeGate;
1004
+ vRiverWet = riverWetMask * riverWet * step(0.0, vH);
1005
+ // SUBMERGED DEPTH (user 2026-06-01i: 'carves dont line up with the water'). The inland-water
1006
+ // COLOR must land ONLY where the surface is actually at/below the flat water plane, not over the
1007
+ // whole carve mask (which includes the graded erosion shoulder ABOVE the waterline). vWaterDepth
1008
+ // = metres the water plane sits above the pre-flatten carved floor, >0 ONLY inside the true open
1009
+ // water; the FS gates ALL inland-water shading on this so blue == the flat water, banks stay land.
1010
+ vWaterDepth = max(waterPlane - floorBefore, 0.0) * step(0.001, haveWater);
1011
+ vCanyonDep = canyonDepMask * canyonArid * step(0.0, vH);
1012
+ vCliffFace = cliffFaceMask * canyonArid * step(0.0, vH);
1013
+ vDuneCrest = duneCrest * duneSand;
1014
+ vGrid = vertex.xy; // parametric mesh-cell coord for the wireframe overlay
1015
+ vLevel = defOffset.w; // quad LOD level -> FS patches view
1016
+ vClimate = hpf0; // (seaBias, elevAmp, temp, humid) -> FS reads this, not the HPF texture
1017
+ }
1018
+ #endif
1019
+
1020
+ #ifdef _FRAGMENT_
1021
+ in highp vec3 vWorld; // W7: MUST match the VS highp vWorld (world pos ~6.4e6 m) -- precision-mismatched varyings fail to link
1022
+ in vec3 vTexWarp; // VS-computed texture domain warp (halved freqs); applied once in the splat block
1023
+ in highp float vH; // W7: match VS highp vH (signed metres)
1024
+ in highp vec3 vNrm; // W8: world-space analytic normal from the VS (matches VS highp out). Sole lit normal.
1025
+ in float vLakeWet; // carve masks computed in the VS, interpolated (no per-pixel re-eval -> no moire)
1026
+ in float vRiverWet;
1027
+ in float vWaterDepth; // metres of submerged water (>0 = real open inland water at the flat plane)
1028
+ in float vCanyonDep;
1029
+ in float vCliffFace;
1030
+ in float vDuneCrest;
1031
+ in float vLevel; // quad LOD level (patches view)
1032
+ in vec2 vGrid; // per-quad parametric mesh coord (wireframe overlay)
1033
+ uniform float uWireframe; // 1 = overlay the mesh grid lines (window/cam wireframe toggle)
1034
+ uniform float uFsCheap; // GPU-TIMER VS/FS attribution: 1 = short-circuit the FS to a trivial
1035
+ // constant color immediately after the per-vertex normal is read, so a
1036
+ // timed cheap frame measures VS+raster cost only; (full - cheap) = FS cost.
1037
+ // Set by window.__gpuTimer's measure frame (gl-render). 0 in normal render.
1038
+ in vec4 vClimate; // (seaBias, elevAmp, temp, humid) interpolated -- FS does NOT sample the HPF texture
1039
+ layout(location=0) out vec4 fragColor;
1040
+
1041
+ uniform vec3 sunDir; // world-space sun direction (normalized) -- unit, mediump-safe
1042
+ uniform int displayMode; // 0 = lit, 1 = raw normals, 2 = material albedo (unlit)
1043
+ uniform highp vec3 camWorld; // W7: world-space camera position ~6.4e6 m -> highp
1044
+ uniform highp float terrainR; // W7: sphere radius ~6.4e6 m -> highp
1045
+ uniform highp float oceanTime; // W7: animation time (seconds) grows unbounded -> highp (fp16 would freeze the wave phase)
1046
+ uniform float oceanAmp; // wave amplitude scale (0..1+), drives normal perturbation
1047
+ uniform float oceanChoppy; // wave directional sharpness / count weighting
1048
+ uniform float oceanFoam; // foam amount (0..1): whitecaps on steep wave slopes
1049
+ // CONTINUOUS sampled world position (seamless across tiles), 0 = atlas normal
1050
+
1051
+ // ---- Animated ocean: sum-of-Gerstner-wave NORMAL perturbation in the surface tangent
1052
+ // frame. We don't displace geometry (FS-only v1) -- we synthesize an animated water
1053
+ // normal from a few directional waves and shade it with fresnel + sun glint + a depth
1054
+ // tint. wave dirs are 2D in the local (ux,uy) tangent plane; phase advances with time.
1055
+ // Returns a tangent-space normal perturbation (dx,dy) to add to the flat (0,0,1) normal.
1056
+ vec2 oceanWaveSlope(highp vec2 p, highp float t) { // W7: p = camera-relative wave coord, t = unbounded oceanTime -> highp phase
1057
+ // a handful of directional waves with varied freq/dir/speed (kept cheap)
1058
+ vec2 slope = vec2(0.0);
1059
+ // (dir.xy, wavelength_m, speed, steepness)
1060
+ const int N = 5;
1061
+ vec2 dirs[5]; float wl[5]; float spd[5]; float amp[5];
1062
+ dirs[0]=vec2( 1.0, 0.0); wl[0]=520.0; spd[0]=1.10; amp[0]=1.0;
1063
+ dirs[1]=vec2( 0.6, 0.8); wl[1]=310.0; spd[1]=1.35; amp[1]=0.7;
1064
+ dirs[2]=vec2(-0.4, 0.9); wl[2]=170.0; spd[2]=1.60; amp[2]=0.5;
1065
+ dirs[3]=vec2( 0.9,-0.3); wl[3]= 95.0; spd[3]=2.10; amp[3]=0.35;
1066
+ dirs[4]=vec2(-0.7,-0.6); wl[4]= 47.0; spd[4]=2.80; amp[4]=0.22;
1067
+ for (int i=0;i<N;i++){
1068
+ vec2 d = normalize(dirs[i]);
1069
+ highp float k = 6.2831853 / wl[i]; // W7: highp wave number
1070
+ highp float phase = k*dot(d,p) + t*spd[i]*k*8.0; // W7: highp accumulated phase (unbounded t)
1071
+ // slope contribution: derivative of a sine height field -> cosine, weighted by
1072
+ // amplitude and choppiness. amp[i] tapers the higher-freq waves.
1073
+ float a = amp[i] * oceanAmp * (1.0 + oceanChoppy);
1074
+ slope += d * (cos(phase) * a * 0.06);
1075
+ }
1076
+ return slope;
1077
+ }
1078
+
1079
+ // Procedural height+slope material ramp (placeholder until the OrthoProducer lands):
1080
+ // water / shore / lowland / grass / rock / snow by signed elevation, with rock on steep
1081
+ // slopes. World-continuous (pure function of h + slope) so it has no per-tile seam.
1082
+ // LIVE-ADJUSTABLE biome ramp (full-adjustability via window.__gen.biome). Each color + band edge
1083
+ // is a uniform defaulting to its tuned literal when the JS global is unset (no behaviour change
1084
+ // until edited). bcXxx = band colors; bandEdgesLo=(shore->lowland end, lowland->grass end),
1085
+ // bandEdgesHi=(rock start, rock end), snowEdges=(snow start, snow end), seaDepthM, slopeRock=(lo,hi).
1086
+ uniform vec3 bcDeepSea, bcSea, bcShore, bcLowland, bcGrass, bcRock, bcSnow;
1087
+ uniform vec2 bandEdgesLo; // (lowland-blend end, grass-blend end)
1088
+ uniform vec2 bandEdgesHi; // (rock start, rock end)
1089
+ uniform vec2 snowEdges; // (snow start, snow end)
1090
+ uniform float seaDepthM; // depth (m) over which sea->deepSea
1091
+ uniform vec2 slopeRock; // (lo, hi) slope range that forces rock
1092
+ uniform float uAoAmt; // canyon/cliff ambient-occlusion strength (window.__aoAmt; 1.0 default)
1093
+ uniform float uBiomeBandBias;// elevation/latitude biome-band bias strength (window.__biomeBandBias; 1.0)
1094
+ // REAL-WORLD LOOK overhaul uniforms (2026-06-05 workflow wxb9n2907) -- all window.__gen-overridable.
1095
+ uniform vec3 uOceanDeep; // deep open-ocean color (near-black navy) [0.008,0.025,0.06]
1096
+ uniform vec3 uOceanShallow; // shallow-water turquoise (first metres) [0.07,0.22,0.26]
1097
+ uniform vec3 uOceanK; // per-channel Beer-Lambert extinction (kR>>kG>>kB) [0.030,0.012,0.0045]
1098
+ uniform float uBiomeSat; // biome-palette saturation pull toward luminance (1=full, <1 desaturate) 0.72
1099
+ uniform float uVariationAmt; // intra-biome value mottle amplitude (+/-) 0.08
1100
+ uniform float uHazeMul; // aerial-perspective strength multiplier (1 = full haze, 0 = none)
1101
+ // LIVE A/B ISOLATION TOGGLES (window.__rockBump / __chroma / __strata, default 1). Multiply each material
1102
+ // detail layer so the user can flip one to 0 and see which layer produces the close-up uv scramble.
1103
+ uniform float uFlatNormal; // 1=force the SMOOTH analytic normal (nBand), bypassing the raw cross(dFdx,dFdy) geometric normal that scrambles on steep deck faces (A/B isolation)
1104
+ uniform float uSkyFill; // sky-ambient fill weight (was implicit 1.0) 0.45
1105
+ uniform float uTerminatorGlow;// sunset reddening strength at the grazing terminator 0.5
1106
+ uniform float uNightLights; // night/shadow fill intensity (lift dark areas, not black) 1.0
1107
+ uniform float uNightFloor; // unlit-hemisphere brightness floor (dim, not black) 0.05
1108
+ uniform float uTermWidth; // terminator half-width (wider=softer twilight) 0.25
1109
+ uniform float uExposure; // pre-tonemap exposure (was 1.25) 1.0
1110
+ uniform float uLookSat; // post-ACES saturation Look (>1 = more saturated) 1.15
1111
+ uniform float uLookContrast; // post-ACES contrast Look (>1 = more contrast) 1.08
1112
+ // SURFACE PHOTO-TEXTURES (user 2026-06-10 'use the textures in textures/, calculate normals from
1113
+ // displacement'). Two 1024x1024x4 sampler2DArrays loaded at runtime (gl-render loadSurfaceTextures):
1114
+ // uSurfAlb = sRGB color (RGB) + displacement (A), uSurfNrm = tangent normal xy (RG, 0.5-biased) +
1115
+ // displacement (B). Layers: 0=grass 1=rock 2=sand 3=snow. The normals are Sobel-derived from the
1116
+ // displacement JPGs at load (wrap edges, JS). SUPERSEDES the 2026-06-01e 'no detail texturing'
1117
+ // directive by explicit user request. Triplanar-sampled in the FS, blended by the existing material
1118
+ // gates (slopeRock / snowEdges / climate), height(displacement)-sharpened, distance-faded by pxWorld.
1119
+ uniform sampler2DArray uSurfAlb;
1120
+ uniform sampler2DArray uSurfNrm;
1121
+ uniform float uHasSurfTex; // 1 once the arrays are uploaded (0 = procedural-only fallback)
1122
+ uniform float uTexTileM; // world metres per texture repeat (__texTile, default 2400 -- user: 24m read as noise/rock, '100x bigger')
1123
+ uniform float uTexNrmK; // texture detail-normal strength (__texNrmK, default 0.5; keep <=1: scramble lesson)
1124
+ uniform float uTexMix; // texture albedo blend amount (__texMix, default 0.85; 0 = off)
1125
+ uniform float uTexWarp; // anti-repetition domain-warp amplitude (__texWarp, default 1.0)
1126
+ uniform float uReliefShade; // gentle-slope relief-shading exaggeration (__reliefShade, default 1.7; 1 = off)
1127
+ uniform float uTexPhoto; // raw photo-color fraction (__texPhoto, default 0 = full macro tint; user: patch must match the shade it replaces)
1128
+ uniform float uTexPhotoNear; // near-field MATERIAL-IDENTITY fraction (__texPhotoNear, default 0.45): up close the
1129
+ // photo keeps its OWN hue at the macro's luminance, so ground reads clearly as
1130
+ // grass/rock/sand/snow (user 2026-06-12 'light brown patches that need to be either
1131
+ // grass or sand but is neither' -- the full macro tint painted biome browns/greys onto
1132
+ // every layer). Far field returns to the shade-matched macro (no distance pop).
1133
+ uniform vec4 uSurfMeanL; // per-layer mean linear luminance of the photo color (loader-computed; shade-match divisor)
1134
+ // MATERIAL-BOUNDARY DITHER REVERTED (2026-06-05): the threshold-perturbation approach (matEdgeNoise on
1135
+ // the smoothstep input) produced HARD-EDGED PATCHES + a UV-like grid on uniform grass/snow (user live
1136
+ // eye: 'hard uninteresting lines between rocky/grass', 'grass/snow UV problem') -- perturbing a near-
1137
+ // binary boundary snaps material across wide areas instead of softly interfingering, and the high-freq
1138
+ // octave aliased on bright materials. Reverted to the clean smoothstep boundaries; a non-aliasing
1139
+ // 'interesting boundary' technique (e.g. a wide soft transition material band) is a separate future task.
1140
+ vec3 terrainAlbedo(float h, float slope, float rockSlope, highp vec3 worldPos) { // highp: worldPos feeds normalize(worldPos)*freq noise UVs -- mediump would scramble the lattice at close range
1141
+ vec3 c;
1142
+ if (h < 0.0) {
1143
+ // SEABED CONTINUES AS LAND MATERIAL (user 2026-06-11 'instead of turning terrain into water,
1144
+ // it should continue under water as sand and rock'): below sea level the macro albedo is a
1145
+ // sandy bed on gentle ground and rock on steep faces (same slope gate as land). The water
1146
+ // look (per-channel absorption, fresnel, waves) is composited OVER this in the ocean branch,
1147
+ // so the terrain level information stays readable through shallow water.
1148
+ c = mix(bcShore, bcRock, smoothstep(slopeRock.x, slopeRock.y, rockSlope));
1149
+ } else {
1150
+ c = mix(bcShore, bcLowland, smoothstep(0.0, bandEdgesLo.x, h));
1151
+ c = mix(c, bcGrass, smoothstep(bandEdgesLo.x, bandEdgesLo.y, h));
1152
+ // ROCK IS VERTICALITY-DRIVEN, NOT HEIGHT-SPLATTED (user 2026-06-03: 'rock face based on
1153
+ // verticality not splatted directly on albedo'). The old height-band rock mix
1154
+ // (bcRock by bandEdgesHi) painted tan rock flat across every high plateau/summit regardless
1155
+ // of slope, so flat high ground read as rock. REMOVED. High flat ground now keeps its biome
1156
+ // (and snow by height below); ONLY the slope/verticality term and the steep cliff term turn
1157
+ // rock. Snow still bands by altitude (a flat snowfield is physically correct).
1158
+ c = mix(c, bcSnow, smoothstep(snowEdges.x, snowEdges.y, h));
1159
+ c = mix(c, bcRock, smoothstep(slopeRock.x, slopeRock.y, rockSlope) * step(0.0, h));
1160
+ // OLD PROCEDURAL GREY ROCKFACE DELETED (max-speed sweep 2026-06-10, user 'replace the original
1161
+ // rock completely'): the photo-rock splat owns steep faces; the 3-tap grey fBm fallback is gone.
1162
+ }
1163
+ return c;
1164
+ }
1165
+ // CLIMATE-BIASED albedo: the anchor field carries latitude-driven temp + humidity (now
1166
+ // SHIPPED). Drive Earth-like biome COLOR on land from climate so warm+wet reads lush green,
1167
+ // warm+dry reads arid tan, and cold reads pale/desaturated with an earlier snow line --
1168
+ // instead of a pure height ramp. Sea unchanged. temp,humid in [0,1].
1169
+ // BIOME PALETTE: each biome a DISTINCT recognizable color, blended by soft temp/humidity
1170
+ // thresholds (mirrors wasm/terrain-cli/biome-climate-tune.mjs which CLI-validated 7 distinct
1171
+ // classes, entropy 2.59). World-continuous (pure fn of climate -> seam-safe).
1172
+ vec3 biomeColor(float temp, float humid) {
1173
+ // PHYSICALLY-ANCHORED ALBEDOS (Real-World Look overhaul): real surface albedos are LOW and fairly
1174
+ // DESATURATED (conifer ~0.08, broadleaf ~0.15, grass ~0.22, dry sand ~0.35, snow ~0.85 the ONLY
1175
+ // bright class). The old palette was too bright + too saturated -> the sickly-yellow pastel
1176
+ // watercolour land (defect #1). Re-anchored to muted linear albedos; ICE stays bright.
1177
+ vec3 ICE = vec3(0.86, 0.90, 0.96); // snow/ice -- the only high-albedo class
1178
+ // TUNDRA WAS THE 'FLAT ROCK' (user 2026-06-11, witnessed by A/B: rock gate + texture OFF and the
1179
+ // grey patches remained = MACRO biome color): 0.34/0.36/0.31 is neutral grey, and the grass photo
1180
+ // tinted to it reads exactly as flat rock basins. Recolor to a muted olive-brown that is
1181
+ // unmistakably vegetation -- still duller/colder than MEADOW, no longer rock-grey.
1182
+ vec3 TUNDRA = vec3(0.27, 0.33, 0.18);
1183
+ // FOREST CLASSES LIFTED (user 2026-06-11 'large rocky patches... flat areas should be snow sand
1184
+ // grass' -- same disease as the TUNDRA grey: 0.03-0.13 luminance is DARKER than the rock photo,
1185
+ // and desaturation + grass-photo tinting turns the near-black greens into flat dark patches that
1186
+ // read as rock). Lifted ~2x with the green channel dominant so they are unmistakably vegetation;
1187
+ // still the darkest land classes, canopy still deepest.
1188
+ vec3 TAIGA = vec3(0.11, 0.19, 0.10); // conifer green
1189
+ vec3 FOREST = vec3(0.10, 0.22, 0.08); // broadleaf green
1190
+ vec3 DEEPFOR = vec3(0.07, 0.16, 0.07); // dense canopy (deepest green, no longer near-black)
1191
+ vec3 MEADOW = vec3(0.28, 0.34, 0.15); // muted olive grassland
1192
+ vec3 SAVANNA = vec3(0.46, 0.40, 0.22); // dry gold (desaturated)
1193
+ vec3 STEPPE = vec3(0.42, 0.39, 0.25); // pale dry grass (desaturated)
1194
+ vec3 DESERT = vec3(0.55, 0.45, 0.30); // sand ochre (desaturated, darker)
1195
+ float cold = 1.0 - smoothstep(0.16, 0.34, temp);
1196
+ float warm = smoothstep(0.42, 0.62, temp);
1197
+ float dry = 1.0 - smoothstep(0.34, 0.50, humid);
1198
+ // FOREST vs MEADOW split (user: they must look distinct, not one green). A CRISPER humidity
1199
+ // boundary (0.48->0.56, was 0.46->0.66) so meadow (drier-temperate) and forest (wetter) read
1200
+ // as DISTINCT adjacent regions instead of a long ambiguous blend; very-wet deepens to canopy.
1201
+ float wet = smoothstep(0.48, 0.56, humid);
1202
+ float veryWet = smoothstep(0.62, 0.80, humid);
1203
+ vec3 c = MEADOW; // temperate mid-humidity default
1204
+ c = mix(c, FOREST, wet); // wet -> forest (crisp boundary)
1205
+ c = mix(c, DEEPFOR, veryWet); // very wet -> dense dark canopy
1206
+ c = mix(c, SAVANNA, dry * warm); // dry + warm -> savanna
1207
+ c = mix(c, DESERT, smoothstep(0.60,0.85,1.0-humid) * warm); // very dry + warm -> desert
1208
+ c = mix(c, STEPPE, dry * (1.0-warm) * (1.0-cold)); // dry temperate -> steppe
1209
+ c = mix(c, TAIGA, wet * (1.0 - smoothstep(0.34,0.50,temp))); // cool + wet -> taiga
1210
+ c = mix(c, TUNDRA, cold); // cold -> tundra
1211
+ c = mix(c, ICE, 1.0 - smoothstep(0.10, 0.18, temp)); // very cold -> ice
1212
+ // GLOBAL SATURATION PULL toward luminance (Real-World Look): real terrain is less saturated than a
1213
+ // naive palette; pull ~28% toward grey so biomes read natural, not poster-paint. uBiomeSat<1.
1214
+ float bl = dot(c, vec3(0.2126, 0.7152, 0.0722));
1215
+ c = mix(vec3(bl), c, uBiomeSat);
1216
+ return c;
1217
+ }
1218
+ // DISCRETE biome class -> a flat distinct color (for the biome-MAP diagnostic displayMode 9 +
1219
+ // __biomeAt). Hard argmax of the same climate axes biomeColor blends, so the witness can COUNT
1220
+ // contiguous biome regions instead of reading a continuous gradient. Water/ice keyed on h/temp.
1221
+ // Returns a flat saturated key color per class (NOT the naturalistic biomeColor palette).
1222
+ #ifdef _DEBUGVIEW_
1223
+ vec3 biomeClassColor(float temp, float humid, float h) {
1224
+ if (h < 0.0) return vec3(0.10, 0.30, 0.75); // OCEAN (blue)
1225
+ if (temp < 0.14) return vec3(0.95, 0.97, 1.00); // ICE (white)
1226
+ if (temp < 0.30) return vec3(0.55, 0.60, 0.55); // TUNDRA (grey-green)
1227
+ // SWAMP: warm + very-wet + low ground (h<120m proxy) -> murky teal key color (distinct class).
1228
+ if (temp > 0.50 && humid > 0.66 && h >= 0.0 && h < 120.0) return vec3(0.20, 0.40, 0.30);
1229
+ bool warm = temp > 0.52;
1230
+ if (warm && humid < 0.22) return vec3(0.95, 0.80, 0.30); // DESERT (yellow)
1231
+ if (warm && humid < 0.42) return vec3(0.80, 0.70, 0.20); // SAVANNA (gold)
1232
+ if (humid > 0.66) return warm ? vec3(0.00,0.55,0.10) // RAINFOREST (bright green)
1233
+ : vec3(0.05,0.25,0.12); // TAIGA (dark green)
1234
+ if (humid > 0.45) return vec3(0.20, 0.65, 0.25); // FOREST (green)
1235
+ return vec3(0.55, 0.70, 0.30); // MEADOW/STEPPE (yellow-green)
1236
+ }
1237
+ // RIVER NETWORK: thin continuous water lines on land. A world-dir-continuous ridged-noise
1238
+ // network: riverField = 1 - |fBm(worldDir)| peaks (->1) along the zero-crossing ridge lines of
1239
+ // the fBm, giving a connected branching channel pattern. Thresholded thin near 1.0 -> the river
1240
+ // line. Pure fn of worldPos (vWorld) -> C0 seam-safe across tiles AND faces by construction (no
1241
+ // per-tile state). Gated to land below a snowline + present in wet/temperate regions, scarce in
1242
+ // desert (dry). `px` is a sub-pixel fade so the thin line does not alias/shimmer at altitude.
1243
+ // NOT an elevation incision (FS albedo only) -> zero CLOD/seam/LOD-uniformity risk.
1244
+ float riverMask(vec3 worldPos, float h, float temp, float humid, float px) {
1245
+ if (h <= 0.0) return 0.0;
1246
+ // ONE FRACTAL (user 2026-06-02, same fix as canyon): the FS used its OWN inline loop over
1247
+ // worldPos/terrainR (= dir*(1+h/R), elevation-shifted) -> a different field/position/resolution
1248
+ // than the VS riverCarveM. Call the IDENTICAL riverRidgeField the VS carve uses, at
1249
+ // normalize(worldPos) (== radial dir), so the FS river network coincides with the carved geometry.
1250
+ float ridge = riverRidgeField(normalize(worldPos)); // 0..1, ->1 on the channel network
1251
+ // thin the network to a line. MEASURED ridge distribution (diag-river.mjs nodejs-2037):
1252
+ // p90=0.858 p97=0.903 max=0.985; areaFrac>0.90 = 3.4%, >0.88 = 6%. Center the line band at
1253
+ // ~0.90 (lo 0.88 -> hi 0.935) for ~4-6% believable drainage density -- the old 0.92->0.985
1254
+ // band selected <2% and was invisible. Width grows slightly with px so the line stays >=1px.
1255
+ float wid = clamp(px * 0.0008, 0.0, 0.04);
1256
+ float line = smoothstep(0.875 - wid, 0.935, ridge);
1257
+ // climate gate: rivers in wet/temperate land, sparse in desert (very dry+warm) and on ice.
1258
+ float wetGate = smoothstep(0.30, 0.55, humid); // need some moisture
1259
+ float notFrozen = smoothstep(0.20, 0.34, temp); // not polar ice
1260
+ // NO altFade (user 2026-06-02: 'river field is fading in instead of integrated'). The line WIDTH
1261
+ // already grows with px (wid above) so the channel stays >=1px and AA-safe at every distance
1262
+ // WITHOUT vanishing -> the river network is a permanent part of the field, not a distance overlay.
1263
+ return line * wetGate * notFrozen;
1264
+ }
1265
+
1266
+ // CANYONS (user-named): deep narrow incised gorges cutting ELEVATED ARID plateaus. Distinct from
1267
+ // rivers (canyons = dry+deep+arid biome; rivers = water+shallow+wet). World-dir-continuous ridged
1268
+ // network like riverMask but NARROWER + a HIGHER threshold (sparser, sharper) and a DIFFERENT
1269
+ // noise phase so canyons and rivers do not coincide. Gated to ELEVATED (h>250m) DRY+WARM regions.
1270
+ // FS-albedo only (no elevation incision) -> zero CLOD/seam risk, AA via pxWorld. Returns line + a
1271
+ // depth term for strata banding. out_depth = how deep into the gorge (0 rim -> 1 floor), for strata.
1272
+ float canyonMask(vec3 worldPos, float h, float temp, float humid, float px, out float depth) {
1273
+ depth = 0.0;
1274
+ // gentle elevation gate: canyons want SOME relief above the lowland, not only rare high
1275
+ // plateaus (h>250 made them NEVER appear -- witnessed canyonFieldFrac 0). Fade in over 60..200m.
1276
+ float elevGate = smoothstep(60.0, 200.0, h);
1277
+ if (elevGate <= 0.0) return 0.0;
1278
+ // ONE FRACTAL (user 2026-06-02: 'canyon field shows two different resolutions, expected the same
1279
+ // fractal'). The FS used its OWN inline loop over worldPos/terrainR (= dir*(1+h/R), elevation-
1280
+ // SHIFTED off the VS sample) -> a different field/position/resolution than the VS carve. Now call
1281
+ // the IDENTICAL canyonRidgeField the VS canyonCarveM uses, at normalize(worldPos) (== the radial
1282
+ // dir), so the FS network coincides EXACTLY with the carved geometry. Same field, no elevation shift.
1283
+ float ridge = canyonRidgeField(normalize(worldPos));
1284
+ float wid = clamp(px * 0.0006, 0.0, 0.03); // narrower than rivers
1285
+ float line = smoothstep(0.875 - wid, 0.94, ridge); // ~river-density threshold so they appear
1286
+ depth = smoothstep(0.875, 0.95, ridge); // deeper toward the channel centre
1287
+ float aridGate = (1.0 - smoothstep(0.40, 0.58, humid)); // DRY (deserts/steppe), opposite of rivers
1288
+ float warmGate = smoothstep(0.38, 0.56, temp); // warm arid plateaus
1289
+ // NO altFade (user: canyon field fading instead of integrated). wid grows with px -> >=1px AA at
1290
+ // every distance without vanishing; the canyon network is permanent, not a distance overlay.
1291
+ return line * aridGate * warmGate * elevGate;
1292
+ }
1293
+ #endif // biomeClassColor/riverMask/canyonMask: DEBUGVIEW-only (called solely from displayMode blocks) -- excluded from render FS cold-compile (FS-2, workflow w4y1bnrqc)
1294
+
1295
+ vec3 terrainAlbedoClimate(float h, float slope, float rockSlope, float temp, float humid, highp vec3 worldPos, float pxWorld) { // highp: worldPos feeds normalize(worldPos)*freq noise UVs (mottle/river/canyon ridge) -- mediump scrambles the lattice up close
1296
+ vec3 c = terrainAlbedo(h, slope, rockSlope, worldPos);
1297
+ if (h < 0.0) {
1298
+ // SEA ICE: near-polar ocean (very cold) freezes to white-blue pack ice. Pure fn of the
1299
+ // anchor temp -> seam-safe; the soft threshold gives an irregular (not hard-zonal) margin.
1300
+ float seaIce = 1.0 - smoothstep(0.12, 0.22, temp);
1301
+ return mix(c, vec3(0.82, 0.88, 0.94), seaIce * 0.9);
1302
+ }
1303
+ // biome weight: full on gentle lowland, fading where rock/snow/steep takes over.
1304
+ float veg = (1.0 - smoothstep(bandEdgesHi.x, bandEdgesHi.y, h)) * (1.0 - smoothstep(slopeRock.x, slopeRock.y, slope));
1305
+ // ELEVATION + LATITUDE BIOME BIAS (user 2026-06-02: 'the hypsometric ramp should influence biome
1306
+ // distribution, pulling biomes out of their anchor areas a bit for better distribution'). On top
1307
+ // of the anchor climate temp/humid, bias the EFFECTIVE temperature DOWN with elevation (lapse rate
1308
+ // ~6.5C/km -> normalize ~ h/4000 over the [0,1] temp scale) and a touch with latitude, so biomes
1309
+ // BAND by height+latitude: high ground -> alpine/tundra/ice, lowland keeps its anchor biome. This
1310
+ // spreads biomes into their physically-expected bands so grass/forest isnt one anchor blob. humid
1311
+ // also rises slightly on windward high ground (orographic) for variety. uBiomeBandBias scales it.
1312
+ float lat = asin(clamp(worldPos.y / max(length(worldPos), 1.0), -1.0, 1.0));
1313
+ float latCool = 0.18 * (abs(lat) / 1.5708); // poles cooler
1314
+ float elevCool = clamp(h / 4500.0, 0.0, 0.55) * uBiomeBandBias; // lapse rate -> alpine bands
1315
+ float tempEff = clamp(temp - elevCool - latCool * uBiomeBandBias, 0.0, 1.0);
1316
+ float humidEff = clamp(humid + clamp(h / 9000.0, 0.0, 0.12) * uBiomeBandBias, 0.0, 1.0);
1317
+ vec3 biome = biomeColor(tempEff, humidEff);
1318
+ // STRONG blend (0.82) so the biome DOMINATES the lowland color -> regions read distinct,
1319
+ // not the faint 0.55 tint that made the whole continent look the same.
1320
+ c = mix(c, biome, veg * 0.82);
1321
+ // INTRA-BIOME VALUE MOTTLE (Real-World Look): real terrain is never one flat color per biome --
1322
+ // soil/moisture/vegetation patchiness mottles albedo. Reuse the snoise3 already evaluated for
1323
+ // cliffRock (same normalize(worldPos)*~1k pattern, no new octave) as a VALUE-only multiplier (NO
1324
+ // hue jitter, low 1200 freq -> does NOT reintroduce the per-pixel swamp moire the pipeline removed).
1325
+ float mot = snoise3(normalize(worldPos) * 120.0); // detail-tex-rockface-canyon-10x: *1200->*120 (~10x larger mottle features, user 2026-06-06)
1326
+ c *= (1.0 + uVariationAmt * mot);
1327
+ // earlier snow/ice on high ground in cold regions.
1328
+ float coldSnow = (1.0 - smoothstep(0.30, 0.75, temp)) * smoothstep(snowEdges.x * 0.5, snowEdges.x, h);
1329
+ c = mix(c, bcSnow, coldSnow * 0.7);
1330
+ // BEACH BAND (user 2026-06-11 'all land at the level of the ocean should be beach -- the grass
1331
+ // must stop and become sand, which continues under the water'): below uBeachTopM the biome/
1332
+ // grass/snow color yields to shore sand (the same bcShore the underwater bed starts from, so the
1333
+ // material is continuous through the waterline). Steep coastal cliffs keep their rock.
1334
+ float beachM = (1.0 - smoothstep(uBeachTopM * 0.6, uBeachTopM, h))
1335
+ * (1.0 - smoothstep(slopeRock.x, slopeRock.y, rockSlope));
1336
+ c = mix(c, bcShore, beachM);
1337
+ // INLAND WATER -- COLOUR GATED BY THE CARVE FIELD (alignment + no-fade fix, browser-2705/2699).
1338
+ // The water colour now keys off the SAME world-dir carve fields that cut the geometry
1339
+ // (lakeCarveM/riverCarveM at this fragment's world dir), NOT an independent h-smoothstep. So the
1340
+ // blue sits EXACTLY in the carved bowl/channel at every LOD (color == depression by construction)
1341
+ // -- this kills both the 'lakes fade on approach' (was gated by h<70 which shifts with relief)
1342
+ // and the 'rivers/lakes out of alignment with their elevation' defects. Climate still gates WHICH
1343
+ // regions get water (wet for lakes/rivers) but no longer the SHAPE (the carve owns the shape).
1344
+ // INLAND WATER + INCISION COLOUR -- UNIFIED: keyed off the carve masks computed ONCE in the VS
1345
+ // and INTERPOLATED here (vLakeWet/vRiverWet/vCanyonDep), NOT re-evaluated per-pixel. This removes
1346
+ // the separate per-pixel ridged-field evaluation that aliased into biome-localized moire (user
1347
+ // 2026-06-01h: 'UV issue only in patches where the biome is like that'). The VS already folded
1348
+ // the climate gates into the masks, so the FS just applies colour.
1349
+ float warmth = smoothstep(0.48, 0.62, temp);
1350
+ // SWAMP: only the WARM + VERY-WET end of a lake basin reads murky olive-teal. The humid gate
1351
+ // (0.66-0.86) was lost in the unify rewrite -> the teal leaked into merely-warm wet land (user:
1352
+ // 'faint teal area, moire'); restored here. The per-pixel mottle snoise3 (freq 900) was the last
1353
+ // per-pixel noise field -> it gave the teal its grain; REMOVED (flat swamp colour, no aliasing).
1354
+ // INLAND WATER ALBEDO IS NOT SET HERE ANYMORE. Lakes and rivers are WATER (user 2026-06-01i:
1355
+ // 'use the same content as the sealevel to draw because its also water'), so they are shaded by
1356
+ // the SAME ocean water branch (fresnel + sun glint + depth tint) post-lighting, gated on the
1357
+ // submerged-depth varying vWaterDepth so the color lands EXACTLY on the flat water plane (lines
1358
+ // up with the carve) instead of being painted over the whole basin including the dry shoulder.
1359
+ // The terrain albedo (rock/biome) stays under the water and shows through where shallow.
1360
+ // CLIFF STRATA BLOCK DELETED (max-speed sweep 2026-06-10): the photo-rock splat owns cliff
1361
+ // appearance; the 2-tap bedding-warp strata fallback is gone (-2 snoise3, ~35 LOC).
1362
+ // DUNES: warm sand on the desert floor, keyed off the interpolated dune-crest mask (vDuneCrest =
1363
+ // crest * duneSand gate from the VS). Crest sun-bleached lighter, trough ochre.
1364
+ vec3 sandLo = vec3(0.78, 0.64, 0.40); // trough ochre sand
1365
+ vec3 sandHi = vec3(0.90, 0.82, 0.62); // sun-bleached crest sand
1366
+ vec3 sandCol = mix(sandLo, sandHi, smoothstep(0.0, 0.6, vDuneCrest));
1367
+ c = mix(c, sandCol, smoothstep(0.0, 0.25, vDuneCrest) * (1.0 - smoothstep(0.30, 0.50, slope)) * 0.9);
1368
+ // PERLIN-EVERYWHERE OVERLAY (user 2026-06-10 'pale + featureless in some areas -- add perlin
1369
+ // everywhere and overlay the other noises'): 3-octave value fbm over ALL materials (biome, rock,
1370
+ // snow, sand) so no area is ever a flat color. World-dir keyed (no camera scroll -- the lesson of
1371
+ // the deleted 2026-06-01 detail texturing), value-only (no hue shift), and each octave fades out
1372
+ // via a pxWorld Nyquist gate before its features go sub-pixel (no orbit speckle, no leopard band).
1373
+ {
1374
+ highp vec3 od = normalize(worldPos);
1375
+ float ov = 0.0, oa = 0.0;
1376
+ float fq = 150.0, am = 1.0; // octaves: ~42km / 8.5km / 1.7km features
1377
+ for (int o = 0; o < 3; o++) {
1378
+ float wl = 40000000.0 / fq; // feature wavelength (m) ~ 2*pi*R / fq
1379
+ float nyq = 1.0 - smoothstep(wl * 0.03, wl * 0.12, pxWorld); // fade before sub-pixel
1380
+ ov += am * nyq * snoise3(od * fq + vec3(float(o) * 7.3));
1381
+ oa += am;
1382
+ fq *= 5.0; am *= 0.6;
1383
+ }
1384
+ // ALBEDO amplitude decoupled from the elevation lever (user 2026-06-11 'normals seem wrongly
1385
+ // calculated, not on the sides of slopes' -- witnessed: at the user-tuned uDetailOverlay 6 the
1386
+ // albedo term was *= 1 +/- 0.54, painting slope-INDEPENDENT dark braids over every material
1387
+ // that read as misplaced shading / rock bands). 0.09 -> 0.02: lever 6 now gives +/-12% value
1388
+ // variation (anti-featureless, as asked) while the +/-180m ELEVATION term keeps the full lever.
1389
+ c *= 1.0 + uDetailOverlay * 0.02 * (ov / max(oa, 1e-3));
1390
+ }
1391
+ return c;
1392
+ }
1393
+
1394
+ // ---- DETAIL MATERIAL (1-2 texel/pixel from anchor biome weights). The coarse biome+ortho
1395
+ // PER-PIXEL DETAIL TEXTURING REMOVED (user 2026-06-01e: "get rid of any detail texturing, we only
1396
+ // want one fractal"). The old FS grain (detailMaterial/detailNormal/grainH + the dnoise lattice)
1397
+ // was a SECOND high-frequency source on top of the VS height fractal -- it caused the moire and the
1398
+ // scroll/jump (its lattice was keyed on the moving camera). Deleted entirely. The single terrain
1399
+ // fractal is the VS world-dir height field; closeup detail comes from the mesh subdividing into a
1400
+ // denser sample of THAT one field, never a per-pixel fake-relief texture.
1401
+
1402
+ // SURFACE PHOTO-TEXTURE triplanar tap: 3 axis-projected samples of one array layer, blended by
1403
+ // the pow-softened |n| weights (same continuous-projection rationale as the rock bump biplanar --
1404
+ // no hard dominant-axis flip). wt = world pos in tile units (fragment-anchored, fp32-precise).
1405
+ vec4 surfTriTap(sampler2DArray sm, highp vec3 wt, vec3 bw, float layer) {
1406
+ return texture(sm, vec3(wt.y, wt.z, layer)) * bw.x
1407
+ + texture(sm, vec3(wt.x, wt.z, layer)) * bw.y
1408
+ + texture(sm, vec3(wt.x, wt.y, layer)) * bw.z;
1409
+ }
1410
+ // WORLD-SPACE triplanar normal perturbation (UDN) -- THE 'math issue causing both' (user 2026-06-11):
1411
+ // the old path triplanar-BLENDED the per-plane RG tangent normals, then applied the blend in the
1412
+ // single radial (ux,uy) frame -- but each projection plane has its own tangent axes, so the blended
1413
+ // RG was rotated arbitrarily vs the frame it was applied in (bumps lit from wrong directions =
1414
+ // statistical darkening + shading off the slope sides), and the apply site SUBTRACTED an
1415
+ // already-negated Sobel normal (double negation = lit from the wrong side outright). Here each
1416
+ // plane's RG perturbs along that plane's own world axes, sign-flipped to push outward, blended by
1417
+ // the same weights -- a world-space delta added to the lit normal. Same 3 taps.
1418
+ vec3 surfTriNrm(sampler2DArray sm, highp vec3 wt, vec3 bw, float layer, vec3 sn) {
1419
+ vec2 px = texture(sm, vec3(wt.y, wt.z, layer)).rg * 2.0 - 1.0; // X plane: in-plane axes (Y,Z)
1420
+ vec2 py = texture(sm, vec3(wt.x, wt.z, layer)).rg * 2.0 - 1.0; // Y plane: in-plane axes (X,Z)
1421
+ vec2 pz = texture(sm, vec3(wt.x, wt.y, layer)).rg * 2.0 - 1.0; // Z plane: in-plane axes (X,Y)
1422
+ return vec3(0.0, px.x, px.y) * (bw.x * sign(sn.x))
1423
+ + vec3(py.x, 0.0, py.y) * (bw.y * sign(sn.y))
1424
+ + vec3(pz.x, pz.y, 0.0) * (bw.z * sign(sn.z));
1425
+ }
1426
+
1427
+ void main() {
1428
+ // PER-FRAGMENT tangent frame from vWorld (the sphere position, C0 across quad edges):
1429
+ // uz = up (radial), ux = normalize(Y x uz), uy = uz x ux. Continuous across adjacent quads
1430
+ // -> no per-quad shading-baseline seam.
1431
+ vec3 uz = normalize(vWorld);
1432
+ // AUDIT FIX 2026-06-06 (audit-tangent-frame-degeneracy): the reference axis was a FIXED vec3(0,1,0),
1433
+ // so cross(Y, uz) -> zero-length (NaN normal) wherever uz is parallel to Y -- i.e. the +Y/-Y cube
1434
+ // faces and the poles, exactly 'wrong normals in the wrong places'. Pick the world axis LEAST
1435
+ // parallel to uz so the cross product is always well-conditioned (|ux| ~ 1) everywhere on the sphere.
1436
+ vec3 refAxis = (abs(uz.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
1437
+ vec3 ux = normalize(cross(refAxis, uz));
1438
+ vec3 uy = cross(uz, ux);
1439
+ // ---- WATER-SURFACE PASS (uIsWater=1, the separate ocean surface at sea level). vH carries the
1440
+ // TRUE seabed elevation under this fragment -> depthM = the water column. Shade as animated
1441
+ // water (Gerstner normal + fresnel sky + sun glint + foam) and ALPHA-blend over the already-
1442
+ // rendered seabed: alpha rises with optical depth (Beer-Lambert) + fresnel/foam, so shallows
1443
+ // show the sand through real transparency and deep basins read opaque navy. Early return --
1444
+ // none of the terrain material/atmosphere work below runs for water fragments.
1445
+ if (uIsWater > 0.5) {
1446
+ if (vH > 1.0) discard; // surface under land: depth test culls it anyway; discard kills shoreline shimmer
1447
+ highp float depthM = max(-vH, 0.0);
1448
+ highp vec3 wOriginW = floor(camWorld / 1024.0) * 1024.0; // snapped anchor (fp32 wave-phase fix, same as the old branch)
1449
+ highp vec2 wpW = vec2(dot(vWorld - wOriginW, ux), dot(vWorld - wOriginW, uy));
1450
+ vec2 slopeW = oceanWaveSlope(wpW, oceanTime);
1451
+ highp float wDistW = length(camWorld - vWorld);
1452
+ slopeW *= clamp(1.0 - wDistW / 4000.0, 0.0, 1.0); // sub-pixel wave fade (anti-alias)
1453
+ vec3 wn = normalize(uz - ux * slopeW.x - uy * slopeW.y);
1454
+ vec3 viewW = normalize(camWorld - vWorld);
1455
+ float fres = 0.02 + 0.98 * pow(clamp(1.0 - max(dot(wn, viewW), 0.0), 0.0, 1.0), 5.0);
1456
+ highp float camAltW = length(camWorld) - terrainR;
1457
+ float specFade = clamp(1.0 - camAltW / 150000.0, 0.0, 1.0); // orbit glint fade (kept)
1458
+ vec3 hlW = normalize(sunDir + viewW);
1459
+ float spec = pow(max(dot(wn, hlW), 0.0), 220.0) * specFade;
1460
+ float ndl = max(dot(wn, sunDir), 0.0);
1461
+ vec3 T = exp(-uOceanK * depthM); // per-channel transmittance
1462
+ float Tavg = (T.r + T.g + T.b) * (1.0 / 3.0);
1463
+ vec3 waterBase = mix(uOceanDeep, uOceanShallow, Tavg);
1464
+ vec3 skyColW = vec3(0.30, 0.42, 0.55);
1465
+ vec3 wcol = mix(waterBase * (0.25 + 0.75 * ndl), skyColW, fres * 0.85)
1466
+ + vec3(1.0, 0.95, 0.85) * spec * ndl;
1467
+ float foamAmt = clamp((length(slopeW) - 0.6) * 1.5, 0.0, 1.0) * oceanFoam;
1468
+ wcol = mix(wcol, vec3(0.9, 0.95, 1.0), foamAmt * (0.3 + 0.7 * ndl));
1469
+ wcol += waterBase * 0.05; // ambient floor (never black)
1470
+ // distance haze: the terrain pass gets the full aerial-perspective march; give the water a
1471
+ // cheap matched fade toward the same sky-haze color so far ocean recedes like far land
1472
+ // (no 8-step march on the largest-area surface -- the perf theme of this pass).
1473
+ float apGW = smoothstep(3000.0, 120000.0, wDistW) * uHazeMul;
1474
+ wcol = mix(wcol, uSkyFill * vec3(0.40, 0.55, 0.78) * 1.6, apGW * 0.7);
1475
+ // day/night + tonemap chain kept consistent with the terrain pass so the two surfaces match.
1476
+ float macroMuW = dot(uz, sunDir);
1477
+ float dayShadeW = mix(uNightFloor, 1.0, smoothstep(-uTermWidth, uTermWidth, macroMuW));
1478
+ vec3 cW = wcol * dayShadeW * uExposure;
1479
+ vec3 mappedW = clamp((cW * (2.51 * cW + 0.03)) / (cW * (2.43 * cW + 0.59) + 0.14), 0.0, 1.0);
1480
+ float lumW = dot(mappedW, vec3(0.2126, 0.7152, 0.0722));
1481
+ mappedW = mix(vec3(lumW), mappedW, uLookSat);
1482
+ mappedW = clamp((mappedW - 0.5) * uLookContrast + 0.5, 0.0, 1.0);
1483
+ // coverage: optically thick water -> opaque; first metres see-through; grazing fresnel and
1484
+ // foam are opaque regardless of depth; feather the exact waterline contact.
1485
+ float alphaW = clamp(1.0 - Tavg, 0.0, 1.0);
1486
+ alphaW = max(alphaW, fres * 0.9);
1487
+ alphaW = max(alphaW, foamAmt * 0.8);
1488
+ // SHALLOW-SURFACE FLOOR (2026-06-11, found by the coast witness after the shelf landed):
1489
+ // the continental shelf keeps water metres-deep for kilometres, where Beer-Lambert alpha
1490
+ // ~0 and nadir fresnel ~0 made the whole shoreline INVISIBLE (coverage 0 at a bisected
1491
+ // waterline pose). Real shallow water still shows its surface (sky reflection + ripple):
1492
+ // floor the opacity once genuinely submerged so shorelines read as water.
1493
+ alphaW = max(alphaW, 0.30 * smoothstep(0.3, 4.0, depthM));
1494
+ alphaW *= smoothstep(0.0, 1.5, depthM);
1495
+ fragColor = vec4(pow(mappedW, vec3(1.0 / 2.2)), alphaW);
1496
+ return;
1497
+ }
1498
+ // W8 SOLE FS LIT NORMAL (P1 data-first, P9 all-distances): vNrm is the per-vertex analytic central-
1499
+ // difference normal of the FULL composeHeight field, computed in the VS at a FIXED STABLE world step.
1500
+ // Full landform relief (broadShapeM fine octaves + carves) at ALL distances, NO per-pixel jitter, NO
1501
+ // fade, NO band-limit. Replaces the cross(dFdx,dFdy) geometric normal (it jumped per-pixel across the
1502
+ // bumpy displaced vWorld = the 'completely messed up' scramble) and the bandFadeN macro-only fade
1503
+ // (which made fine relief go missing). It is a continuous function of dir0 only -- adjacent fragments
1504
+ // get the linearly-interpolated VS value, so it is smooth by construction (no dFdx/dFdy, no fwidth).
1505
+ // uFlatNormal kept as a cheap escape hatch: pure sphere normal (uz) for A/B isolation.
1506
+ vec3 n = (uFlatNormal > 0.5) ? uz : normalize(vNrm);
1507
+
1508
+ // GPU-TIMER VS-ISOLATION: a cheap measurement frame short-circuits the whole per-pixel shade
1509
+ // here (uses vNrmPV + vWorld so the VS outputs are not dead-code-eliminated) -> the timed cheap
1510
+ // frame is VS+raster only; (full - cheap) attributes the per-pixel FS cost. No effect when 0.
1511
+ if (uFsCheap > 0.5) { fragColor = vec4(n * 0.5 + 0.5, 1.0); return; }
1512
+
1513
+
1514
+ // DIAGNOSTIC displayModes (1,5,6,7,8,9,10,11,12) are gated behind _DEBUGVIEW_ so they are
1515
+ // ONLY compiled into the lazily-built DEBUG program (gl-render.js), NOT the hot render program.
1516
+ // They added 7132 chars (25%) of translated HLSL to the render FS (browser-1590) -- pure dead
1517
+ // weight on the lit path, and the heavy ones pull in riverMask/canyonMask/biomeClassColor which
1518
+ // ANGLE then drops from the render program once unreferenced. Cheap albedo modes 2/4 stay live
1519
+ // below (one fragColor=albedo each). The render program forces displayMode 0 (lit) effectively.
1520
+ #ifdef _DEBUGVIEW_
1521
+ // displayMode 1 (Normals) MOVED below the splat (user 2026-06-11 'in the normals view we dont
1522
+ // see the texture normals'): this early return showed the pre-splat geometric normal, so the
1523
+ // displacement-derived texture normals (texDn) were invisible in the debug view. The handler
1524
+ // now lives after nLit/texDn assembly and shows the FINAL lit normal.
1525
+ // DIAG displayMode 5: land/sea map -- RED where vH>0 (land), BLUE where vH<0 (ocean).
1526
+ // Unambiguous test of the elevation sign distribution the render actually samples.
1527
+ if (displayMode == 5) { fragColor = (vH < 0.0) ? vec4(0.1,0.2,0.9,1.0) : vec4(0.9,0.3,0.1,1.0); return; }
1528
+ // DIAG displayMode 6: encode signed vH into RGB. R = vH/8000 (positive), B =
1529
+ // -vH/8000 (negative), G = |vH|/8000. Lets a readback recover the actual elevation
1530
+ // magnitude the render samples (independent of the biome ramp / readTile artifact).
1531
+ // ELEVATION displayMode 6: HYPSOMETRIC ramp (no hard clip / washed-out tops, user 2026-06-02).
1532
+ // Sea = blue (deeper -> darker); land ramps green->tan->brown->grey->white by height over a 0..9km
1533
+ // scale, each band a smoothstep so peaks read as distinct grey/white rock, NOT a saturated white
1534
+ // blob. >9km (shouldn't occur after the massif tame) just stays at the snow-rock top, no blow-out.
1535
+ // ELEVATION displayMode 6: a SCIENTIFIC (non-albedo) data ramp so it reads as an elevation MAP,
1536
+ // not naturalistic terrain colour (user 2026-06-02: 'elevation view looks weird like albedo').
1537
+ // Sea = dark->mid blue by depth; land = turbo-style cyan->green->yellow->orange->red->white by
1538
+ // height (8.5km full scale). Clearly a heatmap, no hard clip.
1539
+ if (displayMode == 6) {
1540
+ if (vH < 0.0) {
1541
+ float dep = clamp(-vH / 6000.0, 0.0, 1.0);
1542
+ fragColor = vec4(mix(vec3(0.10,0.55,0.85), vec3(0.0,0.05,0.30), dep), 1.0); return;
1543
+ }
1544
+ // full scale 11km so even an 8km peak reads RED/MAGENTA, not saturated white; white reserved
1545
+ // for the very rare >10.5km tip -> NO broad white-clipped areas (user 2026-06-02: 'fully white').
1546
+ float t = clamp(vH / 11000.0, 0.0, 1.0);
1547
+ vec3 ec = mix(vec3(0.0,0.80,0.85), vec3(0.10,0.85,0.20), smoothstep(0.0, 0.18, t)); // cyan->green
1548
+ ec = mix(ec, vec3(0.95,0.95,0.10), smoothstep(0.18, 0.38, t)); // -> yellow
1549
+ ec = mix(ec, vec3(0.95,0.50,0.05), smoothstep(0.38, 0.58, t)); // -> orange
1550
+ ec = mix(ec, vec3(0.80,0.10,0.10), smoothstep(0.58, 0.78, t)); // -> red
1551
+ ec = mix(ec, vec3(0.55,0.10,0.40), smoothstep(0.78, 0.93, t)); // -> magenta (very high)
1552
+ ec = mix(ec, vec3(1.0,1.0,1.0), smoothstep(0.95, 1.0, t)); // -> white only at the tip
1553
+ fragColor = vec4(ec, 1.0); return;
1554
+ }
1555
+ #endif // _DEBUGVIEW_ (modes 1,5,6)
1556
+
1557
+ float slope = 1.0 - max(0.0, dot(n, uz)); // 0 = flat, ->1 = steep
1558
+ // Rock classification keys on TRUE slope = 1-dot(n,uz) from the sole band-limited geometric normal.
1559
+ // gslope is the same quantity (kept as a name for the AO/detail-normal terms below).
1560
+ float gslope = slope;
1561
+ // ROCK-FACE BREAKUP NOISE DELETED (user 2026-06-11, 3-way isolation witness: the braided
1562
+ // km-scale rock/grass interfingering at this noise's exact 2.5km wavelength WAS the 'rocky
1563
+ // patches' -- fake texture ridges the real terrain (normals-view smooth) does not have, hence
1564
+ // 'normals dont match elevation'. The breakup compensated for the over-steep pre-14ad9b8 peak
1565
+ // stack; with sane slopes the plain gate is correct.)
1566
+ float rockSlope = clamp(slope, 0.0, 1.0);
1567
+ // world metres spanned by one screen pixel (MOVED up from ~line 1321 so the close-up signal fade is
1568
+ // available to the rock-detail block below). nearFade=1 at the deck (a crag is many px) -> 0 at
1569
+ // altitude (the crag goes sub-pixel) so the entire close-up rock signal fades to zero from orbit and
1570
+ // the macro path is unchanged there (contrast-on-approach-safe, no orbit shimmer).
1571
+ float pxWorld = max(length(fwidth(vWorld)), 0.001);
1572
+ // FADE CEILING 40->180m (live-deck witness 2026-06-05, browser-31: pxWorld is ~5m DIRECTLY below the
1573
+ // eye but climbs PAST 40m across the rest of a wide-FOV near-ground frame, so at 40m the close-up rock
1574
+ // detail + microAO + chromatic colorVar switched OFF across most of the visible deck -- the realism
1575
+ // never reached the FPS range the user actually flies (tens-to-hundreds of m/px). 180m keeps the whole
1576
+ // near-ground swath engaged while still fully fading by orbit (8m floor unchanged -> no orbit shimmer).
1577
+ float nearFade = 1.0 - smoothstep(8.0, 180.0, pxWorld); // macro crease-AO distance fade (vAO only)
1578
+ // PROCEDURAL ROCK DETAIL-NORMAL DELETED (max-speed sweep 2026-06-10): the photo-rock
1579
+ // displacement normal owns rock micro-relief; the 3-tap snoise3D biplanar bump is gone.
1580
+ // microSlope/microCurv keep their macro defaults for the material/AO consumers below.
1581
+ vec3 nLit = n;
1582
+ float microSlope = rockSlope;
1583
+ float microCurv = 0.0;
1584
+ // base biome albedo from the coherent height/slope ramp, biased by anchor CLIMATE
1585
+ // (latitude-driven temp + humidity) so lowland color sorts by biome (lush/arid/cold).
1586
+ vec4 climate = vClimate; // (seaBias, elevAmp, temp=.z, humid=.w) -- INTERPOLATED from the VS,
1587
+ // NOT a per-pixel HPF texture read (that read showed the HPF texel
1588
+ // grid as UV lines/moire up close). Smooth biome transitions now.
1589
+ // (pxWorld is computed up near the slope block now -- moved so nearFade can fade the rock detail.)
1590
+ vec3 albedo = terrainAlbedoClimate(vH, slope, microSlope, climate.z, climate.w, vWorld, pxWorld);
1591
+ // CURVATURE MICRO-AO (close-up realism, workflow w5gywvug1): the concave creases between rock crags
1592
+ // (microCurv < 0) darken -- the contrast that reads as 3D rugged relief instead of flat lit clay.
1593
+ // Gated by microSlope (flat ground untouched); band-limited by the crag bump it derives from; scaled by the
1594
+ // live uAoAmt lever so it stays terraform-tunable. negCurv = how concave the crease is (0 on convex).
1595
+ float negCurv = max(0.0, -microCurv);
1596
+ float aoLever = (uAoAmt > 0.0 ? uAoAmt : 1.0);
1597
+ // LOW-CONTRAST crease AO (user 2026-06-06 flecked-rock fix): the crease darkening was up to 60% (the
1598
+ // dark half of the fleck speckle). Soften to <=30% so creases read as gentle shading, not hard specks.
1599
+ // microCurv already comes from the band-limited (rockBumpGate) crag bump, so this is present at all
1600
+ // distances and fades out naturally as the bump does -- no separate nearFade gate.
1601
+ float microAO = 1.0 - clamp(negCurv * 3.0, 0.0, 0.30) * smoothstep(0.04, 0.2, microSlope) * aoLever;
1602
+ albedo *= microAO;
1603
+ // CLOSE-UP CHROMA VARIATION DELETED (max-speed sweep 2026-06-10): the photo textures carry
1604
+ // near-field color variation now (-2 snoise3/pixel).
1605
+ // ---- SURFACE PHOTO-TEXTURE SPLAT (user 2026-06-10): triplanar grass/rock/sand/snow color +
1606
+ // displacement-derived normal, blended by the SAME gates the procedural materials use (slope ->
1607
+ // rock, snowline+cold -> snow, hot-dry climate / dunes -> sand, else grass) so texture and macro
1608
+ // material always agree. Distance-faded by pxWorld; mips + macro biome color carry the far field.
1609
+ // MIP-OUT, not fade-out (user 2026-06-11 'instead of fading out the textures, mip them out'):
1610
+ // the 15-20km camera-distance fade is GONE -- the splat persists at every distance and the
1611
+ // hardware mip chain carries the far field, where the texture averages to its mean color
1612
+ // (= the macro shade, by the layer-mean shade-match; rock's raw-photo mean = __surfRockMean).
1613
+ vec3 texDn = vec3(0.0); // photo-texture WORLD-SPACE normal perturbation, applied after uReliefShade
1614
+ // SPLAT RUNS UNDERWATER TOO (user 2026-06-11 'continue under water as sand and rock'): the old
1615
+ // vH > -2 gate cut the photo textures at the waterline, leaving the seabed flat-colored.
1616
+ if (uHasSurfTex > 0.5 && uTexMix > 0.001) {
1617
+ // material weights from the existing gates (climate = vClimate: z=temp, w=humid)
1618
+ // SAND GATE = THE MACRO DESERT GATE (user 2026-06-11 'all the grassy areas need to have the
1619
+ // grass texture'): the old humid<0.42 band splatted SAND across savanna/steppe/meadow-edge
1620
+ // regions whose MACRO color is green/gold vegetation (biomeColor only reaches DESERT at
1621
+ // smoothstep(0.60,0.85,1-humid)) -- green ground wore the sand photo. One source of truth:
1622
+ // reuse the exact macro desert ramp, so the sand texture appears precisely where the macro
1623
+ // paints desert; savanna/steppe splat GRASS tinted dry-gold by the layer shade-match.
1624
+ float dryHot = smoothstep(0.60, 0.85, 1.0 - climate.w) * smoothstep(0.42, 0.62, climate.z);
1625
+ // BEACH (user 2026-06-10 'the beaches should be sand'): the shore profile eases over the
1626
+ // first ~60m of elevation, so low gentle land near sea level splats sand regardless of climate.
1627
+ float beach = (1.0 - smoothstep(10.0, 80.0, vH)) * (1.0 - smoothstep(0.22, 0.42, slope));
1628
+ // SAND REGIONS SUPPRESS ROCK (user 2026-06-10 'rock being used instead of sand'): in
1629
+ // deserts/dunes/beaches sand drapes moderate slopes; rock only wins on genuinely steep faces
1630
+ // there (gate shifted toward 0.5-0.7 inside sand regions instead of slopeRock 0.28-0.55).
1631
+ float sandRegion = clamp(max(max(dryHot, beach), smoothstep(0.0, 0.25, vDuneCrest)), 0.0, 1.0);
1632
+ // SPLAT ROCK GATE DECOUPLED from the macro slopeRock (user 2026-06-11 'a lot of grass turning
1633
+ // into rocky patches again'): the user-calibrated global soft blend slopeRock [-0.6,1] puts
1634
+ // ~37% ROCK LAYER weight on perfectly flat ground, and the displacement-sharpened top-2
1635
+ // crossfade then flips whole displacement blobs to the rock PHOTO on grassland. The macro
1636
+ // color blend keeps the calibrated tone; the rock TEXTURE layer needs real slope: floor the
1637
+ // splat gate at 0.18 so flat/gentle land splats grass, rock starts on genuine slopes.
1638
+ float srLo = max(slopeRock.x, 0.18), srHi = max(slopeRock.y, srLo + 0.2);
1639
+ float wRock = smoothstep(mix(srLo, 0.50, sandRegion), mix(srHi, 0.70, sandRegion), rockSlope);
1640
+ float snowHi = smoothstep(snowEdges.x, snowEdges.y, vH);
1641
+ float snowCold = (1.0 - smoothstep(0.30, 0.75, climate.z)) * smoothstep(snowEdges.x * 0.5, snowEdges.x, vH);
1642
+ // POLAR/ICE-BIOME SNOW (user 2026-06-10 'put the snow texture on the snow'): the ICE biome
1643
+ // whitens cold lowland (biomeColor gate 1-smoothstep(0.10,0.18,tempEff)) at ANY elevation, but
1644
+ // wSnow was elevation-gated only -- polar snowfields were splatting the GRASS layer. Match the
1645
+ // effective-temperature ice gate (raw temp minus the elevation/latitude lapse the biome uses).
1646
+ float lat2 = asin(clamp(vWorld.y / max(length(vWorld), 1.0), -1.0, 1.0));
1647
+ float tempEff2 = clamp(climate.z - clamp(vH / 4500.0, 0.0, 0.55) * uBiomeBandBias
1648
+ - 0.18 * (abs(lat2) / 1.5708) * uBiomeBandBias, 0.0, 1.0);
1649
+ float iceClimate = (1.0 - smoothstep(0.10, 0.20, tempEff2)) * step(0.0, vH); // no snow layer on the seabed
1650
+ float wSnow = clamp(snowHi + 0.7 * snowCold + iceClimate, 0.0, 1.0) * (1.0 - 0.6 * wRock);
1651
+ float wSand = sandRegion * (1.0 - wRock) * (1.0 - wSnow) * (1.0 - smoothstep(0.40, 0.60, slope));
1652
+ float wGrass = max(1.0 - wRock - wSnow - wSand, 0.0);
1653
+ vec4 w4 = vec4(wGrass, wRock, wSand, wSnow); // layers 0..3 = grass,rock,sand,snow
1654
+ // BEACH BAND + UNDERWATER in one mask (user 2026-06-11): sand owns everything below the
1655
+ // beach ceiling uBeachTopM -- grass/snow weight folds into sand continuously through h=0 to
1656
+ // the seabed, so no vegetation can ever splat at or below the waterline (structural).
1657
+ float uwM = 1.0 - smoothstep(uBeachTopM * 0.6, uBeachTopM, vH);
1658
+ w4.z += (w4.x + w4.w) * uwM; w4.x *= 1.0 - uwM; w4.w *= 1.0 - uwM;
1659
+ w4 /= (w4.x + w4.y + w4.z + w4.w + 1e-4);
1660
+ // top-2 layer pick: 4-way blending would cost 24 taps; the gates are spatially near-exclusive,
1661
+ // so the two heaviest layers + a displacement-sharpened crossfade carry every transition.
1662
+ float lA = 0.0, wA = w4.x, lB = 0.0, wB = -1.0;
1663
+ if (w4.y > wA) { lB = lA; wB = wA; lA = 1.0; wA = w4.y; } else if (w4.y > wB) { lB = 1.0; wB = w4.y; }
1664
+ if (w4.z > wA) { lB = lA; wB = wA; lA = 2.0; wA = w4.z; } else if (w4.z > wB) { lB = 2.0; wB = w4.z; }
1665
+ if (w4.w > wA) { lB = lA; wB = wA; lA = 3.0; wA = w4.w; } else if (w4.w > wB) { lB = 3.0; wB = w4.w; }
1666
+ // tile-unit coords, FRAGMENT-ANCHORED snap (the rockO fp32 lesson): raw vWorld/tile loses
1667
+ // fp32 precision = visible UV stairs. Snap step = 1024 tiles exactly, so wt jumps by an
1668
+ // integer tile count across a snap boundary = identical wrapped sample (REPEAT), no phase
1669
+ // reset, camera-independent.
1670
+ highp float snapM = uTexTileM * 1024.0;
1671
+ highp vec3 wt = (vWorld - floor(vWorld / snapM) * snapM) / uTexTileM;
1672
+ // DOMAIN WARP anti-repetition (user 2026-06-10 'distort the textures so they dont look
1673
+ // repeated' + 'distort more, waves a bit large'): 2-octave world-dir warp -- ~11km waves at
1674
+ // +/-0.6 tile plus ~2.8km waves at +/-0.3 tile -- so repeats both shift AND shear locally;
1675
+ // no straight tile lattice survives. World-dir keyed -> seam-safe, camera-independent.
1676
+ // uTexWarp (__texWarp) scales the whole warp live.
1677
+ // VS-COMPUTED WARP, APPLIED EXACTLY ONCE (2026-06-12): vTexWarp carries the 3-octave halved-
1678
+ // frequency warp from the vertex shader (-9 snoise3/pixel). This is the ONLY place the warp
1679
+ // touches texture coordinates; every layer tap (albedo A/B, normal A/B, all four materials)
1680
+ // samples through this shared wt, so the warp cannot layer or double-apply.
1681
+ wt += vTexWarp * uTexWarp;
1682
+ vec3 tw = abs(n); tw = tw * tw; tw /= (tw.x + tw.y + tw.z + 1e-4);
1683
+ vec4 albA = surfTriTap(uSurfAlb, wt, tw, lA);
1684
+ vec3 nrmA = surfTriNrm(uSurfNrm, wt, tw, lA, n);
1685
+ float bAB = clamp(wA / max(wA + wB, 1e-4), 0.0, 1.0);
1686
+ vec4 texAlb = albA; vec3 texNrm = nrmA;
1687
+ if (wB > 0.02) { // second layer only where a real transition exists (saves 6 taps elsewhere)
1688
+ vec4 albB = surfTriTap(uSurfAlb, wt, tw, lB);
1689
+ vec3 nrmB = surfTriNrm(uSurfNrm, wt, tw, lB, n);
1690
+ // displacement-sharpened transition -- WEIGHT-DOMINANT (user 2026-06-10 'large patches
1691
+ // of rock texture in mountains, not slope-keyed'): the old (hA-hB)*4 let the texture's
1692
+ // 2.4km displacement features decide the material outright wherever the gate weights sat
1693
+ // mid-range, so whole displacement blobs flipped to rock. The slope/snow/climate weight
1694
+ // now dominates (x3) and displacement only crisps the edge (x0.8) within the true
1695
+ // transition band -- material placement is the gates', texture only shapes the seam.
1696
+ // displacement term 0.8 -> 0.3 (user 2026-06-11 'still see bowls of rock texture'):
1697
+ // 0.8 still let the displacement photo's bowl-shaped blobs flip whole patches to rock
1698
+ // inside the transition band; 0.3 only feathers the seam edge.
1699
+ float bSharp = clamp((bAB * 2.0 - 1.0) * 3.0 + (albA.a - albB.a) * 0.3 + 0.5, 0.0, 1.0);
1700
+ texAlb = mix(albB, albA, bSharp);
1701
+ texNrm = mix(nrmB, nrmA, bSharp);
1702
+ }
1703
+ float k = uTexMix;
1704
+ // macro-tinted detail (user 2026-06-10 'the textured patch must be tinted to the same shade
1705
+ // as the spot its replacing'): the texture contributes STRUCTURE + relative chroma only,
1706
+ // luminance-normalized onto the macro biome/climate color, so the splat never shifts the
1707
+ // shade of the ground it covers. uTexPhoto (default 0) can blend raw photo color back in.
1708
+ vec3 texC = texAlb.rgb;
1709
+ // LAYER-MEAN shade-match (user 2026-06-11 'dont see grass/snow textures' + 'terrain gets
1710
+ // darker'): dividing by the PER-PIXEL luminance cancelled all texture structure, and the
1711
+ // raw-photo blend that replaced it shifted the shade (the photos are darker than the macro).
1712
+ // Dividing by the layer's MEAN luminance (loader-computed uSurfMeanL) keeps every per-pixel
1713
+ // deviation visible while the patch average lands exactly on the macro shade.
1714
+ float mA = uSurfMeanL[int(lA + 0.5)];
1715
+ float texL = wB > 0.02 ? mix(uSurfMeanL[int(lB + 0.5)], mA, bAB) : mA;
1716
+ vec3 detail = texC * (albedo / max(texL, 0.02));
1717
+ // ROCK SHOWS THE TRUE PHOTO (user 2026-06-10 'we still see the original rock texture --
1718
+ // replace completely'): tinting rock to the macro shade just reproduced the old grey/tan,
1719
+ // so the rock layer takes the raw photo color; grass/sand/snow stay shade-matched.
1720
+ // raw-photo rock is NEAR-FIELD only (user 2026-06-11 'elevation and color mismatching'):
1721
+ // with mip-out the raw photo (mean lum .18, dark grey) was carrying to every distance, so
1722
+ // far mountains went dark instead of the macro tan. Fade photoF out as the texture detail
1723
+ // goes sub-pixel (pxWorld 60->240m); the far field returns to the shade-matched mip average.
1724
+ float nearTex = 1.0 - smoothstep(60.0, 240.0, pxWorld);
1725
+ float photoF = max(uTexPhoto, max(smoothstep(0.25, 0.6, w4.y) * 0.5, uTexPhotoNear) * nearTex);
1726
+ // MATERIAL IDENTITY (user 2026-06-12): the photo's OWN hue at the MACRO's luminance -- the
1727
+ // ground reads unambiguously as its material (grass green / sand tan / rock grey) without the
1728
+ // raw photo's darkness ('terrain gets darker' lesson) and without the macro's biome-brown
1729
+ // repaint ('neither grass nor sand' defect). Layer-mean normalized like `detail`.
1730
+ vec3 texIdent = texC * (dot(albedo, vec3(0.2126, 0.7152, 0.0722)) / max(texL, 0.02));
1731
+ albedo = clamp(mix(albedo, mix(detail, texIdent, photoF), k), 0.0, 1.0);
1732
+ // displacement-normal relief: WORLD-SPACE UDN perturbation from surfTriNrm (each projection
1733
+ // plane's tangent axes, not the radial frame). Amplitude capped low (scramble lesson d262b5e);
1734
+ // applied AFTER the uReliefShade exaggeration below so the exaggeration never amplifies it.
1735
+ texDn = texNrm * (uTexNrmK * k);
1736
+ }
1737
+ #ifdef _DEBUGVIEW_
1738
+ // DIAG displayMode 7: raw river field -> blue where the river line fires, grey ridge field
1739
+ // elsewhere. Lets a witness SEE the drainage network + camera together to tune frequency
1740
+ // (not guess from screenshots). Pure diagnostic; no effect on the lit path.
1741
+ if (displayMode == 7) {
1742
+ float rv = riverMask(vWorld, vH, climate.z, climate.w, pxWorld);
1743
+ fragColor = vec4(mix(vec3(0.15), vec3(0.1,0.4,0.9), rv), 1.0); return;
1744
+ }
1745
+ // DIAG displayMode 8: RIVER GATING -- the ACTUAL integrated river the geometry+lit path uses (user
1746
+ // 2026-06-02: 'river gating is empty'). The old version re-evaluated riverMask per-pixel AND a dead
1747
+ // lake-suppression gated on humid>0.62 (most land is ~0.39) so it read ~0 = empty. Now it shows the
1748
+ // VS-integrated varyings directly: BLUE = vRiverWet (the flowing-water line the geometry carved +
1749
+ // the lit path composites), GREEN = vLakeWet (lake water), so it matches what actually renders. The
1750
+ // raw per-pixel network is still in mode 7.
1751
+ if (displayMode == 8) {
1752
+ vec3 col = vec3(0.12);
1753
+ col = mix(col, vec3(0.10, 0.45, 0.95), clamp(vRiverWet, 0.0, 1.0)); // river line (integrated)
1754
+ col = mix(col, vec3(0.15, 0.85, 0.55), clamp(vLakeWet, 0.0, 1.0)); // lake water
1755
+ fragColor = vec4(col, 1.0); return;
1756
+ }
1757
+ // DIAG displayMode 9: DISCRETE BIOME MAP -> each fragment flat-colored by its classified
1758
+ // biome (ocean/ice/tundra/desert/savanna/rainforest/taiga/forest/meadow). Lets a witness
1759
+ // COUNT contiguous biome regions + confirm logical placement (deserts in subtropics, ice at
1760
+ // poles, rainforest at equator), instead of eyeballing the blended lit palette.
1761
+ if (displayMode == 9) {
1762
+ fragColor = vec4(biomeClassColor(climate.z, climate.w, vH), 1.0); return;
1763
+ }
1764
+ // DIAG displayMode 10: CANYON field -> red where the gorge network fires (arid elevated only),
1765
+ // grey ridge field elsewhere. Lets a witness SEE the canyon network density independent of
1766
+ // landing the nadir exactly on a thin gorge line.
1767
+ if (displayMode == 10) {
1768
+ // ONE FRACTAL: canyonMask now samples the SAME canyonRidgeField as the VS carve (unified), so
1769
+ // the per-pixel network coincides with the geometry -- show the single full-res field (no more
1770
+ // overlaying canyonMask + vCanyonDep at two sample rates, which read as 'two resolutions').
1771
+ float cd; float cv = canyonMask(vWorld, vH, climate.z, climate.w, pxWorld, cd);
1772
+ // grey background -> orange canyon line by mask, deepening toward the gorge floor (cd).
1773
+ vec3 col = mix(vec3(0.15), mix(vec3(0.75,0.40,0.15), vec3(0.9,0.15,0.05), cd), cv);
1774
+ fragColor = vec4(col, 1.0); return;
1775
+ }
1776
+ // DIAG displayMode 11: CLIFF validation view -> RED = cliff/escarpment riser faces (vCliffFace),
1777
+ // GREEN = canyon walls (vCanyonDep on steep slope), so the user can SEE where cliffs are placed
1778
+ // independent of lighting/strata. Grey elsewhere. The witness for the cliff terrace placement.
1779
+ if (displayMode == 11) {
1780
+ float cliffR = vCliffFace;
1781
+ float canyonG = vCanyonDep * smoothstep(0.30, 0.55, slope);
1782
+ fragColor = vec4(mix(vec3(0.12), vec3(1.0, 0.2, 0.1), cliffR)
1783
+ + vec3(0.0, 0.8, 0.2) * canyonG, 1.0); return;
1784
+ }
1785
+ // DIAG displayMode 12: PATCHES -> each LOD LEVEL a DISTINCT colour (user: validate the dense LOD
1786
+ // is always centered under the camera). Hue cycles by level via a golden-ratio rotation so any two
1787
+ // adjacent levels are far apart in hue; the finest (highest-level) patches under the camera read as
1788
+ // the hottest band, and the rings must be CONCENTRIC + CENTERED on the camera nadir. A thin dark
1789
+ // cell-grid (vGrid) overlays so individual quads are visible. Pure per-instance vLevel -> no field.
1790
+ if (displayMode == 12) {
1791
+ float h = fract(vLevel * 0.61803398875); // golden-ratio hue per level (well-spread)
1792
+ // HSV->RGB at full sat/val for h, mid-V so text/grid reads.
1793
+ vec3 rgb = clamp(abs(fract(h + vec3(0.0, 0.6666667, 0.3333333)) * 6.0 - 3.0) - 1.0, 0.0, 1.0);
1794
+ vec3 col = rgb * 0.85 + 0.1;
1795
+ // quad-cell grid overlay (same fwidth-AA lines as wireframe) so patch boundaries are crisp.
1796
+ vec2 g = vGrid * 24.0; vec2 gf = abs(fract(g) - 0.5); vec2 gw = fwidth(g) * 1.2;
1797
+ float line = 1.0 - min(smoothstep(0.0, gw.x, gf.x), smoothstep(0.0, gw.y, gf.y));
1798
+ col = mix(col, vec3(0.0), clamp(line, 0.0, 1.0) * 0.55);
1799
+ fragColor = vec4(col, 1.0); return;
1800
+ }
1801
+ #endif // _DEBUGVIEW_ (modes 7-12)
1802
+ // Material = the biome height/slope/climate ramp (provably 0 edges). The ortho atlas + per-pixel
1803
+ // detail texturing are both REMOVED (GPU one-fractal): closeup detail comes from the mesh
1804
+ // subdividing into a denser sample of the single VS height fractal, never a tile sample or grain.
1805
+ if (displayMode == 4) { fragColor = vec4(albedo, 1.0); return; } // biome ramp
1806
+ if (displayMode == 2) { fragColor = vec4(albedo, 1.0); return; }
1807
+
1808
+ // ---- Bruneton-style atmospheric lighting (ported analytic single-scatter) ----
1809
+ // Map the terrain point + camera into atmosphere space (km, surface=ATM_BOTTOM).
1810
+ highp vec3 pAtm = atmPos(vWorld, terrainR); // W7: km-scale (~6360) atmosphere coords -> highp for the AP segment precision
1811
+ highp vec3 camAtm = atmPos(camWorld, terrainR);
1812
+ // GENTLE-SLOPE RELIEF SHADING (user 2026-06-10 'even gentle slopes shaded so terrain elevation
1813
+ // is obvious'): exaggerate the tangential tilt of the LIT normal only -- the material gates keep
1814
+ // the true geometric n, so placement is unchanged; lighting contrast on rolling ground increases.
1815
+ if (vH > -2.0 && uReliefShade > 1.0) nLit = normalize(uz + (nLit - uz) * uReliefShade);
1816
+ // photo-texture detail normal at its calibrated amplitude, OUTSIDE the relief exaggeration.
1817
+ // ADDED in world space (the old `- ux*dn.x - uy*dn.y` subtracted an already-negated Sobel
1818
+ // normal in a frame the triplanar RG was never expressed in -- the fundamental both-at-once
1819
+ // math bug: bumps lit from wrong directions everywhere the splat is active).
1820
+ nLit = normalize(nLit + texDn);
1821
+ vec3 nAtm = nLit; // relief-exaggerated normal for lighting (macro n still drives material)
1822
+ #ifdef _DEBUGVIEW_
1823
+ // Normals view shows the FINAL lit normal (geometry + relief exaggeration + texture detail
1824
+ // normal) -- the normal the lighting actually uses, texture normals included.
1825
+ if (displayMode == 1) { fragColor = vec4(nAtm * 0.5 + 0.5, 1.0); return; }
1826
+ #endif
1827
+
1828
+ // ANIMATED OCEAN BRANCH REMOVED (user 2026-06-11 'the ocean should be a separate surface'):
1829
+ // sub-sea fragments are the real SEABED now (sand/rock, lit as land below); the animated water
1830
+ // shading moved to the uIsWater=1 water-surface pass (early return at the top of main), alpha-
1831
+ // blended over this seabed. Sea ice still keys the seabed albedo near the poles.
1832
+
1833
+ vec3 skyIrr;
1834
+ vec3 sunIrr = atm_sunSkyIrradiance(pAtm, nAtm, sunDir, skyIrr);
1835
+ // The Rayleigh sky irradiance is strongly blue; at full strength it tints the lit
1836
+ // CONTINENTS blue-grey from orbit (witnessed: lit mean [83,95,112] vs warm albedo
1837
+ // [90,83,74] at 2000km -- the blue is the sky-irradiance term, NOT aerial inscatter).
1838
+ // Partially DESATURATE skyIrr toward its luminance so it still brightens shadowed/
1839
+ // ambient regions without washing the land's true color blue. Land albedo dominates;
1840
+ // a modest blue ambient remains (physically the sky does add some blue fill).
1841
+ float skyL = dot(skyIrr, vec3(0.2126, 0.7152, 0.0722));
1842
+ vec3 skyIrrBalanced = mix(vec3(skyL), skyIrr, 0.35); // 0 = grey, 1 = full blue sky
1843
+ // RELIEF RECOVERY (Real-World Look): the bright flat sky fill washed N.sun ridge/valley contrast
1844
+ // to ~flat. CUT the fill (uSkyFill, was implicit 1.0) + cool-tint it so shadows read sky-blue, so
1845
+ // the direct sun becomes the dominant key and relief self-shading actually shows.
1846
+ skyIrrBalanced *= uSkyFill * vec3(0.85, 0.92, 1.10);
1847
+ // Lambert BRDF (albedo/PI) under sun + (balanced) sky irradiance. Keep an ambient
1848
+ // FLOOR so night/shadow faces are never pure black (PRD lit-terrain-dark-black).
1849
+ // CURVATURE / SLOPE AMBIENT-OCCLUSION (canyon floors + cliff bases): incised/concave fragments
1850
+ // see less sky, so darken the AMBIENT (sky + floor) there for contact-shadow depth. Keyed on the
1851
+ // interpolated canyon gorge depth (vCanyonDep ->1 in the gorge core) so floors darken while rims
1852
+ // stay bright. Direct SUN is NOT occluded (avoids double-darkening already-shaded faces). uAoAmt
1853
+ // lever. Pure fn of the carve varying -> no extra sampling, seam-safe.
1854
+ // OBJECT-SPACE AO (2026-06-05): augment the gorge-depth term with a cheap analytic sky-occlusion
1855
+ // estimate so cliff BASES and steep concave faces -- not just canyon floors -- pick up contact
1856
+ // shadow. A true N-sweep horizon-angle integral over the fractal (karim.naaji.fr/lsao) is the
1857
+ // research-best but per-pixel fractal sweeps would re-add the close-up VS/FS cost the pipeline is
1858
+ // sensitive to; instead approximate the sky-view factor from data already in hand: (a) the gorge
1859
+ // depth varying (deep incision -> walls occlude sky), and (b) surface STEEPNESS (a near-vertical
1860
+ // face sees only a hemisphere's edge of sky -> ~half occlusion at slope->1). Both darken AMBIENT
1861
+ // only (never the direct sun, avoiding double-shade). uAoAmt lever. Pure fn of varyings+normal ->
1862
+ // no extra sampling, seam-safe, zero FPS cost vs the old single-term version.
1863
+ float aoAmt = (uAoAmt > 0.0 ? uAoAmt : 1.0);
1864
+ float gorgeAO = 0.0; // canyon AO decoupled (user 2026-06-10: canyons impose ELEVATION only, no material/AO keying)
1865
+ float slopeAO = smoothstep(0.45, 0.95, slope) * 0.40; // steep concave faces see less sky (0.30->0.40, deeper valleys)
1866
+ float cliffAO = 1.0 - min(gorgeAO + slopeAO, 0.75) * aoAmt; // cap so faces never go black
1867
+ // STRONGER NORMAL-DRIVEN SHADING (user 2026-06-03: 'normals arent affecting the lit view properly').
1868
+ // The lit relief was too subtle (witnessed litON SD 6.4 vs flat 5.9 = only ~8% contrast) because
1869
+ // sunIrr (the N.sun direct term) was diluted by the flat sky ambient + the 0.06 albedo floor. Cut
1870
+ // the ambient floor (0.06->0.03) so shaded slopes go genuinely darker, and BOOST the directional
1871
+ // sun term (x1.6) so the N.sun slope contrast reads clearly. The (1/PI) Lambert norm + ACES tonemap
1872
+ // keep it from blowing out; a small floor still prevents pure-black shadow faces.
1873
+ // sunIrr boost 1.6 -> 1.25 + ambient floor 0.03 -> 0.04 (user 2026-06-03 screenshot: the 1.6 boost
1874
+ // BLEW OUT high/bright rock to flat white, losing relief detail). 1.25 keeps the slope contrast
1875
+ // strong (N.sun still drives the shading) without clipping the highlights to white on lit rock.
1876
+ // AMBIENT FLOOR lifted 0.025->0.05 (live-deck witness mut-1780681995401: with the analytic+fs normal ON
1877
+ // HALF the deck frame crushed to NEAR-BLACK -- mean 151->91, n898k->434k -- so relief read as black
1878
+ // HOLES not legible rugged texture). A genuine shadowed rock face is dark grey-blue (skylight), never
1879
+ // pure black; the higher floor keeps micro-relief readable as texture while the direct sun still drives
1880
+ // the ridge/valley key contrast. Tied to cliffAO so true contact-shadow concavities still darken.
1881
+ // PER-VERTEX SHADING (DEFECT 2): fold the interpolated vShadeAO crease/micro-relief occlusion into
1882
+ // the ambient term so geometry reads relief even where the FS detail-normal is faint. nearFade-gated
1883
+ // (full at the deck, ->1 i.e. no effect at orbit) so the macro/orbit look is unchanged. Combines
1884
+ // multiplicatively with cliffAO -- both are sky-occlusion factors that should compose.
1885
+ // W5: vShadeAO (per-vertex, from the deleted VS gradient) RECOMPUTED here from the Sobel slope (user
1886
+ // 2026-06-07 'recompute AO from Sobel'). creaseAO = slope*0.45 (steep landform -> valley/crease
1887
+ // occlusion); microAO from the fine per-pixel gslope. uVertexAO lever + the 0.45 floor preserved.
1888
+ float fsShadeAO = clamp(1.0 - (slope * 0.45 + gslope * 0.35) * uVertexAO, 0.45, 1.0);
1889
+ // FADE-IN FIX (user 'a layer fades in at close distance'): vAO was mix(1.0, fsShadeAO, nearFade) where
1890
+ // nearFade rises 0->1 as pxWorld shrinks 180->8m on approach -> the Sobel crease-AO darkening animated
1891
+ // IN as the camera neared = the visible 'layer fading in'. fsShadeAO is a per-pixel SOBEL-slope quantity
1892
+ // (not sub-pixel noise) so it does NOT alias and needs no distance gate -- present at all distances, no
1893
+ // pop. This is the nearFade gate the P5 unification missed on the shading-AO path. nearFade now unused.
1894
+ float vAO = fsShadeAO; // crease-AO present at all distances (P9, no fade-in pop)
1895
+ float skyAO = cliffAO * vAO;
1896
+ // SHADOW-FACE FILL (user 2026-06-09: 'shadows are still full black'). On a day-side face pointing AWAY
1897
+ // from the sun, sunIrr->0 so ONLY this floor lights it. The old floor (albedo*0.05*cliffAO*vAO) was
1898
+ // crushed to ~0 on dark-albedo rock AND on steep/concave faces (cliffAO*vAO -> 0 exactly where shadows
1899
+ // are) -> black shadows. FIX: (a) an ALBEDO-INDEPENDENT additive sky term so even dark rock never goes
1900
+ // black, (b) DROP the cliffAO/vAO multipliers from the floor (they belong on the sky KEY, not the
1901
+ // guaranteed floor), (c) lift the albedo-tinted part to 0.22. Result: shadowed faces settle to a dim
1902
+ // cool-grey, never black; the direct sun still drives the lit/shadow contrast above this floor.
1903
+ // The 0.22 flat lift (no AO) flooded shadowed ANGLED faces to a uniform cool-grey, washing out the
1904
+ // relief (user 2026-06-10: 'angled faces turned to rock/grey'). Restore directional shading: scale the
1905
+ // albedo-tinted part by a SOFTENED sky exposure (mix(0.45,1.0,skyAO) -- never crushes to 0 like the old
1906
+ // cliffAO*vAO did, but lets faces that see less sky read darker = the relief returns), and lower the lift
1907
+ // 0.22->0.14. The albedo-INDEPENDENT flat sky floor stays as the never-black guarantee (the 9cfa643 win).
1908
+ vec3 ambientFloor = albedo * (0.14 * mix(0.45, 1.0, skyAO)) + vec3(0.020, 0.026, 0.038);
1909
+ // sky AO no longer SQUARED (was cliffAO*cliffAO): the squared sky term over-crushed shadowed micro-
1910
+ // relief to black at the deck. Single cliffAO still darkens concavities for depth, but shadowed
1911
+ // faces retain sky fill so the analytic normal reads as rugged texture, not black blotches.
1912
+ // (the old vH<-2 ocean bypass is gone -- the seabed is land now, lit like land; the water
1913
+ // surface is its own pass.)
1914
+ vec3 lit = albedo * (sunIrr * 1.25 + skyIrrBalanced * skyAO) * (1.0/ATM_PI) + ambientFloor;
1915
+
1916
+ // AERIAL PERSPECTIVE (analytic single-scatter, re-added 2026-06-05 for the space->ground depth
1917
+ // cue -- the single biggest missing realism term for the seamless descent; ref Hillaire-2020,
1918
+ // LUT-free inline form). The OLD removed version was a flat 15km horizon haze band the user
1919
+ // disliked; this is physically-grounded instead: integrate Rayleigh+Mie in-scatter along the
1920
+ // EXACT camera->fragment segment and attenuate the lit color by the segment transmittance, so
1921
+ // the haze is PROPORTIONAL to how much atmosphere the view ray actually crossed. DISTANCE-GATED
1922
+ // hard at both ends: ~zero within a few km (FPS-ground stays crisp, no over-haze the user hated)
1923
+ // and ramping in only across tens-of-km+ (the depth cue that makes distant ridges recede and the
1924
+ // space->ground transition read as real). N=8 march (cheaper than the sky pass's 16; the surface
1925
+ // term tolerates it), single draw, reuses atm_* -> zero assets.
1926
+ vec3 color = lit;
1927
+ {
1928
+ highp vec3 camA = atmPos(camWorld, terrainR); // W7: km-scale -> highp
1929
+ highp vec3 segKm = pAtm - camA; // W7: camera->fragment in km (cancellation of two km-scale points)
1930
+ highp float dKm = length(segKm);
1931
+ // gate: 0 below ~3km path, ramping to full by ~120km. keeps ground crisp, distance hazed.
1932
+ float apGate = smoothstep(3.0, 120.0, dKm);
1933
+ if (apGate > 0.002) {
1934
+ vec3 vRay = segKm / max(dKm, 1e-4);
1935
+ const int APN = 8;
1936
+ float apdt = dKm / float(APN);
1937
+ vec3 inscatR = vec3(0.0), inscatM = vec3(0.0);
1938
+ float odR = 0.0, odM = 0.0; // optical depth camera->sample
1939
+ for (int i = 0; i < APN; i++) {
1940
+ highp vec3 p = camA + vRay * (apdt * (float(i) + 0.5)); // W7: km-scale march point -> highp
1941
+ highp float pr = length(p);
1942
+ float dR = atm_rayleighDensity(pr) * apdt;
1943
+ float dM = atm_mieDensity(pr) * apdt;
1944
+ odR += dR; odM += dM;
1945
+ vec3 tView = exp(-(ATM_RAYLEIGH * odR + ATM_MIE_EXT * odM));
1946
+ vec3 tSun = atm_transmittanceToSun(p, sunDir);
1947
+ vec3 t = tView * tSun;
1948
+ inscatR += t * dR;
1949
+ inscatM += t * dM;
1950
+ }
1951
+ float nu = dot(vRay, sunDir);
1952
+ vec3 apTrans = exp(-(ATM_RAYLEIGH * odR + ATM_MIE_EXT * odM));
1953
+ vec3 apInscat = ATM_SOLAR_IRRADIANCE * (
1954
+ inscatR * ATM_RAYLEIGH * atm_rayleighPhase(nu) +
1955
+ inscatM * ATM_MIE * atm_miePhase(ATM_MIE_G, nu));
1956
+ // HORIZON HAZE FLOOR (Real-World Look): real aerial perspective fades distant terrain toward
1957
+ // a pale blue-grey sky, NEVER to black. When apTrans collapses (long limb path) the physical
1958
+ // single-scatter inscatter can dip in low-phase directions, leaving a black silhouette band.
1959
+ // Floor the fill with a multiple-scatter-style skylight ambient (1-apTrans) so the far ridge
1960
+ // dissolves into haze instead of going dark. Tied to uSkyFill so it's terraformable.
1961
+ vec3 skyHaze = uSkyFill * vec3(0.40, 0.55, 0.78) * (1.0 - apTrans);
1962
+ apInscat = max(apInscat, skyHaze);
1963
+ // attenuate the surface by transmittance and add the in-scatter, both faded by the gate
1964
+ // so the near field is untouched (apGate~0 -> mix back to the un-hazed lit color).
1965
+ vec3 hazed = lit * apTrans + apInscat;
1966
+ // TERMINATOR SUNSET REDDENING (Real-World Look): add a warm Rayleigh-out rim where the sun
1967
+ // GRAZES the limb (strictly gated on 1-|N.sun| near 0 so it never bleeds onto the full day
1968
+ // side). Rides the existing AP inscatter -- zero extra march. uTerminatorGlow lever.
1969
+ // peak at the terminator (N.sun~0), fall off toward both day-center and night-center, and
1970
+ // SQUARE it so the warm band is tight at the day/night line instead of a fat ring round the
1971
+ // whole limb. Warm amber (not pure red) so it reads as sunset haze, not a bruise.
1972
+ float gz = 1.0 - abs(dot(normalize(vWorld), sunDir));
1973
+ float graze = smoothstep(0.55, 1.0, gz); graze *= graze;
1974
+ // only on the LIT side of the terminator (mu>0) -- past the line the surface is night and
1975
+ // additive amber on near-black extinct land reads as a scorched maroon rim. Day-gate kills it.
1976
+ float termDay = smoothstep(-0.02, 0.18, dot(normalize(vWorld), sunDir));
1977
+ hazed += uTerminatorGlow * graze * termDay * vec3(1.0, 0.55, 0.34) * apGate;
1978
+ color = mix(lit, hazed, apGate * uHazeMul); // uHazeMul lever (2026-06-10 'pale hazy': full-strength haze milked the midground)
1979
+ }
1980
+ }
1981
+ // RIVERS post-lighting (witnessed browser-2115/2118: the river-blue in ALBEDO is multiplied
1982
+ // by the warm sun irradiance (sunIrr.b is low) so land rivers lose their blue in the lit
1983
+ // result -- 15k blue albedo px -> 0 lit px, even with the sun HIGH (sunDotUp 0.85). Ocean
1984
+ // sidesteps this by bypassing the irradiance multiply (vH<0 branch). So rivers, like open
1985
+ // water, must be composited AFTER lighting: blend the post-lit color toward a sun-scaled
1986
+ // water-blue on the river line. ndlSun keeps the river shaded by the sun so it is not flat.
1987
+ // INLAND WATER = REAL WATER, SHADED LIKE THE OCEAN (user 2026-06-01i: 'it should use the same
1988
+ // content as the sealevel to draw because its also water'). Where vWaterDepth>0 (the flat water
1989
+ // plane sits above the carved floor -> genuinely submerged) we run the SAME water model as the
1990
+ // ocean branch: fresnel sky reflection + sun glint + shallow->deep depth tint, composited AFTER
1991
+ // lighting (the warm sun-irradiance multiply would otherwise kill the blue, the reason the ocean
1992
+ // bypasses it via vH<0). Gating on vWaterDepth (NOT the whole carve mask) makes the water LINE UP
1993
+ // with the flat carved surface -- the graded erosion banks above the waterline stay dry land.
1994
+ if (vH >= 0.0 && vWaterDepth > 0.0) {
1995
+ // submergence 0..1 over ~0..40m -> shoreline (thin water, terrain shows through) to deep.
1996
+ float sub = clamp(vWaterDepth / 40.0, 0.0, 1.0);
1997
+ // wave-perturbed water normal (small inland ripple), same tangent frame as the ground.
1998
+ highp vec3 wOriginL = floor(camWorld / 1024.0) * 1024.0; // W7: ~6.4e6 m snapped anchor -> highp
1999
+ highp vec2 wpL = vec2(dot(vWorld - wOriginL, ux), dot(vWorld - wOriginL, uy)); // W7: highp camera-relative wave coord
2000
+ vec2 slopeL = oceanWaveSlope(wpL, oceanTime) * 0.5; // calmer than open ocean
2001
+ highp float wDistL = length(camWorld - vWorld); // W7: camera->fragment cancellation -> highp
2002
+ slopeL *= clamp(1.0 - wDistL / 4000.0, 0.0, 1.0); // fade ripple at distance (anti-alias)
2003
+ vec3 wnL = normalize(uz - ux * slopeL.x - uy * slopeL.y);
2004
+ vec3 viewL = normalize(camWorld - vWorld);
2005
+ float f0L = 0.02;
2006
+ float fresL = f0L + (1.0 - f0L) * pow(clamp(1.0 - max(dot(wnL, viewL), 0.0), 0.0, 1.0), 5.0);
2007
+ vec3 hlL = normalize(sunDir + viewL);
2008
+ float specL = pow(max(dot(wnL, hlL), 0.0), 220.0);
2009
+ float ndlL = max(dot(wnL, sunDir), 0.0);
2010
+ // depth tint: a touch greener than the ocean (freshwater/sediment), shallow -> deep.
2011
+ vec3 shallowL = vec3(0.10, 0.30, 0.36);
2012
+ vec3 deepL = vec3(0.02, 0.10, 0.20);
2013
+ // congruent with the ocean: per-channel Beer-Lambert by true submerged depth (freshwater
2014
+ // slightly murkier), so lakes/rivers and the ocean read as one water system.
2015
+ vec3 TL = exp(-uOceanK * vWaterDepth * 1.6);
2016
+ vec3 waterBaseL = mix(deepL, shallowL, TL);
2017
+ vec3 skyColL = vec3(0.30, 0.42, 0.55);
2018
+ vec3 waterLitL = waterBaseL * (0.25 + 0.75 * ndlL);
2019
+ vec3 inlandWater = mix(waterLitL, skyColL, fresL * 0.85) + vec3(1.0, 0.95, 0.85) * specL * ndlL;
2020
+ inlandWater += waterBaseL * 0.05; // ambient floor (never black)
2021
+ // blend over the lit ground: full water in deep parts, terrain shows through at the shoreline.
2022
+ float waterCover = sub; // 0 at waterline -> 1 deep
2023
+ color = mix(color, inlandWater, waterCover);
2024
+ }
2025
+ // WIREFRAME OVERLAY (uWireframe=1): draw the per-quad mesh-cell grid lines so the LOD
2026
+ // tessellation is visible. Uses the parametric vGrid coord (0..1 per tile, GRID cells) with an
2027
+ // fwidth-based anti-aliased line at each cell boundary -> crisp wireframe at any distance,
2028
+ // independent of the triangle topology (WebGL2 has no glPolygonMode). Drawn pre-tonemap so the
2029
+ // lines tonemap with the surface.
2030
+ if (uWireframe > 0.5) {
2031
+ vec2 g = vGrid * 16.0; // GRID cells per tile edge (gridMeshSize 16; was 24, FPS lever)
2032
+ vec2 gf = abs(fract(g) - 0.5); // distance to nearest cell line in cell units
2033
+ vec2 gw = fwidth(g) * 1.2; // line half-width in cell units (AA)
2034
+ float line = 1.0 - min(smoothstep(0.0, gw.x, gf.x), smoothstep(0.0, gw.y, gf.y));
2035
+ color = mix(color, vec3(0.05, 1.0, 0.4), clamp(line, 0.0, 1.0) * 0.85);
2036
+ }
2037
+ // MACRO GLOBE SHADING (user 2026-06-03: at the DEFAULT ORBIT view the planet read as a FLAT MAP --
2038
+ // a luma scan across the disc was ~uniform, no terminator/limb darkening, because the OCEAN bypasses
2039
+ // the sun term and the high exposure pushed land into the tonemap's flat bright shoulder). Apply the
2040
+ // SPHERE sun angle to EVERY fragment (land + ocean) so the globe reads 3D: bright at the sub-solar
2041
+ // point, falling to dark across the day side into the terminator. dayShade = smoothstep over the
2042
+ // surface-normal . sun, with a small ambient floor (0.10) so the night/terminator isn't pure black.
2043
+ float macroMu = dot(normalize(vWorld), sunDir);
2044
+ // SOFT FLOORED TERMINATOR (Real-World Look): a wider, smoother twilight band (uTermWidth) with a
2045
+ // night floor (uNightFloor) so framed night longitudes keep faint detail instead of crushing to
2046
+ // black (defect #4). The old hard 0.10..-0.12,0.55 band was too steep + too dark.
2047
+ float dayShade = mix(uNightFloor, 1.0, smoothstep(-uTermWidth, uTermWidth, macroMu));
2048
+ // LIMB DARKENING: grazing-view fragments (the disc EDGE from orbit) darken, giving the globe its
2049
+ // rounded 3D form instead of a flat disc. viewGraze = surface-normal . view-dir, ->1 facing the
2050
+ // camera (disc centre), ->0 at the limb. Only bites near the limb so it does not dim the main face.
2051
+ vec3 viewDir = normalize(camWorld - vWorld);
2052
+ float viewGraze = max(dot(normalize(vWorld), viewDir), 0.0);
2053
+ float limb = 0.45 + 0.55 * smoothstep(0.0, 0.45, viewGraze); // 0.45 at the limb -> 1.0 facing camera
2054
+ // NIGHT / SHADOW FILL (user 2026-06-09: 'we want night lights in the SHADOW areas, not city lights'):
2055
+ // a UNIFORM dim fill that lifts the unlit hemisphere out of pure black -- a cool ambient night light,
2056
+ // no clusters, no warm dots, no land gate (it lifts every dark fragment, ocean + land). uNightLights
2057
+ // is the live intensity dial (0 = off, back to near-black night). The day-lit side (dayShade->1) is
2058
+ // unaffected because the fill is weighted by (1-dayShade). Raised from the old earthshine 0.012/0.018/
2059
+ // 0.032 (still read as black) to a clearly-visible dim blue so framed night terrain stays legible.
2060
+ vec3 nightFill = vec3(0.06, 0.075, 0.11) * uNightLights;
2061
+ vec3 color2 = (color * dayShade + nightFill * (1.0 - dayShade)) * limb;
2062
+ // HDR -> SDR: ACES tonemap. Exposure 1.7 -> 1.25 so the N.sun + dayShade gradient SPREADS across the
2063
+ // tonemap's linear range instead of clipping flat to the bright shoulder (the orbit-flatness root).
2064
+ vec3 c = color2 * uExposure; // exposure 1.25->uExposure(1.0): stop the bright ACES-shoulder wash
2065
+ vec3 mapped = clamp((c*(2.51*c+0.03))/(c*(2.43*c+0.59)+0.14), 0.0, 1.0);
2066
+ // POST-ACES LOOK (Real-World Look): restore saturation + contrast DELIBERATELY in display-linear so
2067
+ // the result is darker-but-saturated, not bright-but-pastel. Pull saturation up (uLookSat) + an
2068
+ // S-curve contrast about mid-grey (uLookContrast), then gamma. Live-tunable.
2069
+ float lum = dot(mapped, vec3(0.2126, 0.7152, 0.0722));
2070
+ mapped = mix(vec3(lum), mapped, uLookSat);
2071
+ mapped = clamp((mapped - 0.5) * uLookContrast + 0.5, 0.0, 1.0);
2072
+ fragColor = vec4(pow(mapped, vec3(1.0/2.2)), 1.0);
2073
+ }
2074
+ #endif
2075
+
2076
+ // ---- HEIGHT PROBE (collision): compute the EXACT rendered terrain height for one world dir,
2077
+ // reusing the same hpfSample + broadShapeM the mesh VS uses, so the free-fly collision floor
2078
+ // can never diverge from the rendered surface (user-chosen: read px from the GPU, not a CPU
2079
+ // mirror). Paired with a 1-point VS in proland-gl-render.js; writes height (metres) to R32F.
2080
+ #ifdef _PROBE_
2081
+ uniform vec3 probeDir; // world direction under the camera (normalized)
2082
+ out vec4 probeOut;
2083
+ void main(){
2084
+ vec3 dir0 = normalize(probeDir);
2085
+ // THC-Normal W1/W7: the collision height is now the SAME composeHeight() the geometry bake uses,
2086
+ // so sampleGroundM returns EXACTLY the rendered surface -- no parallel mirror to drift (was a
2087
+ // hand-copied carve sequence that OMITTED vtxDisplace = the camera-stops-short gap). Reconstruct
2088
+ // the face-local metres from the probe world dir (max-axis cube projection -> warped face metres)
2089
+ // so vtxDisplace samples the identical micro-relief field the VS does. tileM = a deck-leaf size so
2090
+ // the Nyquist fade keeps every octave the close-up geometry shows (collision == visible surface).
2091
+ int face; vec2 uv; hpfFaceUV(dir0, face, uv);
2092
+ // hpfFaceUV INVERTS the cube projection that built dir0, so (uv*2-1)*defRadius IS ALREADY the
2093
+ // warped face-local metres the mesh VS passes to composeHeight (terrain.glsl:667: faceWarp(...)).
2094
+ // The old line wrapped this in faceWarp() a SECOND time = a double-warp -> the probe sampled a
2095
+ // SHIFTED vtxDisplace noise-lattice position than the VS = a per-probe micro-relief height delta
2096
+ // (collision-elev-facelocal-parity, workflow wb4syopmo). Drop the extra warp: probe lattice == VS lattice.
2097
+ highp vec2 faceLocal = (uv * 2.0 - 1.0) * defRadius; // W7: warped face-local metres ~6.4e6 -> highp (matches VS:667)
2098
+ // tileM is INERT in vtxDisplace (terrain.glsl:495-501 has no tileM term; PURE per-patch-step fix) so
2099
+ // composeHeight is bit-identical for any tileM -> the hardcoded value below is NOT a divergence (verified).
2100
+ highp float h = composeHeight(dir0, faceLocal, 64.0); // W7: metres (tileM inert; collision == rendered surface)
2101
+ // RAW height out (2026-06-11): the old smoothstep(-60,60) flat-ocean clamp made the probe a
2102
+ // CLAMPED GAUGE -- sampleGroundM could never report real bathymetry (witnessed: a 2000-dir
2103
+ // sweep found 'no ocean below -100m' on a planet with km-deep basins), silently corrupting
2104
+ // every depth-based witness. Collision still floats on the water: BOTH movement consumers
2105
+ // clamp at use (planet.html Math.max(0, sampleGroundM(...)) at the move-step and ground-track
2106
+ // sites), which is where a USE-specific policy belongs -- the instrument now reports truth.
2107
+ probeOut = vec4(h, 0.0, 0.0, 1.0);
2108
+ }
2109
+ #endif