diff --git a/cockpit/public/body.manifest.json b/cockpit/public/body.manifest.json index 53415057d..8572107d4 100644 --- a/cockpit/public/body.manifest.json +++ b/cockpit/public/body.manifest.json @@ -3,7 +3,8 @@ "osm_latest": "berlin.helix.soa.gz", "iceland_latest": "iceland_dem.helix.soa.gz", "garmin_scenes": { - "iceland": "iceland_dem.v8grid.soa.gz" + "iceland": "iceland_dem.v8grid.soa.gz", + "grand-canyon": "canyon.v8grid.soa.gz" }, "garmin_note": "garmin_scenes = the /garmin/:location registry (mod-rewrite style). The page route /garmin/ is SPA-handled (BodyHelix parses the path); the DATA is served by the cockpit-server param route /api/garmin/:location, which resolves through this map to an embedded dist file. Adding a scene = bake the .soa.gz into dist (Dockerfile) + one entry here — no server code change. 'iceland' aliases the existing DEM bake; 'canyon' lands with the Garmin typed-rebuild bake (claude-notes/plans/2026-07-07-garmin-img-typed-rebuild.md).", "iceland_note": "iceland_dem.helix.soa.gz = full-resolution TEXTURED Iceland DEM (BSO2 ver 7 = F16 pos + Signed360 normal + per-vertex RGB drape): 16,515,072 verts / 32,997,378 tris / 3,584 HHTL tiles. Colour = KIND classification (ocean/green/rock/scree/ice/lava, from ESRI World Imagery luminance+saturation + elevation) x helix::CurveRuler golden-spiral residue brightness x imagery luminance. Equirectangular, true-scale elevation, sea level -> y=0. Baked by geo/src/bin/iceland_dem.rs (encode_mesh_bso2, feature 'helix'); DEM elevation + ESRI imagery fetched on one grid by scripts/fetch_iceland_dem.py (DEMG v2). 151 MB gz -> version-controlled as a 2-part split zip under .claude/maps/ (iceland_dem_16m.z01 + .zip, each < 100 MB); the Dockerfile reassembles + extracts it into cockpit/dist/ so it serves same-origin. Replaces the sparse iceland.helix.soa.gz scatter (kept on disk, unreferenced).", diff --git a/cockpit/public/canyon.v8grid.soa.gz b/cockpit/public/canyon.v8grid.soa.gz new file mode 100644 index 000000000..c14d2fea5 Binary files /dev/null and b/cockpit/public/canyon.v8grid.soa.gz differ diff --git a/cockpit/src/GeoHelix.tsx b/cockpit/src/GeoHelix.tsx index d2da3d92d..a925dde4a 100644 --- a/cockpit/src/GeoHelix.tsx +++ b/cockpit/src/GeoHelix.tsx @@ -425,18 +425,34 @@ vec3 terrainColor(float h){ void main(){ // GOURAUD: shade per-vertex from the cheap rim normal, interpolate the COLOUR across the // face. At 6.8 M sub-pixel tris this matches per-fragment lighting visually but leaves the - // fragment shader trivial. Two-sided ambient keeps any back faces lit without a flip. + // fragment shader trivial. vec3 n = normalize(normalMatrix * aNormal); - const vec3 L = vec3(-0.401, 0.783, 0.476); - float ndl = max(abs(dot(n, L)), 0.0); - float shade = min(0.34 + 0.20*(abs(n.y)*0.5+0.5) + 0.12*(-n.x*0.5+0.5) + 0.92*ndl, 1.3); - // Colour is ALWAYS aColor now: for the DEM terrain aColor is the baked kind × imagery × helix - // texture (real green / ice / volcano / rock), for buildings + anatomy it is the concept tint — - // the shader no longer guesses colour from height. Gouraud shade applies the 3D lighting form. - vColor = aColor * shade; - // Geo relief: raise the true-scale island by uExag. The Kurvenlineal golden-spiral residue is - // already in position + normal (baked by the DEM baker), so the shader adds no ruler of its own. - // Anatomy (uGeo==0): untouched. + vec3 lit; + if (uGeo > 0.5) { + // (1) HYPSOMETRIC tint — blend the baked KIND colour (aColor: water blue, forest + // green, else bare-terrain grey) with the height ramp so elevation READS as + // colour (green lowland → brown → grey → white peak). terrainColor() normalizes + // by the measured [uYMin,uYMax] and sqrt-spreads the quantized tail. + vec3 base = mix(aColor, terrainColor(position.y), 0.55); + // (2) SUNSET lighting — a low WARM key + a cool SKY fill. This colours the light + // (lit slopes go golden, shadowed slopes cool-blue), the depth cue that makes a + // range feel alive — instead of the old grey brightness multiplier. + vec3 SUN = normalize(vec3(-0.55, 0.42, 0.72)); // low azimuth, ~25° elevation + float ndl = max(dot(n, SUN), 0.0); + float sky = 0.5 + 0.5 * n.y; // hemispheric fill, more from above + vec3 light = vec3(1.26, 0.95, 0.66) * (0.16 + 0.95 * ndl) // warm golden key + + vec3(0.40, 0.50, 0.66) * (0.34 * sky); // cool sky fill + lit = base * light; + } else { + // Anatomy path — DEAD in GeoHelix (only /geo /ice /garmin route here), kept + // byte-identical to BodyHelix so the fork stays a faithful copy. + const vec3 L = vec3(-0.401, 0.783, 0.476); + float ndl = max(abs(dot(n, L)), 0.0); + float shade = min(0.34 + 0.20*(abs(n.y)*0.5+0.5) + 0.12*(-n.x*0.5+0.5) + 0.92*ndl, 1.3); + lit = aColor * shade; + } + vColor = min(lit, vec3(1.35)); + // Geo relief: raise the true-scale terrain by uExag. Anatomy (uGeo==0): untouched. vec3 dpos = position; if (uGeo > 0.5) { dpos.y = position.y * uExag; @@ -539,7 +555,14 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir // aColor) + uExag=1. uRuler/uTime are retained-but-0: the Kurvenlineal golden-spiral residue is // now baked into the mesh (geometry + normals), so the shader applies no ruler — the terrain is a // static surface, not a shader-animated one. - const uniforms = { uAlpha: { value: 1 }, uGeo: { value: isTerrainScene ? 1 : 0 }, uYMin: { value: yMin }, uYMax: { value: yMax }, uExag: { value: isTerrainScene ? 15 : 1 }, uTime: { value: 0 }, uRuler: { value: 0 } }; + // Relief exaggeration AUTO-SCALED by the measured height span so every terrain + // reads with a consistent vertical presence (~0.11 of the frame) regardless of + // its true-scale span. Iceland (span ~0.0074) → ~15 (matches the hand-tuned + // value); the Grand Canyon (span ~0.05, far deeper relative to its extent) → + // ~2.2, so its steep walls read as WALLS, not an over-exaggerated needle + // curtain (caught on the first /garmin/grand-canyon screenshot). Clamped. + const uExagVal = isTerrainScene ? Math.min(20, Math.max(1.5, 0.11 / Math.max(yMax - yMin, 1e-6))) : 1; + const uniforms = { uAlpha: { value: 1 }, uGeo: { value: isTerrainScene ? 1 : 0 }, uYMin: { value: yMin }, uYMax: { value: yMax }, uExag: { value: uExagVal }, uTime: { value: 0 }, uRuler: { value: 0 } }; const mat = new THREE.ShaderMaterial({ uniforms, vertexShader: VERT, fragmentShader: FRAG, side: THREE.FrontSide }); const mesh = new THREE.Mesh(geom, mat); scene.add(mesh);