mapspinner 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapspinner",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "WebGL2 Earth-scale terrain rendering SDK for interactive globe applications",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/planet.html CHANGED
@@ -477,9 +477,9 @@ function frameLoop(){
477
477
  // take the existing CPU-mirror fallback. Below 20km (where collision is load-bearing) keep GPU-exact.
478
478
  const _altRoughKm = _camLenMv - RADIUS;
479
479
  const _gpuGM = (_altRoughKm < 20.0 && _orchMv && _orchMv.render && _orchMv.render.sampleGroundM) ? _orchMv.render.sampleGroundM(_camDirMv) : null;
480
- const _surfM = (_gpuGM != null && isFinite(_gpuGM)) ? Math.max(0, _gpuGM)
481
- : Math.max(0, jsBroadShape(cam.pos[0],cam.pos[1],cam.pos[2]));
482
- const altSurfElev = Math.max(0, Math.min(_surfM, 9500.0)) / WEBGL2_TERRAIN_R_M;
480
+ const _surfM = (_gpuGM != null && isFinite(_gpuGM)) ? _gpuGM
481
+ : jsBroadShape(cam.pos[0],cam.pos[1],cam.pos[2]);
482
+ const altSurfElev = Math.min(_surfM, 9500.0) / WEBGL2_TERRAIN_R_M;
483
483
  const altKmAbove = Math.max(1e-3, (Math.hypot(...cam.pos) - RADIUS*(1.0 + altSurfElev)));
484
484
  // CAMERA MOVE-STEP. NO ELEVATION SLOWDOWN (user 2026-06-05: 'get rid of the camera slowdown code').
485
485
  // The step scales with altitude-above-surface ONLY so the scene still moves at orbit (a constant step
@@ -522,14 +522,12 @@ function frameLoop(){
522
522
  // a height above an inflated reference -> the eye was really ground+margin+2m, not 2m.) The mirror
523
523
  // fallback keeps a small +3m only because the CPU jsBroadShape can under-predict vs the GPU.
524
524
  if (gpuM != null && isFinite(gpuM)) {
525
- renderedM = Math.max(0, gpuM); // GPU-exact rendered height; no margin
525
+ renderedM = gpuM; // GPU-exact rendered height (may be negative = seabed)
526
526
  } else {
527
- renderedM = Math.max(0, jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2])) + 3.0; // mirror fallback (CPU under-predicts)
527
+ renderedM = jsBroadShape(cam.pos[0], cam.pos[1], cam.pos[2]) + 3.0; // mirror fallback (CPU under-predicts)
528
528
  }
529
- // cap = composeHeight's true ceiling CMAX=15000 (terrain.glsl:448), NOT 11000: the GPU probe
530
- // renderedM is honest up to 15km, but re-capping at 11000 before minR let the camera floor sit
531
- // BELOW visible 11-15km peaks -> tunnel through the rendered geometry (collision-elev-margin, wb4syopmo).
532
- const surfElev = Math.max(0.0, Math.min(renderedM, 15000.0)) / WEBGL2_TERRAIN_R_M; // fraction of R (match CMAX)
529
+ // cap to composeHeight's CMAX=15000; allow negative for seabed
530
+ const surfElev = Math.min(renderedM, 15000.0) / WEBGL2_TERRAIN_R_M;
533
531
  const eyeHeight = 2.0 / WEBGL2_M_PER_UNIT; // EXACTLY 2.0 m above ground (km-units = 2m / m-per-unit)
534
532
  const minR = RADIUS * (1.0 + surfElev) + eyeHeight;
535
533
  if (r < minR) { const s = minR/r; cam.pos[0]*=s; cam.pos[1]*=s; cam.pos[2]*=s; }
@@ -537,7 +535,7 @@ function frameLoop(){
537
535
  if (r > maxR) { const s = maxR/r; cam.pos[0]*=s; cam.pos[1]*=s; cam.pos[2]*=s; }
538
536
 
539
537
  // height of the eye above the local ground, in metres (for the HUD readout).
540
- const heightAboveGroundM = (Math.hypot(...cam.pos) - RADIUS*(1.0 + Math.max(0,surfElev))) * 1000;
538
+ const heightAboveGroundM = (Math.hypot(...cam.pos) - RADIUS*(1.0 + surfElev)) * 1000;
541
539
  window.__altM = heightAboveGroundM;
542
540
 
543
541
  // The camera (autopilot + free-fly + clamp) has now been fully updated this frame,
package/src/gl-render.js CHANGED
@@ -870,6 +870,10 @@ export async function initMapspinnerRender(gl, opts = {}) {
870
870
  gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1); // iOffset
871
871
  gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1); // iFace
872
872
  gl.uniform1f(U('uIsWater'), 0.0);
873
+ // UNDERWATER DETECTION: camera below sea level enables underwater shading + water surface
874
+ // rendering from below. Set before the terrain draw so the FS can apply underwater fog.
875
+ const _uw = camDist < R - 2.0;
876
+ gl.uniform1f(U('uUnderwater'), _uw ? 1.0 : 0.0);
873
877
  gl.drawElementsInstanced(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0, n);
874
878
  // SEPARATE WATER SURFACE (user 2026-06-11): second instanced draw with uIsWater=1 -- the VS
875
879
  // pins the mesh to sea level, the FS shades animated water and alpha-blends it over the
@@ -901,9 +905,14 @@ export async function initMapspinnerRender(gl, opts = {}) {
901
905
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(wl), gl.DYNAMIC_DRAW);
902
906
  gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, 0); gl.vertexAttribDivisor(1, 1);
903
907
  gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 4 * 4); gl.vertexAttribDivisor(2, 1);
904
- gl.enable(gl.BLEND);
905
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
906
- gl.depthMask(false);
908
+ if (_uw) {
909
+ gl.disable(gl.BLEND);
910
+ gl.depthMask(true);
911
+ } else {
912
+ gl.enable(gl.BLEND);
913
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
914
+ gl.depthMask(false);
915
+ }
907
916
  gl.uniform1f(U('uIsWater'), 1.0);
908
917
  gl.drawElementsInstanced(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0, wn);
909
918
  gl.uniform1f(U('uIsWater'), 0.0);
@@ -206,6 +206,7 @@ uniform float uVertexAO; // per-vertex shading/AO strength lev
206
206
  // blended over the seabed). One program + a uniform flag = no second cold compile, no duplicated
207
207
  // per-frame uniform churn, and the branch is uniform-coherent (free on the GPU).
208
208
  uniform float uIsWater; // 0 = terrain pass, 1 = water-surface pass
209
+ uniform float uUnderwater; // 0 = camera above water, 1 = camera below sea level
209
210
  uniform float uBeachTopM; // beach ceiling (m): below this, grass/snow yield to sand (window.__beachTop; 30 default)
210
211
  uniform float uHiFreqCut; // hi-freq elevation-noise attenuation, applied to ALL hi-freq sources:
211
212
  // broadShapeM/MD fine octaves (o>=6) + vtxDisplace micro-relief
@@ -1452,6 +1453,34 @@ void main() {
1452
1453
  // none of the terrain material/atmosphere work below runs for water fragments.
1453
1454
  if (uIsWater > 0.5) {
1454
1455
  if (vH > 1.0) discard; // surface under land: depth test culls it anyway; discard kills shoreline shimmer
1456
+ // UNDERWATER WATER SURFACE (camera below sea level): render the surface from below as
1457
+ // opaque deep blue with Gerstner wave animation. No sky reflection, no Fresnel (TIR from
1458
+ // below = mostly opaque deep water), no Beer-Lambert (there is no seabed above the camera).
1459
+ if (uUnderwater > 0.5) {
1460
+ highp vec3 wOriginW = floor(camWorld / 1024.0) * 1024.0;
1461
+ highp vec2 wpW = vec2(dot(vWorld - wOriginW, ux), dot(vWorld - wOriginW, uy));
1462
+ vec2 slopeW = oceanWaveSlope(wpW, oceanTime);
1463
+ highp float wDistW = length(camWorld - vWorld);
1464
+ slopeW *= clamp(1.0 - wDistW / 4000.0, 0.0, 1.0);
1465
+ vec3 wn = normalize(uz - ux * slopeW.x - uy * slopeW.y);
1466
+ vec3 viewW = normalize(camWorld - vWorld);
1467
+ float ndl = max(dot(wn, sunDir), 0.0);
1468
+ // Sunlight attenuated through the water column; deep blue ambient
1469
+ float depthAtten = exp(-max(0.0, terrainR - length(camWorld)) * 0.0005);
1470
+ vec3 sunUnder = vec3(1.0, 0.6, 0.3) * depthAtten * ndl;
1471
+ vec3 deepBlue = vec3(0.005, 0.06, 0.18);
1472
+ vec3 waveBright = vec3(0.0, 0.02, 0.06) * length(slopeW);
1473
+ vec3 wcol = deepBlue + sunUnder * 0.4 + waveBright;
1474
+ float macroMuW = dot(uz, sunDir);
1475
+ float dayShadeW = mix(uNightFloor, 1.0, smoothstep(-uTermWidth, uTermWidth, macroMuW));
1476
+ vec3 cW = wcol * dayShadeW * uExposure;
1477
+ vec3 mappedW = clamp((cW * (2.51 * cW + 0.03)) / (cW * (2.43 * cW + 0.59) + 0.14), 0.0, 1.0);
1478
+ float lumW = dot(mappedW, vec3(0.2126, 0.7152, 0.0722));
1479
+ mappedW = mix(vec3(lumW), mappedW, uLookSat);
1480
+ mappedW = clamp((mappedW - 0.5) * uLookContrast + 0.5, 0.0, 1.0);
1481
+ fragColor = vec4(pow(mappedW, vec3(1.0 / 2.2)), 1.0);
1482
+ return;
1483
+ }
1455
1484
  highp float depthM = max(-vH, 0.0);
1456
1485
  highp vec3 wOriginW = floor(camWorld / 1024.0) * 1024.0; // snapped anchor (fp32 wave-phase fix, same as the old branch)
1457
1486
  highp vec2 wpW = vec2(dot(vWorld - wOriginW, ux), dot(vWorld - wOriginW, uy));
@@ -1840,7 +1869,18 @@ void main() {
1840
1869
  // blended over this seabed. Sea ice still keys the seabed albedo near the poles.
1841
1870
 
1842
1871
  vec3 skyIrr;
1843
- vec3 sunIrr = atm_sunSkyIrradiance(pAtm, nAtm, sunDir, skyIrr);
1872
+ vec3 sunIrr;
1873
+ if (uUnderwater > 0.5) {
1874
+ // Underwater terrain (seabed): sun attenuated through water column, blue ambient, no
1875
+ // atmospheric Rayleigh/Mie scattering. Sun colour shifts toward green-blue with depth.
1876
+ float depth = max(0.0, terrainR - length(camWorld));
1877
+ float atten = exp(-depth * 0.0004);
1878
+ float ndl = max(dot(nAtm, sunDir), 0.0);
1879
+ sunIrr = vec3(1.0, 0.65, 0.35) * atten * ndl * 0.7;
1880
+ skyIrr = vec3(0.02, 0.07, 0.18);
1881
+ } else {
1882
+ sunIrr = atm_sunSkyIrradiance(pAtm, nAtm, sunDir, skyIrr);
1883
+ }
1844
1884
  // The Rayleigh sky irradiance is strongly blue; at full strength it tints the lit
1845
1885
  // CONTINENTS blue-grey from orbit (witnessed: lit mean [83,95,112] vs warm albedo
1846
1886
  // [90,83,74] at 2000km -- the blue is the sky-irradiance term, NOT aerial inscatter).
@@ -1987,6 +2027,20 @@ void main() {
1987
2027
  color = mix(lit, hazed, apGate * uHazeMul); // uHazeMul lever (2026-06-10 'pale hazy': full-strength haze milked the midground)
1988
2028
  }
1989
2029
  }
2030
+ // UNDERWATER FOG (camera below sea level): replace the air-based Aerial Perspective with
2031
+ // absorption/scattering through water. Fog colour deepens with camera depth so the seabed
2032
+ // fades to blue-green with distance, red absorbed first. Applied only to terrain (uIsWater<0.5).
2033
+ if (uUnderwater > 0.5 && uIsWater < 0.5) {
2034
+ highp vec3 camA = atmPos(camWorld, terrainR);
2035
+ highp vec3 segKm = pAtm - camA;
2036
+ highp float dKm = length(segKm);
2037
+ float depth = max(0.0, terrainR - length(camWorld));
2038
+ // Water absorption coefficients (per km): red attenuates fastest, blue slowest.
2039
+ vec3 absorb = vec3(4.0, 0.8, 0.3) * (1.0 + depth * 0.0003);
2040
+ vec3 uwTrans = exp(-absorb * dKm);
2041
+ vec3 uwFog = vec3(0.002, 0.06, 0.16) + vec3(0.0, 0.02, 0.04) * depth / 1000.0;
2042
+ color = mix(color * uwTrans + uwFog * (1.0 - uwTrans), uwFog, smoothstep(50.0, 500.0, dKm * 1000.0));
2043
+ }
1990
2044
  // RIVERS post-lighting (witnessed browser-2115/2118: the river-blue in ALBEDO is multiplied
1991
2045
  // by the warm sun irradiance (sunIrr.b is low) so land rivers lose their blue in the lit
1992
2046
  // result -- 15k blue albedo px -> 0 lit px, even with the sun HIGH (sunDotUp 0.85). Ocean