5
\$\begingroup\$

I want to generate a procedural world using perlin noise. As expected, my terrain is generating with a repetitive patern.enter image description here I'd like to add biomes, zones separated with higher or lower amplitude to simulate mountains or plain. I first thought I could randomly select rectangles of verticesof random sizes and then give them bigger amplitude to simulate mountains. The problem with that is that It wont seem natural, since I will only have rectangle biomes and also I don't know how to make it so the mountains would connect with the plain to avoid things like thisenter image description here

Here is my script:

public int size = 10;
public float amplitude;
public float frequence;
void GenerateTerrain(float amp, float freq)
{
    Mesh meshy;

    MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
    meshRenderer.sharedMaterial = new Material(Shader.Find("Standard"));
    MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();

    List<Vector2> uv = new List<Vector2>();
    Vector3[] vert = new Vector3[size * size];
    List<int> tri = new List<int>();
    List<Vector3> norm = new List<Vector3>();
    meshy = new Mesh();


    for (int i = 0, x = 0; x < size; x++){
        for(int z = 0; z < size; z++){
            float y = Mathf.PerlinNoise(x * amp, z * amp) * freq;
            vert[i] = new Vector3(x, y, z);
            i++;}}


    for (int i = 0; i < vert.Length; i++){
        norm.Add(-Vector3.forward);}


    for (int x = 0; x < size - 1; x++){
        for (int z = 0; z < size - 1; z++){
            int i = size * x + z;

            tri.Add(i);
            tri.Add(i + 1);
            tri.Add(i + size);
            tri.Add(i + size + 1);
            tri.Add(i + size);
            tri.Add(i + 1);}}


    for (int i = 0; i < vert.Length; i++){
        uv.Add(new Vector2(vert[i].x, vert[i].z));}

    meshy.vertices = vert;
    meshy.triangles = tri.ToArray();
    meshy.uv = uv.ToArray();
    meshy.normals = norm.ToArray();
    meshFilter.mesh = meshy;
}

void Update()
{
    if (Input.GetKeyDown("u"))
        GenerateTerrain(frequence, amplitude);
}
\$\endgroup\$
1
  • \$\begingroup\$ Also have a look at libnoise (libnoise.sourceforge.net) that solves your problems with tiling and repeatedness, even for planetary size terrain (if there's enough processing power and storage capacity). \$\endgroup\$
    – user144188
    Commented Nov 1, 2020 at 16:35

1 Answer 1

16
\$\begingroup\$

There are three main steps here:

  1. Use some method to assign biomes to regions (this is the hard part, with multiple strategies I'll break down shortly)

  2. For each point in your mesh or tile/node in your world, determine which biome it's in, as well as which neighbouring biomes it's close to. Compute an interpolation weight representing the influence of each nearby biome.

  3. Evaluate your terrain generation logic for each of the nearby biomes separately. (In practice, this means you've computed 3-4 possible heights or other characteristics for this point/node) The final height for this location is a weighted average of the results from the nearby biomes, according to their influence.

It might seem wasteful to compute multiple full biome results for a single point, but this is a necessary evil if you want to get sensible blending between them. If you try to interpolate the generation input parameters alone (particularly the noise frequency or number of octaves) and generate just one height result based on the blended parameters, you'll get non-sensical looking blends with nasty artifacts, especially when far from the origin. You also limit how complex and varied your terrain generators can be, since they all have to use the same rules.


There are lots of different strategies for step 1. I'll classify the main families as "zoned" and "emergent".

Strategy 1: Zoned

For this strategy, we start by dividing our world into a collection of zones.

A popular method for this is using a Voronoi diagram (also sometimes referred to as a Worley noise basis in procedural generation contexts), where we pseudorandomly scatter points across our map. The set of all locations closer to this point than any other point forms one zone, which is guaranteed to be a convex polygon. This gives us a nice structured region to work with that's still a little more organic than strict rectangles:

Typical Voronoi pattern

(See also this Red Blob Games article for some discussion of how we can tweak the standard Voronoi approach to something we might prefer for world generation)

You can get arbitrarily more complex with your region selection, of course. Maybe applying domain warping to make the borders between regions more organic, etc. Though I recommend starting simple - most of the hard geometric borders will be hidden by the time we do our blending/interpolation, so they might not be a problem.

Next, we assign a biome to each zone.

We could do this randomly, using the zone location/ID to seed a weighted random lookup into a table of biomes. The risk with this is that adjacent biomes make their determinations separately, and so you can get combinations of adjacent zones that don't follow any coherent geographical logic - like a lush jungle completely surrounded by desert, with no fresh water nearby or flowing through it.

We could also use rule-based zone assignments. Maybe we randomly assign a certain fraction of the zones to be water - lakes, seas or oceans. Then we designate any non-water zone adjacent to a water zone must be a beach, cliff, or marsh. Forests and jungles can appear 2 zones away from the nearest water, and mountains only 3+ zones away. You could accomplish this with cellular automata or other adjacency-based rules.

Then for step 2, we can compute where our point-to-be-generated sits relative to its nearest neighbouring zones, and use that to compute the interpolation weights to use for each biome's result. One way we can do that is to compute a Delaunay triangulation of the seed points we used to create our zones. This is the dual of the Voronoi diagram, where each triangle represents a junction between three adjacent zones. We can then use the barycentric coordinates of our point-to-be-generated within this triangle to compute the weight to give to each of the three nearest neighbouring zones.

Delaunay triangles on a Voronoi diagram (Diagram showing the relationship of Voronoi cells and Delaunay triangles from here)

Strategy 2: Emergent

Here we try to model/approximate some of the real-world processes that give rise to different biomes, as a way to get more natural / logical relationships between the different structures. The biomes then "emerge" as a consequence of the simulated natural processes that shape your terrain.

Th first step is to generate the underlying drivers of biome formation in our model, like temperature and moisture, to pick a popular pair of inputs. We could generate a moisture map and a temperature map as two separate, low-frequency Perlin noise maps, for example, though going this route these won't have any particular relationship to landforms or location on your map/planet.

Another route is to generate your broad-scale elevation up-front - say the first couple octaves of your terrain height function. This isn't biome-level detail yet, more like the underlying bedrock under the biomes. Some folks will even generate tectonic plates and simulate their motion to find where there should be mountain ranges formed by two plates colliding, etc.

Once you have this coarse-grained elevation detail (and correspondingly, the locations of oceans/seas based which areas are below sea level), you can generate other metrics like temperature and moisture from that. Say, the temperature gets colder as you move higher in elevation or away from the equator in latitude. You can model the moisture based on proximity to an ocean or sea, or the prevailing winds: if winds at a particular latitude run mostly west-to-east, then areas on the east side of a mountain range will tend to be wetter, and areas on the west side dryer, as the mountain wrings the rain out of the air as it passes.

Now you have, for each site in your map, a collection of input values like latitude, elevation, moisture, and temperature. You can use these as coordinates to look up into a biome assignment map.

Here's an example diagram from Navarras on Wikipedia, showing how real-world biomes relate to precipitation (moisture) and temperature:

Biome map

A game might use a more abstracted version, like this example from an older version of Minecraft, via the Minecraft wiki:

Minecraft Biome

You could even add more inputs like the elevation or latitude into the mix, like this plot of the Holdridge Life Zone Classification Scheme from Wikipedia:

Holdridge Life Zones

With our computed biome driver values for a given point, we can find which region of this plot we fall into, to determine the predominant biome for this location.

For step 2, we can also determine how close we are to other biomes in this map, but this time instead of computing our distance in barycentric coordinates within our triangle of zones, or world-space distances across our terrain, we're computing our distance in moisture-temperature space (or whatever driving inputs you've chosen for biome selection).

This route ostensibly gives a stronger connection between the landforms of your continents and the details of your biomes, but it comes at the cost of less direct control over the placement of those biomes. With the wrong parameters, you could easily end up with a whole world of deserts, or a world with only scarce islands of a particular biome. And maybe that's desirable for the variety of worlds from your procedural generator. Or maybe it's not, and you'd prefer to sacrifice some geographical logic (that maybe only the hardcore Earth sciences geeks will notice or appreciate anyway) in order to have more hands-on control over the gameplay combinations of biomes the players will encounter, as you can get with zoning-style approaches.


Whichever route we've taken, we now have a list of nearby biomes influencing this point, and a relative weight to apply to each one. Now we can run our terrain generation logic for each biome at this point, and blend the results using those interpolation weights, to arrive at one consensus elevation (and other properties) for this location.

For the emergent scheme, note that you might already have a coarse-grained elevation selected, so your individual biome generators can be more flat, capturing only local terrain shapes, and delegating the broad-scale landforms to the bedrock pass you did earlier. This makes it easier to get a clean blend between adjacent biomes that won't have a sudden artificial-looking ramp between them because they disagree about the average elevation in their domain.

You can even use the coarse elevation value as a bias to your blending function, allowing eg. forest biomes to creep further into the valleys while letting rocky biomes dominate the higher elevations, adding a little non-linearity so the blend feels more organic.

\$\endgroup\$
7
  • \$\begingroup\$ This is an amazing answer. I am learning so much with this and I will document myself on everything you talked about in order to succeed following these methods. Thanks a lot \$\endgroup\$ Commented Oct 6, 2020 at 14:26
  • \$\begingroup\$ Awesome answer! Just got the chance to read through it all after seeing you link it. Couple of things I would add or change if it were me: Instead of Barycentric to interpolate on the Voronoi/Delaunay mesh, I would use normalized sparse convolution. Pick a radius big enough to always contain points, use a falloff function that goes to zero smoothly at that radius, and add up weighted contributions of all points in range. Then divide by the total weight. This is both smoother than barycentric interpolation, and doesn't require the Delaunay or Voronoi mesh at all -- just the points. \$\endgroup\$
    – KdotJPG
    Commented Apr 10, 2021 at 11:20
  • \$\begingroup\$ In the emergent section, an important issue I run into is that noise values are not uniformly distributed. Therefore, if you want each biome to represent a roughly equal share of the world, you need to either pass the noise output into a function that makes it more uniform, or bias your lookup map to compensate. If your lookup map is a Voronoi diagram itself, then this amounts to placing points more tightly packed in the center, than the outer edges. \$\endgroup\$
    – KdotJPG
    Commented Apr 10, 2021 at 11:20
  • \$\begingroup\$ Regarding noise, I think it's best to take care what to recommend. Perlin is an older noise with a lot of square bias, which isn't a necessary compromise for most uses. Readily-available Simplex-type noise, and drop-in 3D+ domain rotation, can easily address its problems. However, a huge number of sources teach the old noise in a vacuum rather than in context. If we teach the right thing in newer sources, then slowly this can improve. On one hand, you're only responding to the question. But OTOH, we know the asker is considering the old noise, so it's an impactful opportunity for education. \$\endgroup\$
    – KdotJPG
    Commented Apr 10, 2021 at 11:21
  • \$\begingroup\$ Sounds like you have plenty of content to share in an answer of your own, @KdotJPG \$\endgroup\$
    – DMGregory
    Commented Apr 10, 2021 at 11:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .