This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.

Modelling by numbers: Supplementary (Terrain)

The following blog post, unless otherwise noted, was written by a member of Gamasutras community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

Modelling by numbers

An introduction to procedural geometry

A while ago, I posted a tutorial, or rather a series of tutorials, about generating procedural meshes in Unity. It got quite long, and I had to remove a bunch of topics that I had originally intended to cover. The Modelling by numbers tutorial got a very positive response, and also quite a few people asking for extra information, mostly to do with the topics that I had discarded. So I figured I’d revisit them. What follows is one of them.

For those who have not read the Modelling by numbers series, I would recommend reading part 1A before continuing with this tutorial.

Cons

Unity terrain

Unity terrain is the solution that comes built in with Unity. At the most basic level, it’s just a heightmapped plane, with a mesh that recalculates its mesh structure to give more polygons where the player is, and less far away.

Unity terrain comes with a bunch of tools in the Unity editor, plus a vegetation system that works over the top of it.

Pros

The back-end work is done for you. Just paint up a terrain and you’re away.

The tools and UI are friendly for non-programmers

Vegetation system

Polygon resolution/tessellation is taken care of for you (not always a pro, see below).

Cons

Vertex data cannot be controlled (no putting extra information in uv2 or vertex colour streams, for example). This is not much of an issue unless you’re like me and insist on putting custom shaders on everything.

Moving over the terrain at high speed results in the mesh tessellation constantly being recalculated, causing bits of it to jump around on the screen every few moments.

Heightmapped plane

A heightmapped terrain uses a texture to define height values over an area.

Pros

Implementation is simple.

Using a dynamic texture or render target makes it easy to create terrain that is deformable at runtime.

Cons

The terrain will only be as good as the heightmap that generates it. Generating good heightmaps takes skill and/or powerful tools.

Placing objects on the terrain will require either raycasting or sampling a texture. This may be slow, especially for large numbers of objects.

Perlin noise

Perlin noise is a type of pseudo-random noise that determines height values mathematically.

Pros

No size or boundary limitations: height data does not need to be stored anywhere.

No dependance on initialisation order: the height offset at any point can be determined mathematically, before the terrain itself is generated.

Very quick to add to a level, due to no asset dependencies.

Cons

There is less control over the result compared to height-mapped methods.

Diamond-square algorithm

Diamond-square is an extended/improved version of the midpoint displacement algorithm. The basic principle is that the terrain area is subdivided, with each subdivision receiving a height offset. This process repeats, with the subdivisions getting smaller and smaller.

Pros

No asset dependencies.

Cons

Implementation is more complicated than the other methods.

Possible initialisation order issues: Terrain data must be generated before anything else can be placed on it.

Some from column A, some from column B

It’s possible to mix and match methods. It’s not uncommon to generate a terrain using a procedural method and then modify the resulting mesh by hand. Or a hand-made/heightmapped mesh might have a noise applied over the top to give it a bit of roughness or randomness. The Unity terrain engine is also able to import and export heightmaps.

OK, let’s write some code

We’re now going to take a closer look at three of the methods described above: a height mapped plane, perlin noise and the diamond-square algorithm.

General terrain mesh generation

The code that does the bulk of the mesh generation is the same for all three methods. Also, we’ve done it before. Anyone fresh out of Modelling by numbers: Part One A will find this code familiar:

Most of this code comes straight from the ground mesh part of the first tutorial. We are building a set of quads in a grid formation. It’s all wrapped up in an abstract class that we’ll derive from for each of our terrain methods.

In that first tutorial, we used a random number to offset the Y value of each vertex in the plane. We did this because it was simple, but to make an actual, decent terrain, we need to do something fancier here. The code above declares a GetY() function, that we call to offset each vertex in the mesh. It takes 2 arguments, the X and Z position of that same vertex. What we actually do inside that function depends on which terrain method we’re using.

Onward now, to the interesting stuff. For each different terrain generation method, we’ll derive a child class from ProcTerrain and override GetY().

Heightmap

First, we’ll generate terrain based on a pre-made heightmap texture. Our Y values are based on the brightness of each pixel:

The X and Z coordinates are converted to UV coordinates using the width of the terrain. Using this, we sample a heightmap texture to get the colour value at that point.

The height is the greyscale value of the pixel. We multiply this value by the terrain height to get the final Y offset.

There’s an issue here that’s easy to run into. In Unity, we cannot access pixels in textures that do not have the “Read/Write Enabled” checkbox ticked in the import settings. This is unticked by default, and easy to forget about (or get missed if the person adding the heightmap doesn’t know about it). To keep things running smoothly, we need to add a check to our code for this to avoid trying to access an unreadable texture (and getting an error for every single vertex in our mesh, potentially bringing Unity to its knees). Annoyingly, Unity provides us with no way of checking, so we need to try accessing and then catch the exception:

This function tries to get a pixel from our heightmap, and returns true if that succeeds. We use an additional variable, m_TextureChecked, to ensure that we only check the texture once. Otherwise, we’d have to go through that try/catch block for every vertex, which would not be quick.

Perlin noise

This is a very simple terrain method that doesn’t require a heightmap and is quick to implement. In its simplest form, it gives a very smooth, rolling hills effect, although roughness can be added by layering the noise (we’ll do this in a moment).

We’re using Mathf.PerlinNoise() to generate the height. Official documentation on this function is pretty sparse, which is a little frustrating. Basically, the function takes in two values and spits out a number between zero and one. Increasing m_NoiseScale will increase the amount of noise detail, making smaller bumps, and decreasing it will do the opposite.

One thing to watch out for is that whole number inputs will always return the same result, so if we’re calling GetY at whole number intervals and m_NoiseScale is also set to a whole number, our terrain will come out flat. m_NoiseScale should always be set to a fractional number. If the script is going to be handed over to someone unaware of this, it may be worth enforcing it in code.

Adding randomness

The terrain generated using this method will be the same every time. We can add randomness by offsetting the values passed into the PerlinNoise() function.

The values offsetX and offsetZ could be defined in the editor, loaded from a file, or even re-generated on a button press. As long as they stay the same, we’ll get the same terrain every time. When they change it’ll be different.

Layered noise

It’s possible to apply Perlin noise in layers to get a rougher looking effect. This is also referred to as fractal perlin noise.

With each layer, we increase the noise scale, and decrease the height multiplier. We add all the layered values together, dividing by the total possible sum to get a normalised value.

Diamond-square

This method is more involved than the others, but can get good results. I’d like to admit upfront that my experience with this algorithm is limited: my usual personal preference is to work with Perlin noise. I’ll try my best to explain it well. Some other good explanations can be found here or here.

The diamond-square algorithm is an extension of midpoint displacement. The basic idea is to offset points on a grid, with the grid (and offset) getting smaller and smaller with each pass.

We begin with our seed values. These are random values that will define the overall layout of the terrain.

Next, we calculate the height at the centre of each group of four seed points, averaging the outer values and adding a small random offset.

The next step is very similar, except that we calculate from the values directly adjacent, rather than diagonal, from the centre point.

The steps repeat, getting smaller and smaller, until the grid is full.

That’s the basic idea of the algorithm, on to the code. We’re going to generate all our height values once at the beginning (in the Start() function), and then have GetY() reference them later.

We’ll begin by defining a function that’s going to be called a lot by the following code. What it does is take a row and column reference and turn that into an index to the height values array. This gives us two nice things. First, it means the rest of our code can just work in rows and columns without worrying about how to convert. Second, we can decide here how we want to handle values off the edge of the grid. For this tutorial, the values wrap around to the other side of the grid. An alternative is to clamp the values to the edge, although you’ll get less variation that way.

Now we define the square step. This function looks at the four corner points on a grid square and averages their height. This height, with a random offset, is assigned to the point in the centre. We will record the max and min height values so that all the values can be properly normalised later (values have to be normalised if we’re going to store them in a texture):

Now to put everything together. We begin by initialising our height array and zeroing the values. Our height data will eventually be stored in a texture, but we will generate the values using a float array for greater precision and then convert at the end:

With our array and seed values all ready, we can do the actual diamond-square passes and then build the mesh. The passes get progressively smaller until they reach a size of one. At this point, we have height data for every point on the grid.

And last of all, we call the base Start() function, which will build the mesh:

//build the mesh:
base.Start();
} //end Start()

That is the data generation step. It leaves us with a texture full of height values. Our final GetY() function is now very similar to that used by the heightmapped terrain, although slightly simpler because we created the texture ourselves, so we already know about its readability and pixel format:

Note: This terrain method, unlike the others, is dependent on script execution order. Y offsets cannot be calculated until the height values are built. If objects are to be placed on this terrain, make sure that this script is going to be executed first.

Controlling randomness

Unlike Perlin noise, this method will generate a different terrain every time. Sometimes, this is exactly what we want. Other times, we want a guaranteed result.

The way we get a guaranteed result is by seeding the random number generator to a predefined value:

Pretty simple. Just throw this script onto any game object that needs its height at terrain level. We’ve added an offset to the Y, just in case we have an object that we need just above or below terrain level (very often objects will look better if they are sunk into the ground a little).

There’s a problem with this setup, however: ProcTerrain.GetY() is working in the local space of the terrain mesh. If the terrain GameObject is sitting on the world origin and isn’t transformed at all, this won’t be an issue, however if you ever want to move, rotate or scale the terrain mesh, then local and world space aren’t going to line up anymore.

What we’re going to do about this is to make a new method for ProcTerrain that knows about world space:

This function takes the world space X and Z coordinates and transforms them into local space before passing them on to the GetY() function. Our TerrainOffset script calls this function instead of GetY():