We've got basic terrain, coastlines, and mountains - the bones of the world. But rivers make it feel alive.
Thanks to our work at the global scale, we know the amount and direction of water flowing through every tile of the map. Our task at the regional level is to work out precisely what route it takes through each tile, and to find a way to make that affect the terrain via erosion. We'll store flow volume and direction on an array for the regional map just like the global map. But where almost every cell in the global map (apart from oceans and deserts) has at least some flow on it, this will not be the case for the regional map. So this will look much more like a river map as opposed to a water drainage map.
We can start by repurposing the code from the mountains. The method we used for tracing mountain ridges - from the corners or edges of the tile, via splines, to a central junction point - will work equally well for rivers. For any given tile, there may be water entering the tile from several directions, but it exits in only one direction. All of that information comes from the source cell in the global map, of course. So UW sorts through the flows entering the tile and finds the largest. That is the "main" river of the tile. UW then draws that river, first to the central junction, and then from the central junction to the exit point. It uses splines exactly as it did for the mountains, not worrying about calculating things like meanders, which would rarely be visible at this scale (remember, each cell on the regional map is 2 km across).
There's some additional jiggery-pokery needed though, because the spline function never yields diagonals - it always draws lines that go up then across (or vice versa). We don't want that all the time for the rivers. So I add a function that goes over the drawn river and cuts some of the corners, to produce a more realistic effect.
This gives us tiles that look like this:
Like the global river map, the regional river map stores direction of flow, and flow volume in both January and July. The volume for each cell within that tile is just copied from the flow volume of the source cell on the global map.
This gives us river maps that look like this (darker blue indicates more flow):
Obviously we want the tributaries to meet up with the main rivers. This is harder than it was for mountains, though. There, it didn't matter if the ridges entering a tile all met at the same point - that wouldn't look odd (and in any case there were almost never more than three or four ridges entering the same time). Here there could easily be six or seven flows entering the same tile, and it would look very weird for them all to meet at the same point. Rivers don't do that.
So for each lesser flow that's entering the tile, we have to find a unique point for it to join up with the main flow. So we trace down the length of the already-drawn main flow for a random number of points and make that the target for the new flow. Moreover, if a flow hits another flow before reaching that point, it will end, so it's possible for one tributary to flow into another one before either reaches the main river.
That gives us this:
The flow volumes for the tributaries are taken from the source cells for the tiles that the tributaries are coming from. When a tributary hits an existing river - whether it be another tributary or the main river for this tile - it ends, and its flow is added to the existing river all the way downstream until the exit point of the tile is reached.
Now the map looks like this:
This isn't bad. Also, it's at this point that I had to go back to the global river generation and add lots and lots and lots of tweaks to the depression-filling algorithm and the flow direction calculations to try to get the rivers looking like they go in believable directions. Although I'm writing this account as though I did all the global stuff first and only then did the regional stuff, in fact they go somewhat in tandem because it's only when you can see things at the regional level, sometimes, that you can see what needs changing at the global level.
If there is precipitation in the tile a river is flowing through, it gets additional flow volume from that precipitation. At the moment, this isn't modelled very well - each river just abruptly increases in volume as it passes from one tile to another. (You can see that at some points on the map above where rivers get darker blue at sudden points.) In reality this extra volume would be delivered into the river from other tributaries which arise within the tile. So let's add those!
For each tile, the program now starts off the main river with the total flow of the previous tile that it just came from. It then calculates how much additional flow it should be receiving in the current tile, minus the amount that comes from other rivers entering the tile. This is the amount that should come from small rivers that arise within the current tile (call these springs). It creates several such springs, dividing the additional flow between them, and makes them flow until they hit an existing river. When they do, their flow is added to that river all the way downstream to the exit point of the tile, just as with other tributaries.
Now we get tiles like this:
The main river here flows from the east and leaves the tile to the west. A tributary comes in from the northeast, and other rivers can be seen passing in the northeastern and southeastern corners (remember that tiles share their corners with neighbouring tiles, so these are really rivers in those other tiles). Several springs rise in the tile and flow into the main river. This image doesn't show the relative flow sizes, which makes the map look somewhat confused, but if you examine the flow directions and sizes for each cell it makes sense.
So, skipping over the months of bug-hunting and attempts at optimising this stuff, we have a river network on the regional map that looks quite decent. The rivers wiggle pleasantly across their tiles and meet up more or less convincingly, and the flows are correctly calculated. But what about erosion? We ignored that at the global scale, but we can't at the regional scale, where the effects of fluvial erosion ought to be much more apparent. Rivers ought to erode away the land over which they pass, if they're moving swiftly. If they move slowly, they deposit material. Both of these are notoriously complicated to model.
Here's my solution. You'll notice that, in designing the river courses over each tile, UW has completely ignored the actual terrain in that tile. In general, of course, the rivers will follow the terrain downhill from tile to tile, because that's how they're calculated at the global level, but this ignores whatever local lumps and bumps have been generated within the tile at the regional level with our ever-useful diamond-square method. This is because UW actually does the rivers before it does the local terrain.
The idea is to work out the heights of just the regional cells that the rivers pass over, during the river creation phase. Within each tile, each river starts at the height of the global source cell for the tile it just came from (minus a constant, because rivers should be generally lower than the rest of the terrain in their tiles). When it leaves the tile, it is at the height of the global source cell for the current tile, minus that same constant. The program makes it drop by the appropriate amount as it passes through the tile.
This is more complicated than it might sound, because of tributaries. It may be that a tributary enters the current tile from a tile that is lower than the height of the main river at the point that it hits it. In that case, the height of the rest of the main river has to be recalculated to accommodate it, to ensure that the tributary doesn't leap uphill to join the main river. This may result in a sudden drop in level from the higher part of the main river to the lower, but that's fine - it could be a waterfall.
Anyway, once all these complications are worked out, we have a river system with each river cell assigned a height on the height map, such that the rivers flow constantly downhill. (In fact a lot of the time they flow over completely level ground, but we can live with that.) Only then do we move to the terrain creation phase, using the methods already described. Now the diamond-square algorithm is set so that it doesn't attempt to assign heights to points that already have them; and it will react to existing heights by taking them into account when assigning heights to other points. Because we subtracted a constant from the global source cell height when assigning heights to the rivers, they are lower than most of the rest of the land within the cell. Also, we set the algorithm to mostly nudge heights up when creating the terrain. The upshot of all this is that the land tends to bulge upwards around the rivers, which looks pretty much like the rivers have carved valleys through the land.
The effect can be fairly subtle on the elevation map, but you can now see rivers running through valleys, with hills rising around them:
It's also visible on the relief map, which shows only rivers above a certain flow level. You can clearly see some of the valleys created by smaller, otherwise unseen rivers:
This also has an effect on coastlines. If the river height level is lower than sea level, we get an inlet. So for example:
I think this looks pleasingly natural. In fact when I set this up originally it happened much more often. Something I've done since then (who knows what?) has raised land levels near the coasts and caused this to happen a lot less often. I'm trying to get it to be more frequent again, but that's yet another work in progress. You'd think I could just make the rivers lower, but that seems to break them in incomprehensible ways, so I'm going to have to think about it.
That aside, we're almost there. One more thing to do is to tinker with the visible width of the rivers and their associated valleys. As it stands, every river is one cell wide (i.e. 2 km). Really big rivers should be wider than this. And even more modest-sized rivers might be expected to have wider valleys.
So I add a function that goes over the tiles and increases the visible width of the rivers. Rather than monkeying about with the river array, this uses another array, so that we retain the "real", central portion of the rivers on the proper river array and use the other one simply for the extra bits that are added to make them wider. (This is much easier to do than to explain.) This is simply done by working out how wide the river should be at each point, and if it's more than 2 km, drawing a circular blob of extra river over that point. Wider valleys are equally straightforward, with the program drawing a circular blob of the appropriate height on the height map around the river cell in question.
Here are some images showing the effects. Here is a medium-sized river with an extended river valley around it, which on this visualisation is marked in green:
These green areas are forced to have the same height as the river at that point, so the effect is of a wider, flat valley floor.
A larger river has a much larger valley:
And a larger river still actually takes up more cells itself. They are marked in lavender-grey:
The centre of the river is marked in blue, showing that this is the "real" course of the river, but on the regional relief map it will appear to be three pixels wide - with, of course, an even wider valley around it. (On the river map, we still show all rivers as just one pixel wide, but with higher flows shown in darker blue, as I think that is more useful for seeing their precise courses.)
The relief map of the region we've been looking at now looks like this:
And if you look at the height map, you can see the large valleys around the major rivers:
Was that worth the many months of failures, incomprehensible errors, rivers flowing uphill, coastlines going berserk, all relived in feverish dreams night after night? Probably not, but I'm sure I'm a better person for going through it. Anyway, the next job is to get the rivers to interact properly with lakes, which is where it gets a bit tricky.
Great stuff! It seems to me that not all rivers carve valleys; maybe you could have a factor tied to location so that some areas of the world are "soft" and get deep valleys while others are "hard" and get little or no valleys?
ReplyDeleteThere kind of is, really. I have a "roughness" map - just another fractal - that varies the amount by which heights are perturbed when the regional terrain is generated. That affects the appearance of valleys, although probably not by enough.
DeleteAlso, it makes a difference where in the tile the valley actually is. If it's exactly in the middle of the valley, for example, then its value will be fed into the diamond-square algorithm at an early stage and the result will be a very long, shallow decline from the edges of the tile down to the valley. If it's one pixel away, then the algorithm won't notice it until near the end, and the result will be a much sharper descent from nearby. So although the method is the same for every river cell, the actual effect does vary a lot, from steep narrow valleys to barely discernible ones.
Ideally I'd like to vary the constant by which rivers are lowered compared to their host cells. The problem with that is: what if a river goes from a tile where the constant is large to one where it is smaller, and effectively goes uphill? A way around that is to ensure that every river system has a constant that is the same for the whole river, but have different constants for different river systems. This requires making the program able to identify different river systems, which you'd think would be straightforward, but I couldn't manage it when I tried. I may try again as it would be nice to be able to do this and get a bit more believable variety to the terrain, so thank you for making me rethink it...
I'm thinking at the moment about another tweak to it all to generate large-scale canyon systems (basically by artificially raising large areas of land around the rivers), which would add more variation still if it works.