Birdoggydog's Builds

Putting river water back on its ground in the generator

190 stretches of river hung in the air beside the island's tall cliffs. Three causes in the carve and the spline are fixed, and now none do.

The island’s rivers weren’t lying on the ground. Beside the tall cliffs, 190 stretches of stream hung more than 6 m over their beds, the worst by 65 m. Now none do, all 20 of the tall falls are still there, and the painted waterfalls went from 48 to 52.

54 seconds: six places as a before and after profile of the stream, then a flight past the real cliff.

Why were the rivers hanging in the air?

I wanted the world generation issues fixed, and this was the water part. The generator holds its peaks and rims at a fixed height (height_floor), so erosion can’t notch them. A stream that runs off one drops 57 to 307 m inside a single 8 m cell of the height grid. All 190 floating stretches were on the 20 tall faces that result.

A contact sheet of film frames. The top left shows two line profiles, before and after of a stream falling down a cliff, with a red area in the before picture where the water is far above the ground. The other frames are views of tall cliffs with streams running down them.

The first picture is a stream down a 210 m cliff beside its river: 65.5 m of water hanging over its bed before, 2.4 m after.

Three separate things were wrong, and I found each by going to a stretch that floated and asking what held the ground up there.

What did the carve do wrong?

Each stream carves a trench, and where two overlapped, the lowest trench won. A side stream coming down a cliff 0 to 7.3 m from its river was dug down to the river’s bed, 10 to 270 m lower than its own, so its fall hung over a hole it wasn’t in.

Now each vertex also keeps how far it is past its own bed’s edge, and the stream whose bed is nearer sets the ground. The lowest still wins where both beds cover the vertex, or where the two are within 0.5 m of each other:

lowest = ((c_out <= 0.0) & (old_out <= 0.0)) | (np.abs(c_out - old_out) < OVERLAP_M)
take = np.where(lowest, c_core < old, c_out < old_out)

How did the spline get tributaries under their rivers?

The curve through a stream’s points is a Catmull-Rom spline, which is allowed to overshoot between points. That’s fine in plan view but not in height. Over a 290 m drop in one cell it dipped 11 m under the foot, and since water never rises, the tributary stayed there, 10 to 19 m under the river it joined. The height of each piece of the curve is now clipped between the two points it runs between.

The same kind of mistake was in the join trimming. When a tributary’s last points are moved to the main stream’s edge, they used to take the main stream’s level, which for a steep stream is meters up its own bed and so under its own ground. A moved point now takes the higher of the main stream’s level and its own stream’s.

How can water come down a 300 m face?

A grid 8 m across can’t hold a vertical face. A vertex half a meter off the line of a 300 m face is 18 m off in height, and a neighbour’s bed ends up under it. So _settle() in water_ways.py makes the face a slope the grid can hold, in two steps.

First, a tributary comes down to its main stream’s level before it reaches the main stream’s banks, plus 2 m more. For its last 80 m, each vertex finds the nearest main-stream sample and the width of the main stream’s zone there, and from the last vertex outside the zone on, it takes the lower of its level and the main stream’s.

Second, no face may fall more than 6 m for each meter along (about 80 degrees). Working back from the mouth, a vertex that is higher than its downstream neighbour by more than that is lowered to exactly that. The lowered water upstream is the cutting back: the lip is dug into the cliff top as a slot gorge 10 to 60 m long, in ground only 1 m thick, the way falls cut back into their cliffs. It runs three times, because a main stream lowered by its own face can be another stream’s tributary.

I tried 8 m per meter first. That left 2 floating stretches and 34 shallow ones, so I went to 6.

What are the numbers?

  • Stretches more than 6 m over their bed (3 m at half size): 190 before, 0 after.
  • Stretches under 0.1 m deep: 121 before, 23 after.
  • Worst gap at half size: 65.5 m before, 2.5 m after.
  • tests/world_half_biomes: 21 of 21 pass. Before, 7 fall ends were off the ground where 5 are allowed.
  • The same seed gives the same bytes: 211 of 211 manifest hashes match. Only tables/waterways.json changed, so the playability numbers are the same: easy ground 47.3%, impassable 5.3%, 9 town sites and 27 hamlet sites.

The slot gorge works around a limit: the generator holds the ground and the water has to go into it. Letting the generator notch the held ground itself is still open.