Around The World, Part 13: Zooming in
As mentioned in the previous post, the player will never see our procedurally generated world all at a glance. They’ll be much closer to it, and seeing only a small part at any given time. Let’s see how we can go from coarse, global world maps to something that will actually look good at close range!
Here’s a map of one of our generated worlds:
The map is based on a quad sphere with six faces of 512×512 pixels. At Earth scale, that works out to about 20×20 kilometers per pixel. (I’m not certain that my world will actually be Earth-sized, because it might be too much work to fill such a big planet with enough interesting content — but I want to keep that option open for now.)
How far can the player see? It depends how high above sea level they are: the higher up you are, the farther away the horizon is. Looking at some references, ships of the Age of Sail were no taller than 25 meters. We can then use this calculator to find that the horizon is about 20 km away. Taller objects like mountains and other ships can be seen beyond the horizon, and in theory Mount Everest could be seen from as far as 361 km away if it were on the coast, but I’ll settle for 50 km as an upper bound.
That means that the player could see about 2.5 “world pixels” in any direction — at best:
Clearly, the current world resolution is not quite enough!
Making a mesh of things
I haven’t settled on any particular art style yet, but it’s a safe bet that we’ll need a 3D mesh of the terrain. And if I settle on a low-poly style, it would be nice if that mesh looked good even if you can distinguish individual triangles.
The quad sphere is great for the generative algorithms and simulations I’ve been doing so far, because it lets us pretend that we’re working with squares. But a mesh that the GPU can render is made up of triangles instead. We can split each square into two triangles, but they would not be nice equilateral triangles, so the mesh would not look so beautiful.
Now it’s time to remember what I wrote previously:
But the subdivided icosahedron might make a comeback once we start building meshes!
Indeed, an icosahedron is made of 20 equilateral triangles, and we can subdivide each of these triangles recursively into smaller triangles to get an approximate sphere with almost exactly equilateral triangles:
Source: Wikimedia Commons by Tomruen, CC-BY-SA 4.0
GPU’s don’t much like individual triangles though; they like triangles in large batches. So instead of subdividing each face of the icosahedron into 4 triangles, we subdivide it into 64×64 triangles. At each corner, we interpolate the original quad sphere height map to get the height at that corner. This gives us our first approximation of a terrain mesh:
Clearly this is no better than what we had before. But the beauty of it is, that we can subdivide each face into 4 smaller triangles, and generate a 64×64 triangle mesh for each of those. And we can repeat this procedure indefinitely to get finer and finer meshes, as needed.
If we did that for the whole world, it would take far too much memory and computing power. So we generate meshes only for the part of the world that’s visible, at the proper subdivision level to make it look detailed enough. No matter how far we zoom in, the number of visible triangles, and therefore the amount of work that needs to be done, is more or less the same. Nice!
Now those chunky pixels from before resolve into something much smoother:
Whoops, that’s a little too smooth. That makes sense, because the original large-scale world map simply doesn’t have this amount of detail.
Adding fine details
What we need here is our trusty friend simplex noise to add some smaller features. Remember that this noise is added during the generation of meshes, so it doesn’t need to be computed for the whole world. The drawback is that we can’t easily use any fancy algorithms like erosion simulation, which require information from nearby points; each mesh vertex must be computable independently of all the others. So at this scale level, we’re limited to relatively simple algorithms.
The noise is scaled such that the largest (lowest frequency) octave is roughly the size of one pixel in the global world map. That gives us this:
That’s looking better already, but most coastlines end up looking like this instead:
Unlike the steep volcanic island we looked at above, this is a coastline with a much shallower slope, where the noise adds way too many small islands and inlets. This is because the noise amplitude is the same everywhere, but what looks good on mountains is way too rough for lowlands. In real life though, not all terrain is equally rough.
Modulating the noise
What we need is a system that adds more noise in areas where we’d expect rough terrain, such as mountainous areas, and less noise in smoother terrain like lowlands. We already have a system to add height based on tectonic forces, which is how we create mountain ranges, so let’s bolt it onto that. Let’s also add noise to hotspot volcanoes. Our previously messy coastline now looks much smoother and resolves into an isthmus:
Meanwhile, our hotspot volcano remains pleasingly rough:
The noise itself could be improved, though. Right now it’s just 4 octaves of simplex noise layered on top of each other. While working on the large-scale terrain, I noticed that ridge noise looks much better: the valleys between the ridges almost look like rivers, resembling the effect of hydraulic erosion (something we don’t simulate). Seen here at an angle to better show the relief:
Here I also lowered the frequency (increased the scale) of the noise to span several pixels in the world map. This seems to be better for tying those disparate pixels together.
On mountain ranges, the noise looks perhaps a bit too uniform:
Warping the domain of the ridge noise using three octaves of yet another simplex noise fixes this nicely, making the result look pleasingly irregular:
As you can see in the corners, there’s also something wrong with my calculation of the visible area. We seem to be missing some terrain meshes here. I’ll fix that later.
To wrap up this post, let’s add a simple water surface. The same subdivision algorithm is used as for the land, but I just apply a height of zero everywhere, and use a different material for rendering. When a triangle is entirely covered by land, we don’t need to generate any water mesh and vice versa, but if a triangle is only partly covered by water, we need to render both so we get nice sharp coastlines.
Some pretty islands:
An interesting-looking bay:
It’s obvious that more work is needed. For example, you can still see some artifacts of the underlying square grid, and the coastlines are much too smooth here. But we’re getting there!