Map Generation Tutorial Part 1: Perlin Noise


Welcome to this tutorial on map generation. For this tutorial I will be using Perlin noise. Perlin noise is a type of gradient noise that was developed by Ken Perlin in 1983. It is widely used in computer graphics to create natural-looking textures, clouds, terrain, and many other procedural generated structures, and today I will show you how to use it to make ASCII land masses.

The first thing we need to understand is how to generate Perlin noise. This code generates Perlin noise, it takes in x and y values and returns a double value, which can be used to generate a heightmap or other types of terrain maps.

public static double PerlinNoise(double x, double y) 
{
     double total = 0;
     double p = .25; // amplitude control
     int n = 1; // frequency control
     for (int i = 0; i <= n; i++)
     {
         double frequency = Math.Pow(2, i);
         double amplitude = Math.Pow(p, i);
         total += InterpolatedNoise(x * frequency, y * frequency) * amplitude;
     }
      return total; 
} 

The PerlinNoise function takes in x and y values and returns a double value representing the Perlin noise at that point. The total variable keeps track of the total noise value, which is the sum of the noise values at different frequencies. The p variable is used to control the amplitude of each frequency, and the n variable controls the number of frequencies used.

The function loops over each frequency and calculates the noise value at that frequency by calling the InterpolatedNoise function. The frequency and amplitude values for each iteration are calculated using the Math.Pow function. The total variable is updated by adding the noise value multiplied by the amplitude.

The function then returns the final total value, which represents the Perlin noise at the given x and y coordinates.

static double InterpolatedNoise(double x, double y) 
{     
     int integer_X = (int)x;
     double fractional_X = x - integer_X;
     int integer_Y = (int)y;
     double fractional_Y = y - integer_Y;
     double v1 = SmoothedNoise(integer_X, integer_Y);
     double v2 = SmoothedNoise(integer_X + 1, integer_Y);
     double v3 = SmoothedNoise(integer_X, integer_Y + 1);
     double v4 = SmoothedNoise(integer_X + 1, integer_Y + 1);
     double i1 = Interpolate(v1, v2, fractional_X);
     double i2 = Interpolate(v3, v4, fractional_X);
     return Interpolate(i1, i2, fractional_Y); 
} 

The InterpolatedNoise function uses the SmoothedNoise function to generate four noise values, which are then interpolated to generate the final noise value. The x and y coordinates are split into integer and fractional parts, which are used to calculate the noise values of the four corners of the grid cell. The fractional parts are then used to interpolate the noise values of the four corners to get the noise value of the point.

The InterpolatedNoise function takes in x and y values and returns a double value representing the noise at that point, using bilinear interpolation to smooth out the noise. The integer_X and integer_Y variables represent the integer coordinates of the nearest corner of the grid cell containing the point, and fractional_X and fractional_Y represent the fractional distance from that corner to the point.

The function then calculates the smoothed noise values for the four corners of the grid cell using the SmoothedNoise function. The smoothed noise values are then interpolated using the Interpolate function to calculate the final noise value at the given point.

static double Interpolate(double a, double b, double x) 
{
     double ft = x * Math.PI;
     double f = (1.5 - Math.Cos(ft)) * 0.5;
      return a * (1 - f) + b * f; 
}

The Interpolate function takes in two noise values (a and b) and a distance value (x) and returns an interpolated value between them. In this implementation, the function uses a cosine interpolation method to produce smooth results.

static double SmoothedNoise(int x, int y) 
{
     double corners = (Noise(x - 1, y - 1) + Noise(x + 1, y - 1) +
                       Noise(x - 1, y + 1) + Noise(x + 1, y + 1)) / 16;
     double sides = (Noise(x - 1, y) + Noise(x + 1, y) +
                     Noise(x, y - 1) + Noise(x, y + 1)) / 8;
     double center = Noise(x, y) / 4;
     return corners + sides + center; 
}

The SmoothedNoise function takes in a coordinate pair (x, y) and returns the average of noise values sampled from its neighboring coordinates, weighted to produce smoother results. Specifically, it takes the average of the four diagonal corner coordinates, the four side coordinates, and the central coordinate. The noise values at each of these coordinates are calculated using the Noise function.

static double Noise(int x, int y) 
{     
    int n = x + y * 57;
    n = (n << 13) ^ n;
    int t = (int)(random.NextDouble() * int.MaxValue); 
    t = (t * (t * t * 15731 + 789221) + 1376312589) & 0x7fffffff;      
    return 1.0 - (double)t * 0.931322574615478515625e-9; 
} 

The Noise function takes in a coordinate pair (x, y) and uses it to generate a pseudo-random value. In this implementation, it uses a hash function that takes the bitwise XOR of the coordinate value with a constant (57) and then uses a pseudo-random number generator to generate another value, which is then hashed using a polynomial equation. Finally, the result is scaled and returned as a double value between 0 and 1.

To use the Perlin noise code in your own program, you would need to first copy the code into your C# project, then you can use the PerlinNoise function by calling it and passing in the desired x and y values. The function will return a double value representing the calculated noise value at that location, which you would then use to assign a character to a 2D array based on that value.

Here's an example program that uses the PerlinNoise method to generate an ASCII map stored in a 2D array of characters:

using System;
class Program 
{
     static void Main(string[] args)
     {
         int mapWidth = 50;
         int mapHeight = 20;
         double scale = 0.05;
         char[,] asciiMap = new char[mapHeight, mapWidth];
         for (int y = 0; y < mapHeight; y++)
         {
             for (int x = 0; x < mapWidth; x++)
             {
                 double noiseValue = PerlinNoise(x * scale, y * scale);
                 char character = ' ';
                 if (noiseValue > 0.5)
                 {
                     character = '#';
                 }
                 else if (noiseValue > 0.3)
                 {
                     character = '-';
                 }
                 else if (noiseValue > 0.1)
                 {
                     character = '.';
                 }
                 asciiMap[y, x] = character;
             }
         }
         for (int y = 0; y < mapHeight; y++) 
         {
             for (int x = 0; x < mapWidth; x++)
             {
                 Console.Write(asciiMap[y, x]);
             }
             Console.WriteLine();
         }
     }

The final product is pretty random and may fit your use case.. but for me this is not what I wanted.. I wanted to create an island or continent  for my world and I will be showing you how to do that in part 2 of this tutorial. Thanks for joining me, I hope you have learned something.

Leave a comment

Log in with itch.io to leave a comment.