OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev commits / a211995

lookdev 3D: curvature is the third attribute — altitude and slope alone are monotone
Reported as "compare this with the shadertoy coloring which is much more complex, i believe it samples different attributes?" -- correct on both counts. The material was altitude + slope. Both are SMOOTH functions of the heightfield, so their combination is smooth too, and the result was one dusty tan across an entire massif. No amount of retuning those two fixes it; the information is not in them. The attribute that breaks it up is CURVATURE. Concave ground collects water and sediment and grows things; convex ground sheds both and shows rock. It is the same signal the erosion filter already produces as ErosionResult.ridge -- whose own comment reads "-1 in creases, +1 on ridges. Free; drives materials." -- and which this page was computing and discarding. runevision's post describes the filter as having "a ridge map for water drainage", which is what it is for. Computed here as a normalised laplacian rather than read from the filter, so BOTH panels get it including bare diffusion where no erosion pass has run. Normalised by the 95th percentile of |laplacian|, not the max: a few extreme cells otherwise push everything to zero. What it now drives: wetness creases are darker, greener, hold vegetation higher rock lighter and more exposed on ridges, darker and mossier in creases snowline later on steep ground, since snow does not lie on cliffs AO creases sit in their own shadow, 0.72..1.0 Vegetation is a band -- lush low, meadow mid, dry above -- rather than a single tint, which is the other half of why one colour was covering everything. Still not the Shadertoy: no soft shadows, no real ambient occlusion, and the aerial perspective is still one fog colour. Those are the raymarched version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a2119954a9fd6c9c6b80b39a94e63bee1e94617a
by plankatron <noreply@anthropic.com> · 2026-08-31T20:56:25.000Z

Changed files (1) +70 -31

index.html +70 -31
……@@ -579,43 +579,82 @@ function buildMesh(st, heights, n, mpp, lo, hi, exag) {
579579 for (let i = 0; i < n * n; i++) {
580580 pos.array[i * 3 + 1] = (heights[i] / widthM) * exag;
581581 }
582 // Slope from the heightfield in METRES, before exaggeration, so the material
583 // describes the terrain and not the viewing choice. Exaggerating the mesh
584 // must not turn grassland into cliff face.
582
583 // THREE attributes, not two.
584 //
585 // Altitude and slope are both smooth, so a material built from them alone
586 // comes out monotone -- one dusty tan across the whole massif, which is what
587 // the first version did. The attribute that breaks it up is CURVATURE:
588 // concave ground collects water and sediment and grows things, convex ground
589 // sheds both and shows rock. It is the same signal the erosion filter already
590 // produces as ErosionResult.ridge ("-1 in creases, +1 on ridges. Free; drives
591 // materials.") and which this page was discarding.
592 //
593 // Curvature is computed here from the heightfield so BOTH panels get it,
594 // including bare diffusion where no erosion pass has run.
595 const curv = new Float32Array(n * n);
596 let cmax = 1e-9;
597 for (let r = 1; r < n - 1; r++) {
598 for (let c = 1; c < n - 1; c++) {
599 const i = r * n + c;
600 const lap = heights[i - 1] + heights[i + 1] +
601 heights[i - n] + heights[i + n] - 4 * heights[i];
602 curv[i] = lap;
603 const a = Math.abs(lap);
604 if (a > cmax) cmax = a;
605 }
606 }
607 // normalise robustly: the tail is a few extreme cells, so scale by a high
608 // percentile rather than the max or everything lands near zero.
609 const sorted = Float32Array.from(curv, Math.abs).sort();
610 const p95 = sorted[Math.floor(sorted.length * 0.95)] || cmax;
611
612 const clamp01 = v => v < 0 ? 0 : v > 1 ? 1 : v;
613 const sstep = (a, b, x) => { const t = clamp01((x - a) / (b - a)); return t * t * (3 - 2 * t); };
614 const lerp3 = (A, B, k) => [A[0] + (B[0]-A[0])*k, A[1] + (B[1]-A[1])*k, A[2] + (B[2]-A[2])*k];
615
616 const GRASS = [0.20, 0.30, 0.13], MEADOW = [0.33, 0.40, 0.17];
617 const DRY = [0.47, 0.43, 0.26], SCREE = [0.44, 0.40, 0.36];
618 const ROCK = [0.34, 0.31, 0.30], ROCK_L = [0.52, 0.49, 0.46];
619 const SNOW = [0.95, 0.96, 0.98], WET = [0.16, 0.19, 0.15];
620
585621 for (let r = 0; r < n; r++) {
586622 for (let c = 0; c < n; c++) {
587623 const i = r * n + c;
588 const y = heights[i];
589 const t = (y - lo) / span;
590 let R, G, B;
591 if (bySlope) {
592 const cm = Math.min(n - 1, c + 1), cp = Math.max(0, c - 1);
593 const rm = Math.min(n - 1, r + 1), rp = Math.max(0, r - 1);
594 const dx = (heights[r * n + cm] - heights[r * n + cp]) / (mpp * (cm - cp || 1));
595 const dz = (heights[rm * n + c] - heights[rp * n + c]) / (mpp * (rm - rp || 1));
596 const slope = Math.min(1, Math.hypot(dx, dz) / 1.2); // ~50 deg = 1
597 const rock = Math.pow(slope, 0.75);
598 const snow = Math.max(0, Math.min(1, (t - 0.62) / 0.18)) * (1 - rock * 0.85);
599 const veg = (1 - rock) * (1 - snow);
600 // grass -> dry -> rock -> snow
601 const gr = [0.24, 0.34, 0.16], dry = [0.46, 0.42, 0.24];
602 const rk = [0.36, 0.33, 0.31], sn = [0.94, 0.95, 0.97];
603 const lowveg = Math.min(1, Math.max(0, 1 - t * 2.0));
604 const vg = [gr[0]*lowveg + dry[0]*(1-lowveg),
605 gr[1]*lowveg + dry[1]*(1-lowveg),
606 gr[2]*lowveg + dry[2]*(1-lowveg)];
607 R = vg[0]*veg + rk[0]*rock + sn[0]*snow;
608 G = vg[1]*veg + rk[1]*rock + sn[1]*snow;
609 B = vg[2]*veg + rk[2]*rock + sn[2]*snow;
610 } else {
611 R = 0.20 + 0.75 * t; G = 0.30 + 0.55 * t; B = 0.38 + 0.22 * (1 - t);
624 const t = (heights[i] - lo) / span;
625 if (!bySlope) {
626 col[i*3] = 0.20 + 0.75*t; col[i*3+1] = 0.30 + 0.55*t; col[i*3+2] = 0.38 + 0.22*(1-t);
627 continue;
612628 }
613 col[i * 3 + 0] = R; col[i * 3 + 1] = G; col[i * 3 + 2] = B;
629 const cm = Math.min(n - 1, c + 1), cp = Math.max(0, c - 1);
630 const rm = Math.min(n - 1, r + 1), rp = Math.max(0, r - 1);
631 const dx = (heights[r*n+cm] - heights[r*n+cp]) / (mpp * (cm - cp || 1));
632 const dz = (heights[rm*n+c] - heights[rp*n+c]) / (mpp * (rm - rp || 1));
633 const slope = Math.hypot(dx, dz); // metres per metre
634 const k = clamp01(curv[i] / (p95 * 2) + 0.5); // 0 crease .. 1 ridge
635
636 const rock = sstep(0.30, 0.85, slope);
637 // snow lies later on steep ground and later in creases where it is shaded
638 const snowline = 0.58 + 0.22 * rock;
639 const snow = sstep(snowline, snowline + 0.16, t) * (1 - 0.7 * rock);
640 // creases are wet: darker, greener, and they hold their vegetation higher
641 const wet = sstep(0.55, 0.10, k);
642
643 // vegetation band: lush low, meadow mid, dry above
644 let veg = t < 0.5 ? lerp3(GRASS, MEADOW, sstep(0.0, 0.5, t))
645 : lerp3(MEADOW, DRY, sstep(0.5, 0.85, t));
646 veg = lerp3(veg, WET, 0.55 * wet);
647 // exposed rock is lighter on ridges, darker and mossier in creases
648 let rk = lerp3(ROCK, ROCK_L, sstep(0.45, 1.0, k));
649 rk = lerp3(rk, SCREE, sstep(0.75, 0.35, k) * 0.5);
650
651 const a = lerp3(veg, rk, rock);
652 const b = lerp3(a, SNOW, snow);
653 // cheap curvature AO -- creases sit in their own shadow
654 const ao = 0.72 + 0.28 * sstep(0.15, 0.85, k);
655 col[i*3] = b[0] * ao; col[i*3+1] = b[1] * ao; col[i*3+2] = b[2] * ao;
614656 }
615657 }
616 // Recentre so orbiting is about the terrain, not about y = 0.
617 const mid = ((lo + hi) * 0.5 / widthM) * exag;
618 for (let i = 0; i < n * n; i++) pos.array[i * 3 + 1] -= mid;
619658 pos.needsUpdate = true;
620659 g.setAttribute("color", new THREE.BufferAttribute(col, 3));
621660 g.computeVertexNormals();