Generating 1D procedural terrain in Unity

Well, it’s been a while since I blogged anything. Partially because of laziness, and partially because I haven’t had anything very interesting to share. Recently though I’ve been dabbling in some Unity. I was attempting to create some 1D procedurally generated terrain, but couldn’t find much information on the topic on the internet. So, in order to help the next poor soul who attempts it, I thought I’d take a quick whirlwind tour through the process and it’s pitfalls.

I’ll start first with a brief disclaimer. I’m going to assume that you already know roughly how to use unity. This will not be a tutorial for absolute beginners, so if you find yourself confused on how to do the Unity related parts, please try another Unity tutorial before asking questions here. Another disclaimer is that I’m also a Unity beginner, so if there’s a better way of achieving the things outlined here, please do let me know!

Note that all the code will be in Unity’s flavour of Javascript, but everything should transfer easily to C#.

Setting Up

Start by creating a new project in 2D mode and create all the usual asset folders. The bare minimum you need is an empty GameObject (mine is called “Terrain”), but I added a few extra components including a Directional Light and a Sprite for my background (“Sky” in the screenshot), and my Terrain object is a child of another empty GameObject. Check the screenshot below if you’re not sure.

Next, create a new Script called TerrainGenerator. This will be where we do the majority of the work. Attach this script to your Terrain GameObject. Open up the new script and lets get cracking!

Parameters

I chose to expose a number of parameters to the editor which will allow me to tweak the terrain generation process without having to change the code. Add the following to the top of your script.

#pragma strict

var terrainWidth:int; // How many vertices wide the terrain will be

var resolution:float; // How granular the terrain will be the higher
                      // the number, the closer together points will be

var roughness:float; // How rough the terrain will be

var startHeight:float; // How high the leftmost vertex will be

var endHeight:float; // How high the rightmost vertex will be

The Heightmap

Generating the heightmap is the core of creating the terrain. The heightmap will be an array of floats which specify the height of a given point.

We will be using the Midpoint Displacement algorithm to create the terrain. Put simply, this algorithm recursively finds the midpoint between two points, and displaces it by a random amount. Each step of the algorithm takes the midpoint between the previously created point, and an existing one to create more granular changes, and the random displacement amount is smaller with each step.

My implementation of the algorithm consists of two functions. One to set everything up, and another to recursively create the heightmap. The methods are listed in their entirety below.

function GenerateHeightMap(startHeight:float, endHeight:float, count:int) {
  Debug.Log("Generating heightmap");
  // Create a heightmap array and set the start and endpoints
  var heightmap:float[] = new float[count + 1];
  heightmap[0] = startHeight;
  heightmap[heightmap.Length - 1] = endHeight;
  
  // Call the recursive function to generate the heightmap
  GenerateMidPoint(0, heightmap.Length - 1, roughness, heightmap);
  Debug.Log("Heightmap complete");
  return heightmap;
}

function GenerateMidPoint(start:int, end:int, roughness:float, heightmap:float[]) {
  // Find the midpoint of the array for this step
  var midPoint:int = Mathf.Floor((start + end) / 2);
  
  if (midPoint != start) {
    // Find the mid height for this step
    var midHeight = (heightmap[start] + heightmap[end]) / 2;
    
    // Generate a new displacement between the roughness factor
    heightmap[midPoint] = midHeight + Random.Range(-roughness, roughness);
    
    // Repeat the process for the left side and right side of
    // the new mid point
    GenerateMidPoint(start, midPoint, roughness / 2, heightmap);
    GenerateMidPoint(midPoint, end, roughness / 2, heightmap);
  }
}

Creating Vertices

Now that we have a heightmap, we can use it to generate an array of vertices that represent the terrain. This is a pretty straightforward process, we simply loop through the heightmap and generate a point for the top and a point for the bottom of the terrain for each value.

function CreateTerrainVertices(heightmap:float[], resolution:float) {
  Debug.Log("Creating terrain vertices");
  // The minimum resolution is 1
  resolution = Mathf.Max(1, resolution);
  
  var vertices:Array = new Array();
  
  // For each point, in the heightmap, create a vertex for
  // the top and the bottom of the terrain.
  for (var i:float = 0; i < heightmap.length; i += 1) {
    vertices.Push(new Vector2(i / resolution, heightmap[i]));
    vertices.Push(new Vector2(i / resolution, 0));
  }

  Debug.Log("Created " + vertices.length + " terrain vertices");  
  return vertices.ToBuiltin(Vector2) as Vector2[];
}

You may note that if you joined up all the points in the order they're given, it would look a little odd... after all, we're going top -> bottom -> top -> bottom ad infinitum. There is method to our madness though that'll be explained a bit later on. For now, just trust me!

UV Mapping

UV mapping - the process of mapping mesh co-ordinates to texture co-ordinates - has always been one of my least favourite parts of working with graphics. As such, a lot of this I stumbled across by guess work, so forgive me for some of the magic numbers in it. At some point I'll get around to refactoring them away, but for now they will do.

function GenerateTerrainUV(heightmap:float[]) {
  Debug.Log("Generating terrain UV co-ords");

  var uv:Array = new Array();

  var factor:float = 1.0f / heightmap.Length;

  // Loop through heightmap and create a UV point
  // for the top and bottom.
  for (var i:int = 0; i < heightmap.length; i++) {
    uv.Push(new Vector2((factor * i) * 20, heightmap[i] / 20));
    uv.Push(new Vector2((factor * i) * 20, 0));
  }

  Debug.Log("Generated " + uv.length + " grass UV co-ords");  
  return uv.ToBuiltin(Vector2) as Vector2[];
}

Notice how the UV points are generated in the same way as the vertex points (top, bottom). Doing it this way has essentially allowed us to treat the terrain as a series of slightly deformed quads, which makes UV mapping much simpler. The multiplying and dividing by various numbers controls how often the texture will be tiled across our generated terrain.

Triangulation

Next up we need a way of telling the renderer how to connect all our vertices into triangles. If we had generated the top of the terrain all in one go followed by the bottom, this would be a bit of a chore (I previously attempted it this way). It is possible though, using a great bit of code I found on the Unity wiki. With our vertices ordered the way we have them though, can chop it up into triangles very simply with the following code:

function Triangulate(count:int) {
  var indices:Array = new Array();

  // For each group of 4 vertices, add 6 indices
  // to create 2 triangles
  for (var i:int = 0; i <= count - 4; i += 2) {
    indices.push(i);
    indices.push(i + 3);
    indices.push(i + 1);

    indices.push(i + 3);
    indices.push(i);
    indices.push(i + 2);
  }

  return indices.ToBuiltin(int) as int[];
}

Tying it all Together

Well, we have our heightmap, we have our vertices and we have a UV map for them. We have a list of indices to create triangles for it all. What we don't have though is a way of putting all of that together to make a landscape. We want all this to be done as soon as we run our scene in Unity, so the code to tie everything together will go in the Awake() method:

function Awake() {
  Debug.Log("Generating Terrain");
  
  // Generate the heightmap
  var heightmap:float[] = GenerateHeightMap(startHeight, endHeight, terrainWidth);
  
  // Create vertices, uv's and triangles
  var terrainVertices:Vector2[] = CreateTerrainVertices(heightmap, resolution);
  var terrainUV:Vector2[] = GenerateTerrainUV(heightmap);  
  var terrainTriangles:int[] = Triangulate(terrainVertices.Length);
  
  // Create the mesh!
  GenerateMesh(terrainVertices, terrainTriangles, terrainUV, "ground", 0);
  
  Debug.Log("Terrain Gen complete");
}

The above code uses all the methods we've created so far, and passes the results to the GenerateMesh() method listed below:

function GenerateMesh(vertices:Vector2[], triangles:int[], uv:Vector2[], texture:String, z:int) {
  Debug.Log("Building Mesh");
  var meshVertices:Array = new Array();

  // Convert our Vector2's to Vector3
  for (var vertex:Vector2 in vertices) {
    meshVertices.Push(new Vector3(vertex.x, vertex.y, transform.position.z + z));
  }
  
  // Create a new mesh and set the vertices, uv's and triangles
  var mesh:Mesh = new Mesh();
  mesh.vertices = meshVertices;
  mesh.uv = uv;
  mesh.triangles = triangles;
  mesh.RecalculateNormals();
  mesh.RecalculateBounds();
  
  // Create a new game object
  var go:GameObject = new GameObject(texture);
  
  // Add the mesh to the object
  go.AddComponent(MeshRenderer);
  var filter:MeshFilter = go.AddComponent(MeshFilter);
  filter.mesh = mesh;
  
  // Add a texture  
  go.renderer.material.mainTexture = Resources.Load(texture) as Texture;
  
  // Reparent as a child of this game object
  go.transform.parent = transform;

  Debug.Log("Mesh built"); 
}

You'll notice that we specify a texture for our mesh. The one I'm using can be found here. It is an adaptation of a texture I found called Congruent Pentagon hosted on subtlepatterns.com.

Run it!

Switch back to your Unity scene, and select your terrain GameObject. In the inspector, set values for all the properties on the TerrainGenerator script. The options I use can be seen below:

Now try running it!

If you don't see anything like the above, scout around for any errors, and take another look at the code. If you're with me so far, you may have spotted a difference between our current results and the picture at the beginning of this article. For a bit of extra eye candy, we can add a layer of grass to the top of our terrain. It's achieved in a pretty much identical way to everything we've done so far, but with a few subtle differences. Again, the code here could possibly be cleaned up, but for now it will do.

Add the following to your Awake() method:

function Awake() {
  ...
  // Repeat the process for grass
  var grassVertices:Vector2[] = CreateGrassVertices(heightmap, resolution);
  var grassUV:Vector2[] = GenerateGrassUV(heightmap);
  var grassTriangles:int[] = Triangulate(terrainVertices.Length);
  
  GenerateMesh(grassVertices, grassTriangles, grassUV, "grass", -2);
  ...
}

The code for creating grass vertices is almost identical to the code for terrain vertices, but with the exception that the bottom layer of vertices sits just below the top layer rather than at 0. The UV code differs in that the texture is applied once in it's entirety for each quad.

function CreateGrassVertices(heightmap:float[], resolution:float) {
  Debug.Log("Creating grass vertices");
  resolution = Mathf.Max(1, resolution);
  
  var vertices:Array = new Array();
  
  for (var i:float = 0; i < heightmap.length; i += 1) {
    vertices.Push(new Vector2(i / resolution, heightmap[i]));
    vertices.Push(new Vector2(i / resolution, heightmap[i] - 1));
  }

  Debug.Log("Created " + vertices.length + " grass vertices");  
  return vertices.ToBuiltin(Vector2) as Vector2[];
}

function GenerateGrassUV(heightmap:float[]) {
  Debug.Log("Generating grass UV co-ords");

  var uv:Array = new Array();

  var factor:float = 1.0f / heightmap.Length;

  for (var i:int = 0; i < heightmap.length; i++) {
    uv.Push(new Vector2(i % 2, 1));
    uv.Push(new Vector2(i % 2, 0));
  }

  Debug.Log("Generated " + uv.length + " terrain UV co-ords");  
  return uv.ToBuiltin(Vector2) as Vector2[];
}

The texture for the grass is another variant of Congruent Pentagon and can be grabbed here.

Wrapping Up

If you run your scene again now you should see a beautiful bit of grass topped terrain. If not, you can view the full code listing here. Now, the next question is, what will you do with it!

Some ideas for taking this further would be to have a go at generating infinite terrain. Have a script detect when the camera is about to reach the edge, and generate another batch of terrain to follow on from the first.

Or, you could try adding collision detection to the mesh, add a bike and try out some motocross action!

If you do create something cool, be sure to let me know in the comments below!

Share on FacebookTweet about this on TwitterShare on Google+Share on Reddit

Comments are closed.