Make procedural landmass map in Unity

Lewis Ben
9 min readDec 6, 2017

--

To procedurally generate a landmass map, we are going to begin by generating heightmap by using Perlin noise, then we’ll assign terrain types to the various height ranges and finally use all this information to construct a mesh map.

Let’s start with rectangles. To draw one, we can specify its two opposite corner vertices with mouse input.

Let’s start with Perlin noise.

Noise Map

The roads, rocks, trees, and clouds in our real-world all have an unpredictability surface, which could be called “random”. The scientists came up with many methods to approximate this variety computationally.

Before we master the random, let’s see how the randomness — chaos comes out.

TK Let’s start by analyzing

 y = fract(sin(x)*1.0) 

We are extracting the fractional content of a sine wave. You can increase the width value to “break” it into smaller pieces. What we are doing is multiplying the resultant of sin(x) by larger numbers to “corrupt” the flow of the sine wave into pseudo-random chaos.

Edit on this link
open inGLSL Editor or any glsl editor

Next, we can apply it into two dimensions with the same idea but using dot products transforming the two-dimensional vector into a one-dimensional floating-point value. If look closely at the random map, you will note there is always the same random effect. For example, rand(1.) it is always going to return the same value because of TK sin(x)which is always the same wave. It’s important to keep in mind that our rand() function is a deterministic random, also known as pseudo-random. I bet you now think the Math.Random() which is not-deterministic; every call will return a different value. But we also have Math.Random(int seed).

There are lots of interesting implementations combining patterns with random. Check the chapter of random in The book of shaders.

But in fact, the noise what we want to use in generating landmass map don’t like the random we were talked above. The “real world” is more various and complex. There is a simple noise function provided by Patricio Gonzalez Vivo

y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f));

In these lines, we are subdividing a continuous floating number(x) int it’s integer ( i) and fractional (f) components. We apply rand() to the intergetr part of x which gives a unique random value for each interger. Then we interpolate each random value uses a smoothstep() (a cubic curve function).

Now that we know how to do noise in 1D, it’s time to move on to 2D. In 2D, instead of interpolating between two points of a line (fract(x) and fract(x)+1.0), we are goint to interpolate between the four cornes of the square area of a plane (fract(st), fract(st)+vec2(1.,0.), fract(st)+vec2(0.,1.) and fract(st)+vec2(1.,1.)).

https://thebookofshaders.com/11/

The 1D and 2D implementations we’ve seen so far were interpolations between random value, which is why they’re called Value Noise, but there are more ways to obtain noise…

In 1985, Ken Perlin developed another implementation of the algorithm called Gradient Noise. Ken figured out how to interpolate random gradients instead of values. These gradients were the result of a 2D random function that returns directions (represented by a vec2) instead of singles values(float). Check here to see the code and how it works.

Pay attention to the difference between value noise and gradient noise.

The value noise produces lower quality but it’s faster to compute than gradient noise. The principle is to create a virtual grid all over the plane, and assign one random value to every vertex in the grid.

The gradient noise produces smoother and higher quality than it’s of course slightly more expensive. It’s suitable for procedural texture/shading, modeling and animation. The princpiple is to create a virtual grid all over the plane , and assign one random vector to every vertex in the grid.

Well… enough theories, it’s time for us to use the noise in our own expression way. TK. Let’s first implement the noise value method.

Next, we can write our code to achieve the gradient value. But we can also use Mathf.PerlinNoise provided by Unity.

Below is a script that we can sample the PerlinNoise value with every vertex in the grid and then fill it into a texture.

Enough about noise. And go back to our mountain, currently, it’s too smooth. We need to find a way to add some details for mountains but keep the overall shape.

  • We can layer multiple levels of noise ( “octaves” , the graph in blue )and add them together to preserve the overall shape(graph in red).
  • The “lacunarity’ controls the increase of each octave (the sin wave’s size in the x-axis).
  • The “Persistence” controls the decrease in amplitude of each octave
https://www.youtube.com/watch?v=wbpMiKiSKm8&list=PLFt_AvWsXl0eBW2EiBtl_sxmDtSgZBxB3&index=1
https://gist.github.com/liuxinjia/cccf5329faa6fe796670169149058ffb

So by doing all of this we’ve now achieved a far more natural-looking outline.

Lacunarity = 1.58, Persistence = 0.5,
Lacunarity = 1.58, Persistence = 1,
Lacunarity = 11, Persistence = .5,

Increasing the lacunarity value will increase the small features of the land, and the persistence value will control how much these small features will affect the overall shape.

We want to also control the height of the mountains. Let’s create our terrain mesh. We’ll do this by generating a flat plane and then just set the height of the individual vertices. If you don’t know how to make mesh by vertices, you can check CatLikeCoding’s mesh tutorial. We want to only focus on how to implement noise on our map mesh. We can multiply the noise heightmap value we talked above with the height variable that we control the height of mountains.

meshData.vertices[vertexIndex] = new Vector3 (topLeftX + x, heightMap[x, y] * heightMulitipiler * heightCurve.Evaluate (heightMap[x, y]), topLeftZ — y);

Now you get mountains rise and fall but something ridiculous. The problem is that some part of the plane is too hilly. For example, the flat plain or the sea’s height shouldn't be affected by the noise value. We can use the animation curve to specify which height should be affected by the noise value.

Another improvement in performance is the Level of Detail. The landmass map consists of multiple meshes. We can decrease the number of vertices as it’s far away from the viewer. Level of detail techniques increase the efficiency of rendering and make the game smoother.

for( int x = 0; x < width ; x++){
//Create Vertex
}

We currently loop through each of these points with an increment of 1. And we can simplify the measure bit by increase the improvement to 2 or another factor of width-1.

v = (w-1)/i + 1;

The formula is the number of vertices(i) per line is equal to the map width (w) minus one over i (the factor of(w-1) ) and plus 1. For example, we want to iterate vertices the width is 9 if we use a value of 4 for increment, we can decrease the vertices to 3.

The white and black map is dull. Let’s color it with some cartoon colors. We can map terrain types to our different noise values.

Let’s make another map called ColorMap and assign it with region color according to its height value.

Color[] colorMap = new Color[mapChunkSize * mapChunkSize];for (int y = 0; y < mapChunkSize; y++) {    for (int x = 0; x < mapChunkSize; x++) {         float currentHeight = noiseMap[x, y];         for (int i = 0; i < regions.Length; i++) {              if (currentHeight <= regions[i].height) {                  colorMap[y * mapChunkSize + x] =                 regions[i].colour;                   break;            }     }}

Now you can get a colorful landmass map.

Improve the map

1. LOD switching

If we want to create an endless map like Minecraft, we should consider the performance and decreasing the workload on rendering. It’s necessary for us to take Level of detail into consideration. LOD means decreasing the number of vertices and triangles as it moves away from the viewer. We can achieve it by splitting the whole map into several individual terrain blocks and change its meshverticesincrement which is determined by LOD. For example, if the LOD is 1 and the meshverticesincrement is doubled to 4 (Attention: 0 is 2), which means we can skip three vertices when storing the first and the second vertices. It will reduce the number of vertices and triangles significantly but lose the terrain details. It’s what we want to do for the far terrain with lower but accepted qualities.

2. Seams

It causes a problem that the adjacent terrain blocks' vertices at the same LOD level didn’t match up with each other. Because when we calculate each vertex’s noise value and then normalize it.

noiseMap[x, y] = Mathf.InverseLerp (minLocalNoiseHeight, maxLocalNoiseHeight, noiseMap[x, y]);

However, the different block’s maximum and minimum noise height will not be the same and it shouldn't be, too. So it means we can’t calculate that way when we have different blocks. What can we do is to calculate the max value which is only related to the constant properties like frequency and lacunarity to normalize. But the assuming max number is we will never get access to because of lossy calculations. As Sebastian Lague found out, we can adjust the formula to get better effect.

3. Normals.

Though we currently have the adjacent terrain blocks match up with each other better. But there still exists some glitch bug. If you look deeper into them, the glitch seams between them are very noticeable. The reason for this is very simple the normal direction of vertices at the edge of chunks doesn’t have access to their neighbors normal.

Unity needs the vertex’s normal to calculate the lighting such as diffuse lighting, specular lighting, shadow, etc. It’s easy to average all the triangles normal the vertex belongs to get the vertex normal. The triangles normal can be calculated by cross-product and don’t forget to normalized it at last.

After we calculated the normal by ourselves, the seams still exist but the lighting may be better. The glitchy seam at the edge should handling separately. We can exclude it from the final mesh normal calculations. If you use flat shading, you can neglect to calculate the normals to reduce the workflow of calculations which really cost a lost.

the perfect seam of same LOD adjacent chunks

3. Falloff Map

If we want to make an island instead of an endless mainland which is surrounded by the sea. We can do this with the help of FallOff map which the Txel value(coordinate divided by size) is the bigger one between x and y. And we can mix the falloff value with the noise value we talked above with the help of some amazing function.

4. Mesh Collider

Assign the created mesh to mesh collider is easy. But the performance is terrible. We need to reduce the number of mesh vertices. There are two ways to do it. One is to only assign the mesh collider to the current chunk, and the other one is to reduce the vertices of selecting one. The first method which we can use the LOD system and comparing the distance with the viewer. And the second method is we can also use the LOD and choose a lower LOD level mesh to reduce the mesh vertices.

It’s efficient to separate creating a mesh from creating collider when we need to move often in an endless terrain.

At last

The article is followed by Sebastian Lague’s Procedural Landmass Generation video tutorials.

Congratulations on making it this far!

--

--