Infinite terrain generation in Unity 3D – Part 1

With the constant improvement of hardware performance game terrains got much larger and more detailed – featuring detailed shape, a lot of grass and other objects (trees, water etc.). During the last years size of terrains grew up to hundreds square miles – especially in RPG games.

In this tutorial I’ll show you how to generate a 3D terrain that could take a long, long hours to wander. We’ll be using Unity3D engine and C# language for writing code. Some basic programming knowledge would be required – although entire source code is free to download (see below), in this article I’ll explain only most important parts and code examples.
Let’s start…

One of the most popular way of visualizing 3D terrains is to use some form of heightmap. It is a set of elevation data stored as an image whose dimensions match width and height of the terrain. The darker the color is the lower the ground is elevated. Below you can see how such data transforms into visible mesh:

heightmap  Heightmap_rendered

Because we want to make terrain that is virtually infinite (or at least veeeery large) we cannot use this technique directly – memory footprint required to store all the data would be huge and heightmap size would grow to thousands of pixels.
Instead of making one large terrain we’ll divide it into smaller fragments called “chunks”. Each chunk will have its own mesh and several neighboring chunks will blend seamlessly into one larger terrain. You can treat them as a square tiles with 2D coordinates (X/Z). The key is to create several chunks of terrain around the player (up to the view distance) and add new ones as player moves. Old chunks that are too far from camera are then removed to free memory. See image below:

text3824

We can describe single chunk geometry as:

  • Length – which is the chunk border size in Unity units
  • Height – which is the maximum available height of terrain in the chunk (also in Unity distance units)
  • Heightmap & alphamap resolution – which define how accurate chunk mesh and textures will be – the higher the value the more complex mesh we’ll get. According to the Unity documentation (here) it should be a power of two plus one (129, 257 etc.).

Let’s put it in code – here is out TerrainChunkSettings class:

public class TerrainChunkSettings
{
    public int HeightmapResolution { get; private set; }
    public int AlphamapResolution { get; private set; }

    public int Length { get; private set; }
    public int Height { get; private set; }
}

Now here is our terrain chunk class (for now we will skip the methods):

public class TerrainChunk
{
    public int X { get; private set; }

    public int Z { get; private set; }

    private Terrain Terrain { get; set; }

    private TerrainChunkSettings Settings { get; set; }

    private NoiseProvider NoiseProvider { get; set; }
}

Each chunk is defined by its X/Z position (see image above), settings and Unity Terrain object – this the actual gameobject that holds mesh and all stuff needed to render the terrain on the scene. The last field – NoiseProvider – will be discussed below.

So – how do we create a heightmap texture with all the fancy mountains, hills and so on?
There are a lot of approaches to do that – you can find tons of information about it on the Procedural Generation Wiki. We’ll fill our heightmap with coherent noise by using a LibNoise library. A lot of details about coherent noise and how to use it can be found here and here – I highly recommend reading both of those.
Without getting much into detail let’s just say that for any position in 3D space (x, y, z) we can get a particular noise value that – when put on texture – form a very nice images resembling a real terrain shape.
For our purpose we’ll skip Y component because we’re creating terrain on the flat surface. As LibNoise returns noise values from -1 to 1 we need to scale it to 0..1 (that scale is much more convenient).

I’ve created INoiseProvider interface that forces returning a value for given X/Z coordinates in Unity world space (that is an important information). NoiseProvider class gives us such a value from Perlin noise (I hope you’ve read two links above) – that’s just for a start.

public class NoiseProvider : INoiseProvider
{
    private Perlin PerlinNoiseGenerator;

    public NoiseProvider()
    {
        PerlinNoiseGenerator = new Perlin();
    }

    public float GetValue(float x, float z)
    {
        return (float)(PerlinNoiseGenerator.GetValue(x, 0, z) / 2f) + 0.5f;
    }
}

OK – we’ve got our simple noise generator. Now some more technical stuff about Unity terrain.
Normally you could create single terrain from GameObject/3D Object/Terrain menu. But if we want to create terrain from code we need a TerrainData object which contains most of information required to generate terrain mesh. Here we set our heightmap values, resolutions and map size. After that we create Terrain gameobject using Unity CreateTerrainGameObject() method, place our newly generated object in proper place by setting its transform position and Flush() all the data – rest is done automatically by Unity.
Method below does all those things:

public void CreateTerrain()
{
    var terrainData = new TerrainData();
    terrainData.heightmapResolution = Settings.HeightmapResolution;
    terrainData.alphamapResolution = Settings.AlphamapResolution;

    var heightmap = GetHeightmap();
    terrainData.SetHeights(0, 0, heightmap);
    terrainData.size = new Vector3(Settings.Length, Settings.Height, Settings.Length);

    var newTerrainGameObject = Terrain.CreateTerrainGameObject(terrainData);
    newTerrainGameObject.transform.position = new Vector3(X * Settings.Length, 0, Z * Settings.Length);
    Terrain = newTerrainGameObject.GetComponent<Terrain>();
    Terrain.Flush();
}

As you can see there is a GetHeightmap() method that fills our elevation values by using noise mentioned earlier:

private float[,] GetHeightmap()
{
    var heightmap = new float[Settings.HeightmapResolution, Settings.HeightmapResolution];

    for (var zRes = 0; zRes < Settings.HeightmapResolution; zRes++)
    {
        for (var xRes = 0; xRes < Settings.HeightmapResolution; xRes++)
        {
            var xCoordinate = X + (float)xRes / (Settings.HeightmapResolution - 1);
            var zCoordinate = Z + (float)zRes / (Settings.HeightmapResolution - 1);

            heightmap[zRes, xRes] = NoiseProvider.GetValue(xCoordinate, zCoordinate);
        }
    }

    return heightmap;
}

How does it work?
To fill an entire elevation array – which both dimensions equal terrain resolution – we need to iterate it and get noise value for every position (X/Z). Final coordinates for our NoiseProvider are generated from chunk position plus iteration step divided by resolution minus one. This way we scale our X/Z direction to 0..1 (for first chunk), 1..2 (for second chunk), 2..3 (third chunk) etc. It’ll keep our generation compatible with previously created NoiseProvider.

OK – we’ve got a core of our application and now it is time for some testing.
Let’s create a single chunk with a 129 resolution, 100 meters in size and 20 meters tall:

void Test()
{
    var settings = new TerrainChunkSettings(129, 129, 100, 20);
    var noiseProvider = new NoiseProvider();
    var terrain = new TerrainChunk(settings, noiseProvider, 0, 0);
    terrain.CreateTerrain();
}

After calling this method we’ve got our first terrain!

first_terrain

It doesn’t look impressive (yet!) and has no textures applied – but you can see some hills there – that’s a good start.
Now we need to get it better – let’s create some more chunks to make our terrain larger:

void Test()
{
    Settings = new TerrainChunkSettings(129, 129, 100, 20);
    NoiseProvider = new NoiseProvider();
    for (var i = 0; i < 4; i ++)
        for (var j = 0; j < 4; j++)
            new TerrainChunk(Settings, NoiseProvider, i, j).CreateTerrain();
}

second_terrain

Our terrain is growing – that’s exactly what we want to achieve. We’ve got 16 chunks of terrain, each independent and with own mesh. We could add more of them – thus enlarging our map – but let’s stop for a second…

As you might notice, creating larger terrain can take some time. On my PC creating 16 chunks took about ~1500 milliseconds during which entire application was frozen – it was very visible and is not allowed in a normal game.
Most of this delay was used to calculate a lot of noise values for every part of terrain. Such performance issues are common in single-threaded applications. To fix it we need to put our heightmap generating functions in separate thread that will be independent from main thread. To make it even better – we’ll create a number of threads to simultaneously create few chunks at the same time. It will prevent us from blocking main application thread and should speed up chunk generation time.

This improvement has created a lot of changes in our code – here are most important of them:

  • TerrainChunkGenerator class was added – it will manage adding and removing chunks, keeping them up to date and will serve as a primary interface between our terrain and rest of application. If something in terrain requires modification – it should by done by using appropriate method from this class.
  • Added ChunkCache class – it is used to hold information about all requested and created chunks. It also tracks chunk status.
  • Chunks are explicitly identified by their X/Z position – and it is our only way to distinct one chunk from another. I’ve created Vector2i class to hold information about chunk position.

I’ve also added ability to remove chunks. When chunk needs to be removed it is added to the queue. Each frame chunk cache checks this queue and tries to delete all requested chunks. Chunk cannot be deleted if it is under generation – in this case its removal is delayed until generation is finished. It may not be the most efficient way, but it’s fast to implement and easy to control. Everything is implemented in TryToDeleteQueuedChunks method in ChunkCache class.

Now let’s write method to generate a sufficient amount of chunks around the player. As input we’ll need player position and distance (radius) how far to generate new chunks. As a result we expect a list of coordinates of all chunks that need to be created around the player. Let’s take a look at this code:

private List<Vector2i> GetChunkPositionsInRadius(Vector2i chunkPosition, int radius)
{
    var result = new List<Vector2i>();

    for (var zCircle = -radius; zCircle <= radius; zCircle++)
    {
        for (var xCircle = -radius; xCircle <= radius; xCircle++)
        {
            if (xCircle * xCircle + zCircle * zCircle < radius * radius)
                result.Add(new Vector2i(chunkPosition.X + xCircle, chunkPosition.Z + zCircle));
        }
    }

    return result;
}

This method takes initial chunk position and radius (in chunk units) as an input and gives as all coordinates that match circle equation. Here’s an example – input is in (0, 0) position and chunk radius is 7 (player position is in the middle):

radiusNow it’s time for the magic trick – to give player an illusion of infinite terrain we must constantly watch his position and add new chunks of terrain as he moves towards some direction. Old chunks that are out of sight are then removed. This way player may move for a very long distances and still see miles of terrain in front of him (or her).
We are monitoring player movement in our newly created class – GameController. It is responsible for high level application control – managing player, communication with terrain generator and UI interaction. After we detect that player has moved far enough to regenerate terrain (in other words – he moved from chunk to chunk) we must calculate new chunks in radius of sight, create them and delete some far ones.
Here’s a code for this:

public void UpdateTerrain(Vector3 worldPosition, int radius)
{
    var chunkPosition = GetChunkPosition(worldPosition);
    var newPositions = GetChunkPositionsInRadius(chunkPosition, radius);

    var loadedChunks = Cache.GetGeneratedChunks();
    var chunksToRemove = loadedChunks.Except(newPositions).ToList();

    var positionsToGenerate = newPositions.Except(chunksToRemove).ToList();
    foreach (var position in positionsToGenerate)
        GenerateChunk(position.X, position.Z);

    foreach (var position in chunksToRemove)
        RemoveChunk(position.X, position.Z);
}

First we are calculating chunk that player is in. Then new circle of chunks around the player is computed and we must determine which chunks to remove and which to add (simple subtraction/intersection operations). At the end we are generating and removing respective parts of terrain.
This is repeated each time player moves from chunk to chunk.
To test all we’ve created above I’ve added a standard Unity FPS controller and created player management code in GameController class (along with a simple UI to start terrain generation). Now we can freely walk around and travel hundreds of miles of infinite terrain!

Unfortunately it still looks a bit… dull.
Last thing we need to do is to add some textures so our terrain would look like a real thing.
Adding them is very simple – first we need to define a number of textures (called SplatPrototypes) that we’d like to use on our terrain and then we need to specify the amount of each texture for every point on terrain (the number of depends on AlphamapResolution). All this information is entered to TerrainData class that we already know. We’ll store textures in out TerrainChunkSettings class.
In this tutorial I’ve taken two textures – one for flat terrain and one for steep surfaces. The influence of each texture is based on terrain steepness (which we can obtain from TerrainData class after applying elevation data).
Here’s a code snippet:

private void ApplyTextures(TerrainData terrainData)
{
    var flatSplat = new SplatPrototype();
    var steepSplat = new SplatPrototype();

    flatSplat.texture = Settings.FlatTexture;
    steepSplat.texture = Settings.SteepTexture;

    terrainData.splatPrototypes = new SplatPrototype[]
    {
        flatSplat,
        steepSplat
    };

    terrainData.RefreshPrototypes();

    var splatMap = new float[terrainData.alphamapResolution, terrainData.alphamapResolution, 2];

    for (var zRes = 0; zRes < terrainData.alphamapHeight; zRes++)
    {
        for (var xRes = 0; xRes < terrainData.alphamapWidth; xRes++)
        {
            var normalizedX = (float)xRes / (terrainData.alphamapWidth - 1);
            var normalizedZ = (float)zRes / (terrainData.alphamapHeight - 1);

            var steepness = terrainData.GetSteepness(normalizedX, normalizedZ);
            var steepnessNormalized = Mathf.Clamp(steepness / 1.5f, 0, 1f);

            splatMap[zRes, xRes, 0] = 1f - steepnessNormalized;
            splatMap[zRes, xRes, 1] = steepnessNormalized;
        }
    }

    terrainData.SetAlphamaps(0, 0, splatMap);
}

Let’s build it again – now it looks way better (CC0 textures taken from here):

final_effect

Now we’ve got a fully functional terrain that we can walk on and that can be very large in size. Although it is not really infinite – we’ve got a very convincing illusion of it.
That would be all for the basic tutorial. There are a lot of things that could be improved – mostly performance and visual improvements, but we’ll handle them in the another part.
Have fun!

Source project can be downloaded here.