← Natura·Docs/Modules/Source

terrain.js

109 lines · in natura-src-1.0.0.zip as natura/terrain.js · a module · its entry in the module reference · its test: terrain-test.js

// terrain.js -- Natura TERRAIN (SYSTEMS.md #2, ALGORITHMS.md): the ground the world sits on, and the
// PHYSICAL half of "weather changes the world it falls on." Wraps the Terrain Engine for the initial
// island, then EVOLVES its heightfield by stream-power EROSION driven by the weather's moisture: where
// the rain runs (high flow) over steep ground that has been WET, it incises. So the windward slopes wear
// down under the rain-shadow's rain while the dry lee is preserved -- erosion inherits the rain-shadow,
// the way vegetation does, only in stone instead of leaf.
//
// A Natura system spec (init/step/fields), deterministic + node-testable. Couples to WEATHER (moisture).
// Computes its own D8 flow accumulation (the engine's flow needs separate setup). UMD.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.NaturaTerrain = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  // make a Terrain system. config: { N, seed, preset, TE (the TerrainEngine), Kf, m, n, floor, seaLevel }.
  // TE is the terrain-engine module (window.TerrainEngine / require('terrain.js')); if absent, uses
  // config.elevation(x,y) as the initial heightfield sampler (so this tests without the engine).
  function makeTerrain(config) {
    config = config || {};
    const N = config.N || 48;
    const seaLevel = config.seaLevel != null ? config.seaLevel : 0;
    const Kf = config.Kf != null ? config.Kf : 0.0016;          // erodibility
    const mm = config.m != null ? config.m : 0.5;               // flow exponent
    const nn = config.n != null ? config.n : 1.0;               // slope exponent
    const floor = config.floor != null ? config.floor : -0.35;  // bedrock floor (bound runaway)
    const weatherName = config.weather || 'weather';
    const gx = (i) => (i / (N - 1)) * 2 - 1;

    function initElev(rng) {
      const H = new Float32Array(N * N);
      let sampler = config.elevation;
      if (!sampler && config.TE) { const t = new config.TE.Terrain(config.seed || 1, { preset: config.preset || 'temperate-island', size: config.size || 120 }); sampler = (x, y) => t.heightAt(x, y); }
      if (!sampler) sampler = () => 0.2;
      for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) H[j * N + i] = sampler(gx(i), gx(j));
      return H;
    }

    // D8 flow accumulation on H: each land cell passes (1 + upstream) to its lowest neighbour, processed
    // high-to-low so upstream is done first. Returns a flow grid (drainage area proxy).
    function flowAccum(H) {
      const idx = []; for (let k = 0; k < N * N; k++) idx.push(k);
      idx.sort((a, b) => H[b] - H[a]);                          // high to low
      const flow = new Float32Array(N * N); for (let k = 0; k < N * N; k++) flow[k] = 1;
      for (const k of idx) {
        const i = k % N, j = (k / N) | 0; let lo = k, lh = H[k];
        for (const [di, dj] of [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]]) {
          const ni = i + di, nj = j + dj; if (ni < 0 || ni >= N || nj < 0 || nj >= N) continue;
          const m = nj * N + ni; if (H[m] < lh) { lh = H[m]; lo = m; }
        }
        if (lo !== k) flow[lo] += flow[k];                       // give my water to the lowest neighbour
      }
      return flow;
    }

    return {
      name: config.name || 'terrain',
      domain: 'physical',
      scale: 'planetary',
      params: config,

      init(rng) {
        const H = initElev(rng), land = new Uint8Array(N * N);
        for (let k = 0; k < N * N; k++) land[k] = H[k] > seaLevel ? 1 : 0;
        return { N, H, land, seaLevel, slope: new Float32Array(N * N), flow: new Float32Array(N * N), eroded: 0 };
      },

      step(st, ctx, dt) {
        const dd = clamp(dt / 86400, 0, 3);                     // erosion works on sim-DAYS (slow)
        if (dd <= 0) return st;
        const { N, H, land } = st;
        const w = ctx.fields[weatherName];
        const moist = w && w.moisture ? w.moisture : null;      // accumulated rain -> wetness
        // slope from the current (eroding) heightfield
        for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
          const il = Math.max(0, i - 1), ir = Math.min(N - 1, i + 1), jd = Math.max(0, j - 1), ju = Math.min(N - 1, j + 1);
          const dx = (H[j * N + ir] - H[j * N + il]) * 0.5 * N, dz = (H[ju * N + i] - H[jd * N + i]) * 0.5 * N;
          st.slope[j * N + i] = Math.hypot(dx, dz);
        }
        const flow = flowAccum(H); st.flow = flow;
        // sample wetness from the weather moisture grid (may be a different N)
        const wetAt = (i, j) => {
          if (!moist) return 1;                                 // no weather -> uniform (still erodes by slope)
          const WN = w.N || N; const fi = i / (N - 1) * (WN - 1), fj = j / (N - 1) * (WN - 1);
          const x0 = fi | 0, y0 = fj | 0, x1 = Math.min(WN - 1, x0 + 1), y1 = Math.min(WN - 1, y0 + 1), tx = fi - x0, ty = fj - y0;
          return moist[y0 * WN + x0] * (1 - tx) * (1 - ty) + moist[y0 * WN + x1] * tx * (1 - ty) + moist[y1 * WN + x0] * (1 - tx) * ty + moist[y1 * WN + x1] * tx * ty;
        };
        let tot = 0;
        for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
          const k = j * N + i; if (!land[k]) continue;
          const wet = clamp(wetAt(i, j) * 0.5, 0, 3);            // wetter -> more erosion (stream power scales with discharge)
          const E = Kf * Math.pow(flow[k], mm) * Math.pow(st.slope[k], nn) * wet * dd;
          const dH = Math.min(E, H[k] - floor);                 // never below bedrock floor
          if (dH > 0) { H[k] -= dH; tot += dH; }
        }
        st.eroded += tot;
        return st;
      },

      fields(st) {
        return { N: st.N, elevation: st.H, slope: st.slope, flow: st.flow, land: st.land, seaLevel: st.seaLevel };
      }
    };
  }

  return { makeTerrain };
});