Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cockpit/public/body.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/<loc> is SPA-handled (BodyHelix parses the path); the DATA is served by the cockpit-server param route /api/garmin/:location, which resolves <loc> 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).",
Expand Down
Binary file added cockpit/public/canyon.v8grid.soa.gz
Binary file not shown.
47 changes: 35 additions & 12 deletions cockpit/src/GeoHelix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down