In this step we will add some more detail to the terrain. What we have now is the basis but it lacks variation.

We will do this by adding more layers of perlin noise to the existing layer. Furthermore the height map will be normalized before passing it to Unity.

1 Add another height variable in the GeneratePerlinTerrain() function (height2), call PerlinNoise() again but divide the position by 50 instead of 200 – this makes the resulting height map more dense; furthermore divide the result of PerlinNoise() by 4, so the height is about a fourth of the main terrain. For testing assign the new variable to the height map instead of the old variable:

using System.Collections;
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    // ...

    public void GeneratePerlinTerrain()
    {
        for (int z = 0; z < _terrainData.heightmapHeight; ++z)
        {
            for (int x = 0; x < _terrainData.heightmapWidth; ++x)
            {
                float height1 = 0;
                float height2 = 0;

                height1 = Mathf.PerlinNoise(x / 200f, z / 200f);
                height2 = Mathf.PerlinNoise(x / 50f, z / 50f) / 4;
                _heights[x, z] = height2;
            }
        }
    }

    // ..
}

When you run the scene the result should look like this:

As you can see the height map looks quite different from the first height map we produced.

What we do now is to add those two height maps to produce a more detailed height map. Change the assignment of height2 to the height map to the sum of height1 and height2:

using System.Collections;
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    // ...

    public void GeneratePerlinTerrain()
    {
        for (int z = 0; z < _terrainData.heightmapHeight; ++z)
        {
            for (int x = 0; x < _terrainData.heightmapWidth; ++x)
            {
                float height1 = 0;
                float height2 = 0;

                height1 = Mathf.PerlinNoise(x / 200f, z / 200f);
                height2 = Mathf.PerlinNoise(x / 50f, z / 50f) / 4;
                _heights[x, z] = height1 + height2;
            }
        }
    }

    // ...
}

The result should look like this:

You can add as many layers as you like but keep in mind that every layer takes time to compute and therefore the generation of the map will take longer.

2 When you add more layers, you will notice that the terrain becomes flattened on the peaks. It could look like this:

The reason is simple: Unity can only handle values between 0 and 1 for the heights of a height map. When you add layers you sooner or later will get values that are larger than 1. What we have to do is called normalization. That means, that we have to squeeze the height map after generation so that all values are in the range between 0 and 1.

Add a new function named NormalizeHeightMap() to your class. Call the function in Start() right after the generation of the map. Declare two local variables (minValue and maxValue) that will hold the minimum and maximum values of the height map. Then iterate through the whole map and store the minimum and maximum values:

using System.Collections;
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    // ...

    private void NormalizeHeightMap()
    {
        float minValue = float.MaxValue;
        float maxValue = float.MinValue;

        for (int z = 0; z < _terrainData.heightmapHeight; ++z)
        {
            for (int x = 0; x < _terrainData.heightmapWidth; ++x)
            {
                if (_heights[x, z] < minValue) minValue = _heights[x, z];
                if (_heights[x, z] > maxValue) maxValue = _heights[x, z];
            }
        }
    }

    private void Start()
    {
        _terrain = GetComponent();
        _terrainData = _terrain.terrainData;
        _heights = new float[_terrainData.heightmapWidth, _terrainData.heightmapHeight];
        GeneratePerlinTerrain();
        NormalizeHeightMap();
        _terrainData.SetHeights(0, 0, _heights);
    }

    // ...
}

 

3 After that iterate through the height map again and subtract the min value from every value in the map (so we get the map zero based) and divide each height by the difference between maximum and minimum value:

using System.Collections;
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    // ...

    private void NormalizeHeightMap()
    {
        float minValue = float.MaxValue;
        float maxValue = float.MinValue;

        for (int z = 0; z < _terrainData.heightmapHeight; ++z)
        {
            for (int x = 0; x < _terrainData.heightmapWidth; ++x)
            {
                if (_heights[x, z] < minValue) minValue = _heights[x, z];
                if (_heights[x, z] > maxValue) maxValue = _heights[x, z];
            }
        }

        float diff = maxValue - minValue;

        for (int z = 0; z < _terrainData.heightmapHeight; ++z)
        {
            for (int x = 0; x < _terrainData.heightmapWidth; ++x)
            {
                float height = _heights[x, z];
                height -= minValue;
                height /= diff;
                _heights[x, z] = height;
            }
        }
    }

    // ...
}

Now the result should look like this – the flattened areas should have disappeared: