What the land is

The island's main body is a rough circle about 1.3 km across (a coast about 640 m from its middle, before the peninsulas), set in a sea that runs to the horizon. From the sea floor, 30 m down offshore, the ground shelves up to a sandy beach and a lowland that rises to about 7 m within 90 m of the shore and rolls gently, a meter near the cottage and up to eight meters farther off. Behind the lowland stands a hill 150 m high, reaching 540 m out from its middle at its foot, its flanks cut by gullies that branch, large ones 140 m apart and smaller ones off them, down to 17 m. Two peninsulas reach south with a cove between them; the cottage stands near the cove on a level pad, flat at 4.6 m above the sea for 46 m round, blending back into the land by 100 m.

Two features are cut into this land. The walkway is a smooth path through nine points, from the landing beach north to the foot of the cottage's steps, 1.8 m wide, pressed 5 cm into the ground in a shallow groove of packed earth with an uneven edge. The brook runs about 434 m from a spring 30 m up the hill, down a rill, across the lawn under a bridge, behind the cottage, through a dingle and down a cascade to the sea; its water sits in a trench with a flat bed of wet gravel and banks of mud and moss, a damp darker margin in the grass either side. The ground is colored by what it is: sand by the water, grass above (thinner and browner higher up), bare rock where the slope is steeper than about 33 to 44 degrees, leaf litter under the forest. A walker may go anywhere on the island, up the hill or round the coast, until the ground falls more than 0.6 m under the sea: there they stop, knee deep, and slide along the shore.

The listing

field cottageSpot: place = (-16, 0, 330)                 // a root field: the land's pad and the cottage both read it

part land, at: (0, 0, 0), material: shaded(terrain), edge: sea(wade: 0.6 m),     // placed, never moves; it draws its own ground
          fineOver: outline.points(points: [(-640, -696), (784, -696), (784, 704), (-640, 704)])   // drawn finest here
  // ===== the features the land owns =====
  // each feature is a spine through points on the land's plan (y = 0): the land lays it on its own surface
  part walkway: spine.through(points: [(-22, 0, 498), (-22, 0, 476), (-34, 0, 450), (-14, 0, 424), (-38, 0, 400),
                                       (-12, 0, 378), (-22, 0, 354), (-16, 0, 340), (-16, 0, 333.6)],   // beach to the steps
                              kind: uniform, ends: doubled, segments: chord => floor(chord / 1 m) + 1)
    field halfWidth: meters = 0.9 m
  part brook: spine.through(points: [(34, 0, 128), (22, 0, 150), (8, 0, 168), ..., (-54, 0, 484), (-54, 0, 499)],   // 26 points
                            kind: uniform, ends: doubled, segments: chord => floor(chord / 1 m) + 1)
    // one value at each control point, number 0 to 25; read between two of them in a straight line
    field table   : curve = curve([(0, 30.3 m), (1, 19.1 m), (2, 16.5 m), ..., (25, 0.15 m)])   // the water's designed surface
    field depth   : curve = curve([(0, 0.5 m), ..., (8, 0.22 m), ..., (24, 0.25 m), (25, 0.1 m)])   // the bed below the water
    field halfBed : curve = curve([(0, 1.5 m), ..., (8, 2.2 m), ..., (24, 2.0 m), (25, 3.0 m)])
    field bank    : curve = curve([(0, 0.8), ..., (8, 0.45), ..., (25, 0.3)])             // rise over run
    field rounding: curve = curve([(0, 1 m), ..., (8, 0.6 m), ..., (18, 1.5 m), ..., (25, 1 m)])   // the shoulder's roundness
    field level   : List[meters]
    derive(level, heldTo(table, belowGround: 0.3 m, neverRising: true,
                          inYardFalling: 3 mm / 1 m, ground: height))      // the table, kept to its two rules
    part water: brookRibbon(level: level, halfBed: halfBed,
                              speed: p => min(0.15 m/s + 4.5 m/s * sqrt(fallPerMeter(p)), 3 m/s))
  field dressedArea: outline = outline.points(points: [(-190, 285), (175, 285), (200, 430), (175, 540), (120, 552), (60, 525),
                                                       (0, 507), (-60, 505), (-120, 512), (-200, 545), (-215, 420)])   // (x, z)
  field padMiddle: place = cottageSpot + (12 m, 0 m, 8 m)  // the cottage's pad: 12 m east, 8 m south of its front
  field padHeight: meters = 4.6 m

  // ===== the height: functions of a ground position =====
  // a ground position is a point like any other; these functions read only its x and z
  fn baseHeight(here: point) meters {
    let s = 0 m - coast(here);                          // meters in from the coast; negative at sea
    var h = shelf(s);
    if s > 0 m {
      h += gullies(here, hill(here), gradient(hill, at: here, step: 2 m)) * smoothstep(s, from: 60 m, to: 220 m);
      h += roll(here) * (0.8 m + 7.5 m * smoothstep(planDistance(here, padMiddle), from: 46 m, to: 170 m))
                      * smoothstep(s, from: 0 m, to: 40 m);
    }
    lerp(h, padHeight, t: 1 - smoothstep(planDistance(here, padMiddle), from: 46 m, to: 100 m))   // the pad, flat
  }
  fn shelf(s: meters) meters {                           // sea floor, beach, lowland
    if s < 0 m { -0.3 m - seaDepth(0 m - s) }
    else { lerp(-0.3 m, 5.8 m, t: smoothstep(s, from: 0 m, to: 90 m)) + 1.2 m * smoothstep(s, from: 0 m, to: 18 m) }
  }
  fn seaDepth(out: meters) meters { min(30 m, 0.05 * out + 0.0003 * out * out / 1 m) }
  fn hill(here: point) meters { rawHill(here) * 150 m / highest(rawHill, every: 32 m) }   // its top at 150 m
  fn rawHill(here: point) number { ... }                 // a warped dome, 540 m from its middle to its foot, ridged flanks
  fn roll(here: point) number {                          // two scales of noise
    noise.fbm(here, size: 70 m, octaves: 4, seed: 41) * 0.9 + noise.fbm(here, size: 26 m, octaves: 2, seed: 43) * 0.25
  }
  fn coast(here: point) meters {                         // signed: negative on land, positive at sea
    var d = distance(here, circle(center: (0, 0, -40), radius: 640 m));
    d = smoothMin(d, distance(here, capsule(from: (420, 0, 380), to: (700, 0, 600), radius: 75 m)), width: 100 m);
    d = smoothMin(d, distance(here, capsule(from: (-330, 0, 430), to: (-250, 0, 720), radius: 65 m)), width: 100 m);
    d = smoothMin(d, distance(here, capsule(from: (190, 0, 600), to: (130, 0, 730), radius: 45 m)), width: 60 m);   // a spit
    d = smoothMax(d, -distance(here, circle(center: (-60, 0, 790), radius: 290 m)), width: 80 m);   // the cove, cut out
    d = smoothMax(d, -distance(here, circle(center: (430, 0, -470), radius: 100 m)), width: 60 m);  // a bay
    d = smoothMax(d, -distance(here, circle(center: (-640, 0, -60), radius: 80 m)), width: 50 m);   // an inlet
    d = smoothMax(d, -distance(here, circle(center: (-120, 0, -700), radius: 70 m)), width: 50 m);  // a cleft
    let near = 1 - smoothstep(abs(d), from: 70 m, to: 130 m);    // rough only near the coast
    d + near * 22 m * noise.fbm(warped(here, by: 40 m, size: 300 m, octaves: 3, seed: 11), size: 180 m, octaves: 6, seed: 17)
  }

  // ===== the erosion filter: gullies cut into the hill, point by point =====
  fn easeOut(t: number) number { let v = 1 - clamp(t, within: 0 ..= 1); 1 - v * v }   // 0 at 0, 1 at 1, steep at the start
  fn gullies(here: point, h: meters, slope: direction) meters {   // slope: the raw hill's gradient, rise over run
    var out   = h;
    var steer = slope;
    var size  = 140 m;
    var depth = 9 m;
    if h > 1 m {                                         // only the hill is cut; flat ground is left as it is
      each octave in 0 ..< 4 {
        let steep = length(steer);
        if steep > 0.02 {                                // on ground that level there is no way down to follow
          let stripes = noise.phacelle(here / size, dir: steer / steep, seed: 31 + octave);
          let up = clamp(h / 150 m, within: 0 ..= 1);    // how far up the hill it is
          let crisp = smoothstep(up, from: 0.02, to: 0.2) * (1 - 0.6 * smoothstep(up, from: 0.75, to: 1));   // none at the foot
          let amount = depth * easeOut(steep / 0.25) * crisp;   // deepest on the steep flanks
          out = out - amount * stripes.depth;            // material only taken away
          steer = steer - amount / size * stripes.slope; // the slope the gully leaves steers the next: small gullies branch off large
        }
        size = size / 2;
        depth = depth / 2;
      }
    }
    out
  }

  // ===== the ground: the height, then the features, in the land's own order =====
  field ground: heightGrid = {
    let grid = heightGrid(cell: 2 m, points: 1025, height: baseHeight);
    let pressed = press(grid, walkway, depth: 5 cm, fullWithin: 1.4 m, goneBy: 6 m, smoothedAlong: 9, passes: 2,
                        endHeight: padHeight, endClear: 5 m, endFull: 14 m);
    carve(pressed, brook, bed: brook.level - brook.depth, flat: brook.halfBed, banks: brook.bank, rounding: brook.rounding)
  }

  // ===== the land's methods: what anyone may ask it =====
  fn height(here: point) meters          { ground.heightAt(here) }     // reads here.x and here.z
  fn gradient(here: point) direction     { ground.gradientAt(here) }   // a vector on the plan, rise over run
  fn slope(here: point) number           { length(gradient(here)) }
  fn normal(here: point) direction       { ground.normalAt(here) }
  fn curvature(here: point, over: meters) number { ground.curvatureAt(here, over) }
  fn pathDistance(here: point) meters {
    if let Some(n) = walkway.nearest(here, within: 16 m) { n.0 } else { 16 m }    // n is (distance, along)
  }
  fn brookDistance(here: point) meters {
    if let Some(n) = brook.nearest(here, within: 8 m) { n.0 } else { 8 m }
  }
  fn dressed(here: point) bool           { dressedArea.contains(here) }
  fn forestDensity(here: point) fraction { ... }          // painted patches of thick forest
  fn groveDensity(here: point) fraction  { ... }          // conifer groves on the far slopes

  // ===== the ground's look: layers over grass, each asking a method =====
  derive(ground.look, layered(grass,                      // illustration
    over: [layer(sand,        where: p => fading(height(p), from: 2.7 m, to: 1.3 m)),
           layer(rock,        where: p => fading(slope(p), from: 33 deg, to: 44 deg)),
           layer(litter,      where: p => forestDensity(p)),
           layer(earth,       where: p => fading(pathDistance(p), from: 1.25 m, to: 0.7 m), edge: uneven(0.25 m)),
           layer(damp,        where: p => fading(brookDistance(p), from: 4.6 m, to: 2.4 m)),
           layer(mud,         where: p => fading(brookDistance(p), from: 2.5 m, to: 1.5 m)),
           layer(wetGravel,   where: p => fading(brookDistance(p), from: 1.55 m, to: 0.95 m))]))

part cottage, at: (cottageSpot.x, land.height(cottageSpot), cottageSpot.z)   // stands on the land the pad was cut for

// ===== A bounded land of another kind =====
part skyIsle, at: (0, 400 m, 0), edge: fall(to: cloudsBelow)    // illustration: an island floating in the sky
  field ground: heightGrid = heightGrid(cell: 1 m, points: 257, height: p => ...)

Walking through it

The land is a part

land is an ordinary part with the placed kind of frame, at the world's origin, never moving. It is not a special "world field" asked by position everywhere: it is a part like a planet, answering questions in its own frame, so a land on a moving body (a floating island, a planet in orbit) would answer the same way (Terrain). It draws its own ground: its shape is the height grid, so material: on the part line applies to it as to any drawn part. The grid is centered on the land part's origin, 1025 points 2 m apart reaching 1024 m each way; past it the land answers -30 m. Besides the part's own options it takes two: edge: (what lies past the border, below) and fineOver:, an outline on the plan inside which the ground is drawn at the grid's own cell and outside it at every fourth (the island's covers all its land, so what a walker stands on is what is drawn, to within 1 mm: the engine's guarantee, Terrain). The land is solid on the ground layer with no call.

The features the land owns

The walkway and the brook are child parts of the land: spines, smooth lines through control points (Sculpting: what a spine answers), each with fields of its own. Their points are written on the land's plan, at height 0, and the land lays them on its surface, so a spot the walkway gives (walkway.spotAt(...)) is a point on the ground. Both are uniform Catmull-Rom curves with their ends doubled (the first and last points repeated), and each pair of control points is cut into one piece for each whole meter between them, and one more, so a spine's points (walkway.points()) fall about a meter apart; the land presses and carves along exactly those. The brook's designed water table, its bed's depth below the water, the half-width of its flat bed, the slope of its banks and the roundness of its shoulders are curves, one value at each control point, numbered 0 to 25 and read between two of them in a straight line (table.at(brook.indexAt(along))). Its level is a subscription: the table held to two rules, never less than 0.3 m below the ground round it and never rising downstream, falling at least 3 mm a meter in the yard so it flows. Its water is a part of the brook: a see-through ribbon whose speed is a closure of the point, 0.15 m/s plus 4.5 m/s times the square root of the fall per meter there, never more than 3 m/s (Functions: closures). The dressed area is an outline of eleven corners, written as (x, z) points on the plan, where vegetation is laid out; it answers contains(p), changes no height and is not a boundary. The cottage's spot is a field of the root, cottageSpot, that both the land's pad and the cottage read, so the pad need not read the cottage, which stands on the land.

Height as functions of position

The land's height is a sum of shapes, each an open function: a pure, bounded function of a ground position that a tool runs for every point of the grid. A ground position is a point, (x, y, z), like any other place in the world; the land's functions read only its x and z (Values: positions). The blends are the maths functions smoothstep and lerp, the distances to the pad are planDistance, which measures on x and z only, and the coast joins and cuts areas with smoothMin and smoothMax (Expressions: maths functions). coast is a signed distance (negative on land): a body and peninsulas joined smoothly, a cove, a bay, an inlet and a cleft cut out, roughened only near the shore by a warped fractal noise (warped, noise.fbm, all with a seed:). shelf turns distance from the coast into the sea floor, the beach and the lowland. hill is a warped dome scaled so its top is 150 m; finding that top asks the whole field before any point, over a lattice 32 m apart: highest(rawHill, every: 32 m). roll is two scales of noise, 70 m and 26 m, weighted 0.9 and 0.25, fading in over the first 40 m from the shore and larger the farther from the cottage. The pad is blended in last. Each uses var locals and +=, which change only the function's own copy (Blocks; Expressions: assignment).

The erosion filter

gullies wears the hill as water would, but as a filter on single points, not a simulation: no point waits for another. Four octaves of stripes, 140, 70, 35 and 17.5 m apart and each half as deep as the last, cut into the ground along the way the slope falls. Each octave takes the direction of the steer, the slope the last octave left (the hill's own slope to begin with), gives noise.phacelle stripes that run along it, and cuts amount meters times the stripes' depth, 0 on a crest and 1 on a gully's floor, so it only takes material away. The amount is the octave's depth, less on a gentler slope (full strength at a slope of 0.25 and more) and faded to nothing at the foot and cut back by 0.6 toward the summit, so both stay crisp. The stripes' slope, how the depth changes across them, times amount and divided by the spacing, is the rise over run the gully adds to the steer, so small gullies branch off large ones. On ground with a slope below 0.02 an octave does nothing, and ground no higher than 1 m is left as it is (Open functions: a height from a ground position). Any world with a hill could use it, so it belongs in a package.

The terrain's own order

ground is a construction pipeline owned by the land, its product a grid of heights held in a fixed field (a land's heights are not a shape): the grid of 1025 by 1025 heights 2 m apart, then the walkway pressed in, then the brook carved. The walkway's own height is the grid's at its points, smoothed by a box of nine of them, twice (about 9 m), its last point set to the pad's height so there is no step where it meets the pad; the ground within 1.4 m of its middle is eased fully to 5 cm below that height, the easing fading by smoothstep to nothing at 6 m; and the pressing itself grows from nothing within 5 m of the walkway's end to the full 5 cm at 14 m. The brook is carved as a trench down to its bed, flat across the half-width and then rising at the bank's slope, joined to the ground by smoothMin over the shoulder's roundness. The dressed area is no step: it changes no height. The order is the meaning: pressing the walkway after carving the brook would fill the brook where they cross. Because one owner applies the features in its own order, the features stay separate, named parts that others can ask, without two parts changing one land in an order nobody wrote (Terrain: the two steps, exactly; Loading: order follows dependencies).

The land's methods

The land's functions are its contract: height, gradient (a vector on the plan, rise over run), slope (its length, a plain number), normal and curvature (Terrain: how each is worked out), the distance to the walkway and to the brook (each answers whatever within: asks; these two ask 16 m and 8 m, so a farther place is answered as 16 m or 8 m), whether a point is in the dressed area, how thick the forest and the conifer groves grow there. A place that is not on a feature asks land.height(p), which reads p.x and p.z; the scatter asks the slope, the distances and the density; walkers, the navigation mesh and the birds ask the height without any line of content mentioning it. Two lands made differently are interchangeable to everything that asks the same questions.

The look: layers asking the features

The land draws its own ground, in the browser's ground shader (material: shaded(terrain)), which picks each pixel's color from the height, the facing, the painted masks, the distance to the eye, the size of a pixel on the ground and the time. Its look is also written here as layers over grass, each a closure of a ground point asking one of the land's methods: sand fading in between 2.7 and 1.3 m above the sea; rock where the slope passes 33 to 44 degrees; litter where the forest is thick; packed earth within 0.7 m of the walkway's middle, gone beyond 1.25 m, with an edge made uneven by 0.25 m; and three bands by the brook, a damp margin from 4.6 to 2.4 m, mud from 2.5 to 1.5 m, wet gravel from 1.55 to 0.95 m. The walkway and the brook thus mark the ground they lie in through the land's own questions, pathDistance and brookDistance. This derive is an illustration of the spelling (Materials); a look chosen from where the eye is, as the shader does, is a design question (Terrain: open questions).

What lies past the edge

Whether there is ground everywhere is the designer's choice. The island is bounded by its sea, and states its edge as an option of the land: edge: sea(wade: 0.6 m), a walker going no deeper than 0.6 m under the sea, sliding along the shore instead (Terrain: edges). A land of another kind says something else past its edge. The second part of the listing is an illustration: an island floating 400 m up, whose edge is a fall to the clouds below, edge: fall(to: cloudsBelow). Others might be a wall, a wrap (walk off the east and come back from the west), or dragons; each is a feature of the land like the brook.

← The looking-glass | Contents | A villager →