| … | … | @@ -579,43 +579,82 @@ function buildMesh(st, heights, n, mpp, lo, hi, exag) { |
| 579 | 579 | for (let i = 0; i < n * n; i++) { |
| 580 | 580 | pos.array[i * 3 + 1] = (heights[i] / widthM) * exag; |
| 581 | 581 | } |
| 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 | |
| 585 | 621 | for (let r = 0; r < n; r++) { |
| 586 | 622 | for (let c = 0; c < n; c++) { |
| 587 | 623 | 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; |
| 612 | 628 | } |
| 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; |
| 614 | 656 | } |
| 615 | 657 | } |
| 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; |
| 619 | 658 | pos.needsUpdate = true; |
| 620 | 659 | g.setAttribute("color", new THREE.BufferAttribute(col, 3)); |
| 621 | 660 | g.computeVertexNormals(); |