Wednesday, February 6, 2019

Zooming in: the promise and problems of regional scale

We've reached a stage where Undiscovered Worlds can create worlds at the global scale, but that's not really the heart of the program. I always planned for the zoomed-in, regional level of detail to be the main point of interest.

My thinking was in part inspired by No Man's Sky, which procedurally generates planetary terrain during play. You can fly over immense landscapes that just keep on coming, with the details created as they're rendered - but in such a way that if you go back again you'll see the same things as before. But a problem with No Man's Sky is that it lacks large-scale realistic features such as mountain ranges and rivers. It made me think: what if you made a fairly basic world map first, with large-scale features of that kind, and then based the terrain on that, expanding it as you go? That would keep the idea of procedural generation on the fly, allowing for a huge explorable terrain, but it would give some structure to that terrain.

So that's the idea behind the regional scale of Undiscovered Worlds. And in fact it's become another example of the principle that combining different terrain generation methods produces more interesting results than using just one. The terrain that appears at the regional level of UW is the result of a combination of the large-scale terrain of the global map with different terrain generation methods at the regional level. The global scale provides the broad shape of everything, but new details are added using other techniques.

So it works like this. UW creates a new global map (or loads one in). The user can click on different points of the map and get basic information about that point: elevation, rainfall, climate, etc. If they like, they can press a key and UW will generate a regional map based on an area of 34x34 pixels of the global map, centred on that point. Regional maps are not discrete tiles of the global map - if the user had clicked a point just a couple of pixels away, the map would have been centred on that point. But each pixel of the global map is always expanded in the same way in the regional map. You could in theory scroll over the global map, from pixel to pixel, generating a smoothly scrolling regional map, although it would be horribly slow.

Each of those pixels in the 34x34 grid of the global map is turned into an area of 16x16 pixels in the regional map. The regional map is effectively divided into 34x34 tiles of 16x16 pixels each. (The tiles share their sides with each other though to ensure smoothness.) And the regional map is created by going through each of those tiles in turn and generating its terrain - and then going through them all in turn and generating their temperature, or precipitation, or whatever.

There are several challenges to this. The most important one is:

How to make sure that each tile is always exactly the same, no matter where it may be?

Consider the following zoomed-in section of the world map we had UW make in the previous few posts:


This is 34x34 pixels, so each of these little blocks will become a tile of 16x16 pixels on the regional map. Consider any one of these - say the dark green one in the middle of the south coast of that island. It's surrounded by eight other blocks. In the regional map, these will all have to become 16x16 tiles that make sense next to each other - e.g. if one of them has a coastline in it, that coastline must meet up with the coastline in the neighbouring tiles. And of course the same goes for rivers, mountain ranges, and everything. So it looks like, in generating one tile, the program must take into account the tiles next to it. But now suppose the user leaves and goes back to the global map, and then zooms in on a slightly different area:


That block is now on the edge of this area. So it's missing three of the neighbours it had before. So how can the program know, when generating the tile based on this tile, what's in the neighbouring tiles? It won't have as much information available to generate it. Yet it must generate it so it's identical to how it appears in the other map. That's essential if we're to maintain the illusion of a huge, persistent world - that will be ruined if features appear different every time the user zooms in on the map.

So that yields a basic principle of the regional map: each 16x16 tile must, as far as possible, be autonomous. It must be able to be generated in isolation from all others, so that it always appears the same no matter what other tiles appear around it.

In practice, this hasn't been possible. Some features in tiles inevitably spill over into neighbouring tiles. For example, mountain peaks must sometimes straddle tile boundaries, otherwise there would be long straight valleys going through the ranges at those boundaries. So I've had to bend that basic principle on occasion. I've got around that by doing two things. First, such incursions from neighbouring tiles can only affect directly neighbouring tiles - they can't affect anything two tiles along or more. Second, I make it so that although an area of 34x34 tiles is generated, the tiles along the edges of that area are never displayed. They will be lacking any elements that would have spilled over into them from the next tiles along, that haven't been generated. So they might have the wrong appearance - so even though they are generated (in case they affect the ones we can see), they are not shown and we'll just pretend they're not there. In fact, I also had difficulties generating terrain in tiles on the left- and right-hand sides of the regional map, which I solved in a similar way. So in the event, the displayed regional map is actually 30x30 tiles.

All of this leads to the second challenge, which is this:

How to avoid making the map look like it's made of tiles?

I want the maps to look natural, like they are of real places, as far as possible. But the fact that the maps are actually based on expanded blocks means they have an inherent tendency to look blocky, with lots of straight lines and right angles, and that spoils the illusion.

To illustrate, this is what the heightmap of the island in that zoomed-in section above looks like in the regional map, if we simply take all of the heights in each tile directly from the corresponding points in the global map:


That's the starting point for the regional map. Somehow we have to make it so that blocky coastline becomes a smooth, convincing line which obscures the original blocks from which it's actually constructed. The straight edges have to vanish - for example, consider the line of three blocks in a row on the southern coast, or the line of four in a row on the opposite coast, or the vertical line of eight on the far west. We have to find ways of drawing coastlines that follow this rough shape without revealing those straight lines, and what's more we have to do in such a way that respects (as far as possible) the basic principle of making each tile self-sufficient. And of course what goes for coastlines goes for everything else: the gradients of the terrain inland, the shapes of the mountains and rivers, the temperature map, and so on.

Not an easy task! And it's a constant work in progress. But for reference, here is how UW, in its current form, handles that island:


It's not perfect, but we're getting there. So in the next few posts I'll try to explain how it's done.

3 comments:

  1. I'm interested to what you think of this: https://forhinhexes.blogspot.com/2018/10/one-mile-hexes.html

    (it certainly feels like I'm dumping all my links on you, haha. Just happy to have another brain working on these types of problems!)

    It's a very interesting problem, and I abandoned it (well, put it on the backburner) because it was so difficult to get the river flow to be continuous. I like your idea of only affecting neighbors - my system has to look at the entire drainage area of the target hex in order to correctly place the river.

    So I'm very interested in this project!

    ReplyDelete
  2. Hey, the more links the merrier. I've not yet read all your blog - there's so much of it! - so it's good to be directed to such things.

    I can see why you ran out of steam on that aspect of it, because what you're trying to do there is really difficult. You're making the terrain first and then running the river over it, and of course if you do that then (a) you have to sort out depressions and the like, and (b) it's difficult to make the different tiles match up.

    My method is the reverse: I do the rivers first, and then the terrain afterwards. So for each tile, I first find the river entry and exit points. These are determined by the RNG using a seed unique to the tile. Then I draw suitably wiggly lines between these points. Then I go over them and assign height values to the points where the rivers are, making sure that they go nicely downhill from the entry points to the exit point. Only after all that is done do I generate the rest of the terrain, around the rivers. This is done in a way that makes it tend to rise higher than the existing heights where the rivers are. That results in the rivers tending to be in valleys that look like they've been carved by water erosion.

    This is quite an efficient method compared to doing it the more natural way, and I think it gives quite decent results. Not having to worry about the terrain while doing the route of the river also means you don't need to try to force your terrain to make the river flow naturally to a particular exit point in order to meet up with the entry point on the next tile. You can just draw the river to go to that point, and then force the terrain to rise up around it, which is much easier.

    So I'll describe how I do terrain in the next couple of posts first, but then get on to rivers and explain how they are actually done first. Of course when I did all this I worked out the terrain generation first and then worked on the river functions, but I always anticipated that the river functions would precede the terrain generation functions.

    I hope that makes some kind of sense!

    ReplyDelete
    Replies
    1. That's a very good approach. I might have to revisit it again, armed with new knowledge, haha.

      Delete