Thursday, February 28, 2019

Tinkering with rivers and lakes

So far in this blog I've been describing stuff I've already done - in some cases so long ago that I'm rather hazy about what exactly I did. Time for something a bit more up-to-date, which might illustrate some of the kinds of problems I've been facing with this project, and particularly with the dreaded hydrological models.

I previously drew attention to a flaw with this image:


That blob on the river that flows southeast into the southern lake is a glitch. Here, the river suddenly plunges hundreds of metres down, to be just 1m above sea level - even though it then joins a river that has much higher elevation, and flows into a lake of higher elevation still. This is not a good thing (and I have seen it before elsewhere), so I thought I'd try to deal with it. What's causing it and how can we tackle it?

A look at an expanded visualisation of the tile in which the error occurs shows us in more detail what's going on. This is a height map with the rivers laid over it:


Here we have one main river with two tributaries. The main river enters the tile from the north, and flows out to the southeast. One tributary enters from the northwest, and the other from the west. The problem is associated with the second of these tributaries. At the point where it enters the tile, it's 800m above sea level. One point to the east, it's 140m above sea level. Then it turns northeast and is 1m above sea level, and the whole of the rest of the river remains at that height for the rest of the tile, only to join a lake whose surface is 803m above sea level in the next tile. You can see the dramatic drop in elevation near the junction of the rivers, where the terrain is shaded darker.

UW calculates the course of the main river in any given tile first. Then, it goes over that river again and works out the elevation of each point. It does this by establishing the elevation of the point at which it enters the tile, the elevation of the point at which it leaves the tile, and then simply steps the elevation down over the course of the river between those two points. Having done that, it then calculates each tributary in turn - first its course and then its elevation. When calculating the course of a tributary, UW stops when it reaches either that tributary's target point on the main river or another river cell (which could be part of another tributary, so tributaries could join up with each other before heading for the main river). It takes the elevation of this point - where the tributary first hits another river cell - to be the final elevation of the tributary, and works out its elevation along its length accordingly.

So what happened here? There's something wrong with Tributary 1 (the one that comes in from the northwest). UW doesn't work out any elevation for it in the river-carving phase. Instead, it just gets normal elevation worked out later on in the terrain-generating phase, for reasons as yet undetermined. This leads to two problems. First, it doesn't flow downhill all the way to the main river - instead it flows sometimes downhill and sometimes uphill, following the normal random variation of terrain. That's bad enough. But worse, this means that at the time when Tributary 2 is calculated, Tributary 1 has no assigned elevation at all. (Remember, the rivers are all done first, and only then is the rest of the terrain filled in around them; so if Tributary 1 doesn't get its elevation sorted in the river phase, it has to wait until the terrain phase to gets its elevation assigned.) That means that when Tributary 2 hits Tributary 1, the cell it's joining has no elevation - or, more accurately, its elevation is 0, since that's the default. As a result, when Tributary 2's elevation is calculated, it thinks it's trying to flow downhill to an elevation of 0, which is the bottom of the sea. So the tributary plunges down. The program only allows large rivers to carve below sea level (this is to stop enormously long inlets occurring for minor rivers), so it won't allow the river to drop below 1m above sea level. It does, however, recognise that this is lower than the elevation of the main river that the tributary then joins, so it drops the elevation of that main river to match it. The result is a river that suddenly drops to near-sea level, which looks wrong in itself and screws everything up in the downstream tiles.

Tributary 2's problem is relatively easy to fix now we know what's causing it. I can add a function to the tributary elevation calculations which recognises if it's trying to flow down to an elevation of 0, and responds by tracing the course of the tributary until it reaches a river cell that has a higher elevation than 0, and making that its target instead. Now Tributary 2 works properly, and flows in a reasonably believable way to its junction with Tributary 1 and then into the main river without any sudden chasms:


Now Tributary 2 enters the tile at 800m and remains at that elevation until it reaches the highlighted cell, at which point it drops to 798m, which is the height of the main river. It stays at that height for the rest of the tile. Much better, and no weird artefacts:


Now if our aim were just to create that map image, we might leave it at that, because there's nothing obviously wrong with that map. You can't see on it that Tributary 1 sometimes goes uphill. But I don't want to leave it at that, because (a) I'd like to add a closer level of detail in the future, where any problems with the regional level would become much more apparent, just as many issues with the global map have become clear only when working on the regional map level; and (b) I'm a pathological perfectionist.

So why did Tributary 1 never get its elevation sorted in the river-carving phase, which was the underlying cause of this problem? It doesn't make much sense, because UW does the elevation of a tributary immediately after drawing the tributary itself. This tributary does get drawn, so why does it not then get its elevation done?

Some investigation reveals that the point at which the tributary joins the main river is actually slightly higher than the tributary's starting point. When this happens, the program is supposed to recognise it and calculate instead the drop from the tributary's starting point all the way to the exit point of the main river. Then it should work out the elevation of the whole river from the tributary's entry point to the whole river's exit point - in other words, recalculating the elevation of the main river from the point at which the tributary joins it. This ought to ensure that the tributary flows into the main river without going uphill. (And this was why, originally, Tributary 2 caused the whole river to be plunged down to 1m above sea level.) So this is what ought to happen here. Why then is it not happening?

Further investigation leads us deeper down the rabbit hole. The program is indeed calling the function that's supposed to calculate the tributary's elevation, and it's trying to recalculate the elevation of the whole river. But it's failing. Why? It thinks that the exit point of the main river is actually higher than the entry point of the tributary, so it's quitting the function without doing the elevations.

This initially puzzled me, because the entry point of the tributary is 814m and the exit point of the main river is 798m. But then I remembered: of course, that 814m at the entry point is the value that the terrain generating phase has assigned to that point after the rivers have been done (because, for reasons as yet unknown, this tributary hasn't got its elevation sorted before that point). The tributary-elevation function is getting the elevation at the entry point not by looking at the height map (because there isn't one yet) but by calculating what the elevation at that point should be. And this is done simply: it is calculated like this:

height = ( (elevation of source cell for this tile - constant) + (elevation of source cell for upstream tile - constant) ) / 2

The constant in that equation is used throughout river elevation calculations to ensure that rivers are lower than most of the other terrain around them. The higher that constant, the deeper the valleys carved by rivers.

So what this tells us is: the entry elevation of the tributary, as calculated by that formula, is too low. It's lower than the exit elevation of the whole river (which is also calculated by that formula, but with reference to the current source cell and the next source cell downstream). The program is therefore unable to calculate any elevation for the tributary and it just leaves it. The terrain generation phase then puts elevations under it as part of its normal terrain generation, including an elevation at its entry point that happens to be higher than the exit point for the whole river, leading to this baffling situation.

So now we've traced this whole horrible problem back to a single cause: the elevation of the tributary entering this tile is lower than the elevation of the river leaving this tile. Why?

Further investigation reveals the reason. The elevation of the source cell for the upstream tile (where the tributary is coming from) is 818m. The elevation of the source cell for the current tile is 814m. That seems fine. But the elevation of the source cell for the next tile downstream is 823m! That is why the elevation of the exit point for the river in our tile is too high, and that is what's causing all the trouble.

So then, the problem lies with the global map. Somehow, the river is flowing uphill, from a cell on the global map to a higher cell on that map. Now this should be impossible, because the global water flow is calculated (a) simply by marking on each cell the direction to its lowest neighbour, and (b) then tinkering with this to create a more natural-looking network, but only by sometimes redirecting flow to a lower neighbour other than the lowest one. There should never be a case where flow goes to a higher neighbour (because the Planchon-Darboux algorithm means that every non-coastal cell always has at least one neighbouring cell that is lower than it). So how has this happened?

I think the answer lies with the fact that the downstream tile - which is too high - borders a lake. In fact the lake spills over onto this tile:


This is how lakes work at the regional level: they are drawn on tiles whose source cells are either lake cells in the global map or next to lake cells in the global map.

And part of the function that draws lakes at the global scale checks that all the cells bordering them are at least as high as the surface level of the lake in question. That must be what's happened here. Originally, this cell was lower. When the lake was created, UW raised this cell so that it wasn't lower than the lake that it's next to (otherwise it would be weird - why doesn't the lake spill out?). But of course it didn't recalculate the water flow that had already been calculated over this cell. The result: water flowing uphill. And this had the horribly long and convoluted knock-on effect that we finally saw with the baffling phenomenon we started with, of the rivers at regional level inexplicably plunging down to sea level, carving great canyons, and then leaping back up impossibly. All caused by an issue with lakes - well, it would be, wouldn't it? I really hate lakes.

So what can we do about this? The extra-annoying thing is that I did anticipate this problem when writing the lake function (though I certainly didn't anticipate that it would manifest in this way), and I included a routine that's supposed to check around lake coasts and ensure that rivers going in and out do so properly without having to go uphill. Evidently something went wrong with that routine. I think that, surprisingly, the best solution is simply to remove that routine and every related element of the lake function that tinkers with the heights of neighbouring cells, and allow cells that border lakes to be lower than the surface level if they want to be. That sounds wrong, but we have to remember that at the global level, each cell is 32km across. It's perfectly possible that the average height of that cell could be lower than the surface level of the lake that it borders, but the bits that are actually next to the lake are still higher.

Doing this resolves the river problem. The source cells for our three tiles are now 818m, 814m, and 808m respectively. That means that the elevations of the entry/exit points all make sense, and the program is able to calculate the elevations of all rivers, including tributaries, correctly.

But there's another problem...

The tile where the river goes into the lake now looks like this:


Nothing wrong with that (the river's a bit straight, but we can live with that). Except that something's off with the elevation here too. The last point of the river in the previous tile was 794m above sea level. The first point of the river in this tile is 795m. So it's gone up a metre. It then stays at exactly this height until it hits the lake, which has a surface level of 803m. So it somehow climbs eight metres. What's happening now?

Without investigating too closely, I think that the problem here lies with the global lake generation as well. When the area of a lake is calculated, the program takes into account the heights of neighbouring cells; but it doesn't take into account the heights that rivers in the neighbouring cells would have (which are lower, for reasons already mentioned). So it doesn't check to see whether a river flowing into the lake would be lower than the surface level of that lake, at least not while it's actually making the lake. (As mentioned already, I did have a routine that tried to deal with that after the lake's area had been determined, but it clearly didn't work properly.) The solution, I think, is to tinker with the lake creation function itself. Lakes are made by picking a seed point on a river, and then extending them over neighbouring cells. They're set so that they can't extend over cells that are lower than the surface height, but I can try changing it so that they can't extend over cells that are lower than the elevation of the seed point, or that have rivers flowing into them that are lower than that elevation either. That way we should avoid having too-low rivers flowing into the lake.

Well, one result of that is that the lake is rather drastically reduced:


I'm just going to have to take it on faith that that resolved the problem with any other lakes that would have had this same problem, because I can't examine this case any more - the river we've been looking at doesn't even flow into the lake now. Instead it flows beautifully and plausibly into that larger river, not going uphill at all. Meanwhile, there aren't any issues with the river that does flow into the diminished lake from the southwest - at least until it hits the lake itself, at which point it jumps from 799m above sea level to a lake surface level of 803m. Argh! This is a different issue though, as the overall tile heights are working fine. Further tinkering to the lake creation function is needed: now it won't extend the area of the lake over cells that are lower than the surface area minus the constant that is subtracted from river elevations.

Well, I tried that, but it made no difference at all! Perhaps this problem lies in the way that the program calculates the river elevations at regional level once again. If the source cell of the next tile downstream is lake, then it takes the surface level of the lake to be the target elevation of the river at its exit point from the current tile. Otherwise it uses the formula given above. But what if the next tile downstream is technically next to a lake? The lake might still spill into it, and that would be where the river meets the lake. So I change the calculation: now it checks to see if the next tile downstream is next to a lake, and if so aims to drop the river to the surface level of that lake.


It still makes no difference. The main river comes in from the southwest there, at 799m, and stays that height until it flows into the lake, at 803m. Not only that, but there's a problem with the tributary coming in from the west too. It's not got its elevation done properly, just like Tributary 1 in the original tile. As a result, it starts at 800m and just goes randomly up and down until it flows into the main river. It's because it thinks, for some reason, that it should be starting at a lower elevation, which is lower than the elevation of the main river and therefore abandons the attempt. I don't know why this is, but it's something to do with the lake, because if I regenerate the world without lakes at all the river at this point (and indeed everywhere nearby) behaves itself perfectly.

Well, it's all in a very slightly better position than it was this morning. But as you can see, this thing is always two steps forward, one and eight-ninths steps backwards. I really hate lakes.

No comments:

Post a Comment