Around The World, Part 24: Local terrain
Now that we have a plausible looking height map for the entire world, it’s time to zoom in and see what the terrain looks like up close.
I did this before, when I was still working on a sphere. I started out this time using the same approach, but the implementation I ended up with this time is quite different.
Let’s start by simply interpolating the global height map using bilinear interpolation:
The overall shape is plausible, but the terrain is way too smooth and obviously made of squares. I could fix the squares by using a more advanced interpolation method, but that would only make the smoothness worse. So I turned to my usual solution…
More noise
Adding some octaves of simplex noise gives a much better result:
This is the minimum amount of noise necessary to hide the sharp creases at the cell edges. However, there is a problem. Consider this much flatter terrain:
What happens if we add simplex noise to this?
Whoops! That doesn’t look realistic, and it’s also not going to be fun to navigate. The problem is that the simplex noise is now too strong, compared to the low height of the terrain.
I tried several different approaches to solve this:
Compute the slope of the global height map, and using that to modulate the amplitude of the local noise. Steeper regions get more noise, flatter regions get less. It worked, to some extent, but the effect was hard to control.
If the input height before noise is close to zero, modulate the noise to be close to zero as well. This had the unwanted effect of keeping the coastlines exactly straight, as in the situation without any noise.
Compute the distance to the nearest coastline, then modulate the noise based on that. The problem with that is, that the nearest coastline could be far away in some ungenerated chunk, and we’d still end up creating tons of islands in shallow water.
So none of these approaches was satisfactory.
Prelude to ports
The worst problem is that the simplex noise would arbitrarily move, add or remove coastlines. This is going to be a problem once we start placing port cities for the player to visit. In the global map, we classify each cell as land
, sea
or coast
based on the terrain height at the four corners:
- If all corners are above sea level, the cell is
land
. - If all corners are below sea level, the cell is
sea
. - If some corners are above sea level and some are below, the cell is
coast
.
Using orange for land
, dark blue for sea
and light blue for coast
, the map around the above area looks like this:
Ports will be placed on this map only in coast
grid cells, which should make them accessible from the sea. However, if simplex noise raises that cell fully above sea level or lowers it fully below, where will the port go? Or what if the noise creates a land barrier in the sea surrounding the port, making it inaccessible?
I initially tried to work around this by generating some extra local terrain around the current chunk, and creating the port in a suitable location once all surrounding local terrain was available. My use of the LayerProcGen algorithm made this pretty easy! Again it worked, to some extent, but it remained impossible to give any hard guarantees. On sufficiently tricky terrain, there would still be cases where no coastline could be found for the port to be placed, and it would end up landlocked or floating. (A city on water could still be a cool feature every once in a while; consider Venice. But a landlocked city being picked as a quest destination would be distinctly uncool.)
What I really needed was a way to guarantee that a coast
cell in the global map would remain a coast
cell in the local map, with a coastline bordering on the sea. It took me way too long to realize that this would happen automatically if I kept the height of all four corners the same as in the global map, and only filled in the intermediate terrain. Ideally, land
should also remain above sea level and sea
should remain below. And I know just the algorithm for that! It’s fast, flexible, simple to implement, and predates simplex noise by about two decades! Enter…
Diamond-square
The diamond-square algorithm is explained in many places around the web, so I’ll only give a brief summary.
- We start with an array (height map) in which only the corner points have a known height. These points form a single square.
- For each square, fill in the center value as the average of its four corner values, plus some random value. This produces diamonds, and is therefore called the “diamond step”.
- For each diamond, fill in the center value as the average of its four corner values, plus some random value. This produces squares again, and is therefore called the “square step”.
An illustration should make this clearer:
Source: Christopher Ewin via Wikimedia Commons, CC-BY-SA 4.0
(I think the naming is confusing. The step operating on squares should be called the “square step”, and the step operating on diamonds should be called the “diamond step”. Oh well.)
On the edges, we only get half diamonds (i.e. triangles); the fourth corner is outside of the initial square. We can deal with those using LayerProcGen again: generate the neighbouring chunks as well, then read from those.
Let’s see how this looks when applied to our steep coast:
Okay, it’s rough now, and the creases are gone. What happens to the flatter coast?
Well, it’s rough now too — but still a mess of islands, no better than our previous approach! What gives?
Random tweaking
The key is in the part “some random value” that I handwaved in the algorithm description. The terrains above were produced with uniformly random values between -100 and +100 meters, which is halved on each iteration of the algorithm. (It’s actually multiplied by √½ after the diamond step, and again by √½ after the square step.) This means the amplitude of the noise is the same on rough terrain as on smooth terrain, so we haven’t really gained much yet, compared to simplex noise.
However, we now have a convenient place to tweak the randomness dependent on the input. For instance, we could pick a random value uniformly between the minimum and the maximum of the height at the four surrounding points. This way, we could never get any local maxima or minima where previously there were none.
In my case, I chose to compute the standard deviation of the surrounding four heights, and express the randomization strength relative to that standard deviation. For example, if the four corners are at heights 1.0, 2.0, 3.0 and 4.0, then the average is 2.5 and the (biased) standard deviation is about 1.12. Setting the strength factor to 1.0 would result in a center point between 1.38 and 3.62, which is safely in between the minimum and maximum. If the surrounding corners are at heights 1.0, 1.0, 1.0, 4.0 instead, then the central point would be between 0.45 and 3.05; you can see it shifts down along with the average, and it can go below the minimum and above the maximum.
How does this look when applied to the previous terrain?
I think this already looks far more interesting and realistic than the uniformly noisy previous attempt. The terrain is much more varied; there are clearly delineated steeper and flatter areas now. But the most important test of this algorithm: how does it fare on flat terrain?
Marvellous! Notice how we are getting only small bumps in the terrain now, and how the coastline has become more jagged and interesting, but the overall shape of the coastline has been preserved.
There is one thing that still bothers me, though. All coast
cells are guaranteed to have some combination of land and water, but land
cells aren’t guaranteed to contain only land, and sea
cells aren’t guaranteed to contain only sea. In some cases, this could still cause land to form in sea
cells off the coast, blocking access to the port. Ideally, I’d like to keep all terrain in sea
cells below sea level, and in all land
cells above sea level. Fortunately, that’s now very easy to do: if all four corners are below sea level, limit the range of allowed random values to be below sea level as well, and vice versa. It didn’t visibly change the test terrains we were looking at, but I’ll trust that it does the right thing.
All this together gives me an ironclad guarantee that coast cells will always contain a suitable spot to place a port, and the port will always be accessible from the sea!
Future work
Instead of a uniformly random height change, it would be interesting to experiment with other random distributions, such as the normal (Gaussian) distribution. I’m putting that on the backlog because it’s not a priority right now, and easy to change in isolation later.
Since I have an erosion algorithm, it would be cool to apply that after diamond-square, to make the result more believable. I have actually gotten that to work, and it looks a bit better. The trouble is that erosion messes up all guarantees about coast/land/sea cells again. I might be able to fix that by locally raising or lowering the terrain after erosion, to restore those guarantees… but that’s something to tinker with later. For now, I’m keeping erosion turned off.
For the next post, after all this talk about preparing the terrain for placing ports… let’s place some ports!