Procedural generation and Perlin noise in Unity

Tulenber 17 April, 2020 ⸱ Intermediate ⸱ 10 min ⸱ 2019.3.9f1 ⸱

This post continues a cycle about endless world creation, in which we will get acquainted with the basics of procedural generation.

It seems that quite a large part of people come to the gaming industry for creative reasons. One of the directions of such creativity can be the creation of the worlds. Moreover, if you turn to the Internet with a question on this topic, you will find a relatively large number of articles, which can serve as confirmation of the great desire of people to become demiurges. This post will follow in the footsteps of many predecessors and talk about one of the simplest and most basic ways to create a small, but still your piece of the universe, named “Perlin Noise.”

Procedural Generation

From the programmer view, there are two ways to create any subject: to make it with your hands or write code to generate it. Although most likely, there is only the second and the first, for some reason, used by everyone else. First, let’s define basic concepts:

  • Procedural generation(PCG) - a common name for mechanisms for automatically creating content using an algorithm. This concept includes the creation of everything from music to the rules of the game.
  • Pseudorandom number generator(PRNG) - an algorithm that generates a sequence of numbers whose elements are almost independent of each other and obey a given distribution.
  • Seed - determines the sequence of numbers to be issued by the PRNG.

If you need a small amount of content, then, of course, the easiest way is to create it with your hands (arrange trees on a map or draw a texture). However, if we are talking about vast volumes of material, then procedural generation can be a more profitable way. The principle is quite simple when you need something you get the PRNG, the seed, additional input data, and some algorithm for creating the necessary thing and generate a unique unit of content. If any changes occur after generation, then you save them separately. Subsequently, if you need to repeat the object, knowing the generating element, the input data, and the performed changes, you can reproduce its generation and get the given object.

It is a general description of the scheme, which, using various algorithms, is suitable for almost any task, starting from music generation, continuing with fire textures, and ending with animal generation for some planet in No Man’s Sky. In this post, we will consider the purpose of procedural generation to the surface of the earth.

Make our land

One of the universal ways to describe the earth’s surface is to use a heightmap. It is a two-dimensional array with data on the height of points on the ground. If we take a vertical slice from such a map, we get this view:
Elevation Profile

Accordingly, if we manage to generate such a map of heights, we can create a landscape from it. Suppose we generate points through a regular Random and get a graph like this:
Random generator

If you look in a two-dimensional form, the generated map will look something like this:
White noise

It should be clear from the picture that the usual PRNG is not very suitable for creating a map because white noise does not imply dependence on nearby points. Therefore, we get significant surface-level differences that are not very similar to the real surface.

Perlin Noise

To generate the related data, we will use a PRNG called Perlin Noise, which will create a picture like that:
Perlin noise

We will not delve into the principles of the algorithm itself; for more information, refer to this article or this one.

Concepts that we will need for further work:

  • Amplitude - the maximum value of the offset or change of the variable from the average value.
  • Frequency - the characteristic of a periodic process is equal to the number of repetitions or occurrence of events (operations) per unit time.
  • Lacunarity - controls the change in frequency.
  • Persistence - controls the change in amplitude.
  • Octave - samples with different frequencies.

Base

We will demonstrate the principle of operation of the three-octave algorithm:
Lacunarity = 2;
Persistence = 0.5;

The first octave sets the main elevation (sea-mountains)
Frequency = Lacunarity ^ 0 = 1
Amplitude = Persistence ^ 0 = 1
Octave one

The second octave sets the average surface drops (boulders)
Frequency = Lacunarity ^ 1 = 2
Amplitude = Persistence ^ 1 = 0.5
Octave two

The third octave defines small surface drops (small stones)
Frequency = Lacunarity ^ 2 = 4
Amplitude = Persistence ^ 2 = 0.25
Octave three

We put the octaves on top of each other, and as a result, we get something similar to a real height map in section:
Result

Implementation

The basis for this implementation was the video series "Procedural Terrain Generation" from Sebastian Lague.

Make three scripts

  1. NoiseMapGenerator - responsible for generating the heightmap using Perlin Noise
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using UnityEngine;

public static class NoiseMapGenerator
{
    public static float[] GenerateNoiseMap(int width, int height, int seed, float scale, int octaves, float persistence, float lacunarity, Vector2 offset)
    {
        // An array of vertex data, a one-dimensional view will help get rid of unnecessary cycles later
        float[] noiseMap = new float[width*height];

        // Seed element
        System.Random rand = new System.Random(seed);

        // Octave shift to get a more interesting picture with overlapping
        Vector2[] octavesOffset = new Vector2[octaves];
        for (int i = 0; i < octaves; i++)
        {
            // Also use external position shift
            float xOffset = rand.Next(-100000, 100000) + offset.x;
            float yOffset = rand.Next(-100000, 100000) + offset.y;
            octavesOffset[i] = new Vector2(xOffset / width, yOffset / height);
        }

        if (scale < 0)
        {
            scale = 0.0001f;
        }

        // For a more visually pleasant zoom shift our view to the center
        float halfWidth = width / 2f;
        float halfHeight = height / 2f;

        // Generate points for a heightmap
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Set the values for the first octave
                float amplitude = 1;
                float frequency = 1;
                float noiseHeight = 0;
                float superpositionCompensation = 0;

                // Octave Overlay Processing
                for (int i = 0; i < octaves; i++)
                {
                    // Calculate the coordinates to get from Noise Perlin
                    float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency;
                    float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency;

                    // Obtaining Altitude from the PRNG
                    float generatedValue = Mathf.PerlinNoise(xResult, yResult);
                    // Octave overlay
                    noiseHeight += generatedValue * amplitude;
                    // Compensate octave overlay to stay in a range [0,1]
                    noiseHeight -= superpositionCompensation;

                    // Calculation of amplitude, frequency and superposition compensation for the next octave
                    amplitude *= persistence;
                    frequency *= lacunarity;
                    superpositionCompensation = amplitude / 2;
                }

                // Save heightmap point
                // Due to the superposition of octaves, there is a chance of going out of the range [0,1]
                noiseMap[y * width + x] = Mathf.Clamp01(noiseHeight);
            }
        }

        return noiseMap;
    }
}
  1. NoiseMap - links generator and renderer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using UnityEngine;

public enum MapType
{
    Noise,
    Color
}

public class NoiseMap : MonoBehaviour
{
    // Input data for our noise generator
    [SerializeField] public int width;
    [SerializeField] public int height;
    [SerializeField] public float scale;

    [SerializeField] public int octaves;
    [SerializeField] public float persistence;
    [SerializeField] public float lacunarity;

    [SerializeField] public int seed;
    [SerializeField] public Vector2 offset;

    [SerializeField] public MapType type = MapType.Noise;

    private void Start()
    {
        GenerateMap();
    }

    public void GenerateMap()
    {
        // Generate a map
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);

        // Pass the map to the renderer
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.RenderMap(width, height, noiseMap, type);
    }
}
  1. NoiseMapRenderer - creates texture for map display
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using System.Collections.Generic;
using UnityEngine;

public class NoiseMapRenderer : MonoBehaviour
{
    [SerializeField] public SpriteRenderer spriteRenderer = null;

    // Determining the coloring of the map depending on the height
    [Serializable]
    public struct TerrainLevel
    {
        public string name;
        public float height;
        public Color color;
    }
    [SerializeField] public List<TerrainLevel> terrainLevel = new List<TerrainLevel>();

    // Depending on the type, we draw noise either in black and white or in color
    public void RenderMap(int width, int height, float[] noiseMap, MapType type)
    {
        if (type == MapType.Noise)
        {
            ApplyColorMap(width, height, GenerateNoiseMap(noiseMap));
        }
        else if (type == MapType.Color)
        {
            ApplyColorMap(width, height, GenerateColorMap(noiseMap));
        }
    }

    // Create texture and sprite to display
    private void ApplyColorMap(int width, int height, Color[] colors)
    {
        Texture2D texture = new Texture2D(width, height);
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;
        texture.SetPixels(colors);
        texture.Apply();

        spriteRenderer.sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100.0f);;
    }

    // Convert an array with noise data into an array of black and white colors, for transmission to the texture
    private Color[] GenerateNoiseMap(float[] noiseMap)
    {
        Color[] colorMap = new Color[noiseMap.Length];
        for (int i = 0; i < noiseMap.Length; i++)
        {
            colorMap[i] = Color.Lerp(Color.black, Color.white, noiseMap[i]);
        }
        return colorMap;
    }

    // Convert an array with noise data into an array of colors, depending on the height, for transmission to the texture
    private Color[] GenerateColorMap(float[] noiseMap)
    {
        Color[] colorMap = new Color[noiseMap.Length];
        for (int i = 0; i < noiseMap.Length; i++)
        {
            // Base color with the highest value range
            colorMap[i] = terrainLevel[terrainLevel.Count-1].color;
            foreach (var level in terrainLevel)
            {
                // If the noise falls into a lower range, then use it
                if (noiseMap[i] < level.height)
                {
                    colorMap[i] = level.color;
                    break;
                }
            }
        }

        return colorMap;
    }
}
  1. Create an object with new scripts and the Sprite Renderer component

Game object

  1. Set palette for heightmap

Terrain levels

Result

As a result, we got a heightmap that looks like a separate part of the landscape.
Result map

Conclusion

We got acquainted with the basic concepts of procedural generation and Perlin Noise, and also generated a map of heights with it, which can resemble a real landscape. It is a straightforward algorithm that allows you to achieve acceptable results in a short time. The most obvious application for which would be overlaying its values on an object like Plane and thus creating a 3D landscape, as Sebastian did in his video series. However, this is just a basic algorithm that does not count many parameters when creating a landscape, climate, for example. Also, the independence of calculations, allows you to create infinite space but produces a fairly similar result. In the end, it does not allow you to create maps divided into continents or islands without additional mechanisms. Nevertheless, in the following articles of this series, we will try to apply this basic algorithm for something more interesting, because for the most part, the main limitation is only our imagination. After all, even Minecraft was originally based on Perlin Noise. See you next time! =)

Bonus

One of my favorite articles on the same topic, and in general, this blog contains a lot of interesting information on the development of game mechanics.



Privacy policyCookie policyTerms of service
Tulenber 2020