Terrain Generation — Part 1
--
Terrain generation can be a very difficult area for development — if you make it one. What I mean is: generating the mesh for a terrain can be an incredibly difficult and daunting task. This stems from the fact that there are many different techniques on how you can generate terrain. In its simplest form, to generate a terrain mesh, all you need is a grid of vertices with different heights. But, on the more advanced side, the terrain can be variably tessellated in specific areas to provide extra fidelity while still retaining stellar performance. Today, I will be going over how to start generating terrain, which will provide a launching pad for some of the more difficult topics.
For this tutorial, you can use whatever language you would like. There are no special functions that I will be writing that cannot be ported into other languages or libraries. I am personally using C# with Unity as my engine. I’m doing this because I find Unity to be simple enough, while still providing the features that I need.
This tutorial also assumes that you know the programming language that you choose to use. It also assumes that you know how meshes, triangulation, and a few other topics related to procedural modeling work.
Starting Out
Create a new class, name it ‘TerrainChunk’, or something along those lines. It will hold the data for each chunk, or section of the terrain. This class will have a couple of utility functions that you can use to simplify your game development. These functions will also be utilized in future tutorials, so I would strongly suggest that you add them to your code. This class will also hold a function to generate the Terrain Mesh, and we will run through how you do that.
For the Terrain Chunk script, you need to add a few objects. Add two static integers, ‘ChunkWidth’ and ‘ChunkHeight’. These two values will store the sizes for all of the segments of terrain. Since they are not constant, you will be able to change them if a specific map requires it. After those are added, you will need to add a 2D array of floating-point numbers, which will represent the heights of the terrain. This value will be known as ‘ChunkData’, and it should not be static. Alternatively, this value can be a reference to a texture that stores the heightmap data, but I will not be going over how to do that in this tutorial.
Building Some Utilities
There are a couple of utilities that we should write before we start getting into generating the terrain mesh. Each function that we need to write will be part of the TerrainChunk class, so include them all in there. All of the code will be shown in C# but can be easily understood by anyone with a basic knowledge of programming. If I were to explain why each of the functions worked in detail, this tutorial would be incredibly long, and no one would want to read it.
For the sake of simplicity, and a little challenge for readers, I didn’t implement guardrails on any of the utility functions. So, you can access points that are out of range, which will throw errors. The fix is quite easy (just some if statements to make sure you’re in range), but it should provide a slight challenge.
getTerrainHeight
This will be a pretty simple function that takes in an x-coordinate and a y-coordinate and will output the height of the terrain at that point. It will automatically interpolate the height of the four points that it lies between by using bilinear interpolation.
getTerrainSlope
This function will tell us what the slope of the terrain is based on two different sets of x-coordinate and y-coordinate inputs. This function will be very helpful if you have an RTS where specific characters cannot move on high slopes, or buildings can only be placed at certain slope inclines. To imagine how the function works, it gets the height at the first point, and then gets the height at the second point, then returns the slope between those two points. The code shown below is not the fastest way to make a slope function, but it is one of the easiest to understand.
The first point in the input is the ‘baseline point’, so the slope is based on this. If the next point is lower than the ‘baseline point’, the slope will be negative. If the next point is above the ‘baseline point’, the slope will be positive.
setTerrainHeight
This is going to be the opposite of the getTerrainHeight function. This function will take in a height value, x-coordinate, and y — coordinate, and will set the height based on those values. There will not be any interpolation at the moment (it will be covered in a later tutorial), instead, the coordinates will be rounded to their closest neighbor, and that point’s height will be set.
setTerrainHeights
This is the same idea as the setTerrainHeight function. The only difference is that this function will be setting a large chunk of terrain heights at the same time. The input of the function is two integers that specify the x and y offsets of the change, and a 2D array of floating-point numbers representing the heights that need to change.
Getting the Terrain Generated
In the Terrain Chunk class, you will need to create a function that takes in an integer as an input, and a Mesh as the output. This function will be named GenerateTerrainMesh, and the integer will represent the resolution that the grid needs to be. So, as the number is increased, the number of triangles and points in the mesh are decreased. This allows for the generation of lower quality terrain when the camera is further away. This technique is known as Level of Detail, and it can be very helpful to save on resources while still providing good quality terrain. There are a couple of different parts necessary for terrain generation, so I separated them in easy to understand areas below.
Vertex Generation
Create a new array of Vector3 (they are an object that stores x, y, z positioning), name it ‘vertices’, and set the size of the array to ((ChunkWidth + 1) * (ChunkHeight + 1)). To explain this equation, imagine a grid. The length of the grid is ChunkHeight, and the width of it is ChunkWidth. Starting with the first point,(0, 0), all of the points can fit in it, except, for the line of points at the top of the terrain (when looking from the sky downwards), and left of the terrain. We can remedy this by adding 1 to both the ChunkWidth and ChunkHeight, to make sure all the vertex positions are included.
Create two ‘for’ loops, one with the variable ‘x’ and the other with the variable ‘y’. Make sure that ‘x’ can be no greater than the width of the grid, and that ‘y’ can be no greater than the height of the grid. (When I say no greater than, in the code, it will be a less than or equal to sign). Then, before both of these loops, create a new variable, ‘i’. It needs to be an integer, and it will pretty much tell us which vertex in the array we are currently talking about.
Inside of the loop, make sure that ‘vertices[i]’ is set to a new vector3 with the current ‘x’ value and current ‘y’ value as its x and z values respectively. Make sure that both of those variables are cast to floating-point values, or else there will be problems when you try and make the terrain higher fidelity in the future. For the ‘y’ value of the vector3, set it to the getTerrainHeight function from the utility section. This means that we can sample the heights from the terrain, even when we have a higher resolution mesh than the heightmap.
Index Generation
To start the generation of the indices, create an array of integers with their length equal to the equation shown below. To explain this equation, imagine the grid mentioned in the example above, except instead of referencing the points, we are mentioning the squares that four of those points create. In each of these squares, there are two triangles, made up of three points each, and that is where the six comes from. It is really saying, “gridWidth * gridHeight * squarePointCount”.
After that is done, create two integers to represent the current vertex, and current triangle index respectively. Then create two for loops for the ‘x’ and ‘y’. This time, I wrapped the ‘y’ first because it was a bit of a change in scenery for me. These for loops, like the vertex for loops, must be limited by the size of the grid. But, the sign should be less than instead of less than or equal to.
Inside of the loops, you need to assign the triangles their indexes. If you read through the code below, we start at the current point and we connect it to the above point, and then finish that triangle with (currentPoint + 1). For our next triangle, start it at (currentPoint + 1), and then connect it to the second point in the last triangle, and then connect it to the point above (currentPoint + 1). You might be wondering: does the order of this matter, and the answer is yes. If we don’t order these like this, when our engine (Unity) generates the Normals, it will generate them in the wrong direction.
After that in the for loop, we need to increase the current vertex by 1 and increase the triangle index by 6. In the lower for loop, we also have to increment the vertices by one because we don't want extra triangles at the end of the grid where there are no points to connect to.
UV Generation
Finishing Up
To finish up, create a new local variable with Mesh as the type. This is class is specific to unity, but other languages offer very similar systems. Set the vertices array to the vertices that we just created, set the triangles array to the indices we just created, and finally, set the UV array to the UVs that we just created.
After this is done, you can generate the Normals, Tangents, and Bounds of the object. Without these, physics, lighting, and many aspects of rendering will not work correctly. Unity offers built-in functions for doing this that work fairly well, and in future tutorials in this series, I will go over how these functions work.
Once you are done running those functions, you can return the object. And guess what, you're done! Now you can create a new TerrainChunk object anywhere in your code, change the heights, and render it to the screen with relative simplicity.
You’re Done! (for now)
That’s it. You now have a flexible terrain system that allows you to have high-resolution terrain, low-resolution terrain, and everything in between. But, there is more coming in the future- this is only part 1 of a multipart series. In this series, we will work our way up to terrain generation techniques that can be seen in many AAA games.
If you liked reading this article, make sure to give it a clap (it’s free), and follow me to get notified about new tutorials on important topics like procedural animation, character generation, crowd simulation, building interior creation, etc. Also, make sure to share this article with people you know that would be interested. You might even want to post it on a few subreddits or forums that you think would enjoy it.