There are three main steps here:
Use some method to assign biomes to regions (this is the hard part, with multiple strategies I’ll break down shortly)
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.
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”.
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:
(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.
(Diagram showing the relationship of Voronoi cells and Delaunay triangles from here)
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:
A game might use a more abstracted version, like this example from an older version of Minecraft, via the Minecraft wiki:
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:
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.