From 3d54328b55666db042b20fd8f75681ddb8feab42 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 06:40:53 +0000 Subject: [PATCH] =?UTF-8?q?cockpit/GeoHelix:=20Garmin-as-surfel=20step=201?= =?UTF-8?q?=20=E2=80=94=20continuous=20residue=20+=20smoothed=20normals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toward "treat Garmin as the surfel the body has": upgrade the ver-8 grid decode's per-vertex surfel quality so terrain reads like the /helix body's surfel field rather than a quantized heightfield carpet. - CONTINUOUS residue: the within-kind CurveRuler brightness texture was applied per-CELL (one value per floor(pos·detail) cell → stepped at every boundary, the intra-family discontinuity). Now it's the inter-family CONTINUOUS version — the stride-4-over-17 value sampled at integer lattice CORNERS and smoothstep-interpolated between them (value-noise), matching geo/src/kurvenlineal.rs. The surfel texture flows across cells instead of blocking. - SMOOTHED normals: widen the central-difference gradient stencil from ±1 to ±2 so the shading stops catching every one-cell quantization stair (the "light on every numeric step" needle look) — WITHOUT touching the geometry. Verified: /garmin/grand-canyon screenshot — the plateau reads as a smooth mesa, the walls as clean cliffs with the dendritic side-canyons resolved, the drainage network crisp. tsc + vite build clean. Note: this is the surfel-QUALITY half. The OSM⊕Garmin overlay (drape OSM features onto the Garmin surfel via the shared HHTL address) and a bake-side height dequantize for Iceland's steep coastal cliffs are the remaining pieces of the "Garmin as surfel" arc. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC --- cockpit/src/GeoHelix.tsx | 45 ++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/cockpit/src/GeoHelix.tsx b/cockpit/src/GeoHelix.tsx index a925dde4a..9a8afe2ac 100644 --- a/cockpit/src/GeoHelix.tsx +++ b/cockpit/src/GeoHelix.tsx @@ -181,13 +181,16 @@ function decodeGrid(buf: ArrayBuffer): Decoded { } // normals: one central-difference gradient pass (one-sided at edges). True-scale // heights → true-world slopes, byte-parity with the ver-7 baker's terrain_normals. + // normals: central-difference gradient over a WIDER (±2) stencil → a smoothed + // surfel normal so the shading stops catching every one-cell quantization stair + // (the "light on every numeric step" needle look), WITHOUT touching the geometry. const normals = new Int8Array(nV * 3); for (let r = 0; r < H; r++) { - const rm = Math.max(r - 1, 0), rp = Math.min(r + 1, H - 1); + const rm = Math.max(r - 2, 0), rp = Math.min(r + 2, H - 1); const dzr = (zrow[rp] - zrow[rm]) || 1e-6; for (let c = 0; c < W; c++) { const i = r * W + c; - const cm = Math.max(c - 1, 0), cp = Math.min(c + 1, W - 1); + const cm = Math.max(c - 2, 0), cp = Math.min(c + 2, W - 1); const gx = (heights[r * W + cp] - heights[r * W + cm]) / (((cp - cm) * dx) || 1e-6); const gz = (heights[rp * W + c] - heights[rm * W + c]) / dzr; const il = 127 / Math.hypot(gx, 1, gz); @@ -196,25 +199,37 @@ function decodeGrid(buf: ArrayBuffer): Decoded { normals[i * 3 + 2] = Math.round(-gz * il); } } - // colour: palette[kind] × the deterministic CurveRuler residue (±18% within-kind - // texture — the /helix surfel look). Lattice mixes cached per cell (≈ tens of - // thousands of distinct cells for 16.5 M verts → the BigInt cost is amortized). + // colour: palette[kind] × the CONTINUOUS inter-family CurveRuler residue — the + // ±18% within-kind /helix surfel texture. The stride-4-over-17 golden-spiral + // value is sampled at integer lattice CORNERS and smoothstep-interpolated + // between them (value-noise). This is the inter-family fix from + // geo/src/kurvenlineal.rs: the old per-cell version STEPPED at every cell + // boundary (intra-family discontinuity → blocky texture); this flows + // continuously across cells — the surfel the body has. Corner values cached. const colors = new Uint8Array(nV * 3); const DETAIL = 48; - const cache = new Map(); - const cell17 = (px: number, py: number, pz: number, d: number): number => { - const cx = Math.floor(px * d), cy = Math.floor(py * d), cz = Math.floor(pz * d); + const cornerCache = new Map(); + const corner = (cx: number, cy: number, cz: number): number => { const key = (cx + 512) + (cy + 512) * 1024 + (cz + 512) * 1048576; - let v = cache.get(key); - if (v === undefined) { v = mix17(cx, cy, cz); cache.set(key, v); } + let v = cornerCache.get(key); + if (v === undefined) { + const s = mix17(cx, cy, cz), k = mix17(cx + 7, cy + 13, cz + 29); + v = (((s + 4 * k) % 17) / 16) * 2 - 1; + cornerCache.set(key, v); + } return v; }; + const smoothstep = (t: number) => t * t * (3 - 2 * t); for (let i = 0; i < nV; i++) { - const px = positions[i * 3], py = positions[i * 3 + 1], pz = positions[i * 3 + 2]; - const start = cell17(px, py, pz, DETAIL); - const k = cell17(px, py, pz, DETAIL * 3); - const phase = (((start + 4 * k) % 17) / 16) * 2 - 1; - const sweet = 0.90 + 0.18 * phase; + const X = positions[i * 3] * DETAIL, Y = positions[i * 3 + 1] * DETAIL, Z = positions[i * 3 + 2] * DETAIL; + const bx = Math.floor(X), by = Math.floor(Y), bz = Math.floor(Z); + const wx = smoothstep(X - bx), wy = smoothstep(Y - by), wz = smoothstep(Z - bz); + let res = 0; + for (let dz = 0; dz < 2; dz++) + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) + res += corner(bx + dx, by + dy, bz + dz) * (dx ? wx : 1 - wx) * (dy ? wy : 1 - wy) * (dz ? wz : 1 - wz); + const sweet = 0.90 + 0.18 * res; const kb = kinds[i] * 3; colors[i * 3] = Math.max(0, Math.min(255, Math.round(pal[kb] * sweet))); colors[i * 3 + 1] = Math.max(0, Math.min(255, Math.round(pal[kb + 1] * sweet)));