Paint.NET - Perlin Noise for 2-Dimensional Clouds

Upon browsing the Paint.NET Forums, I ran across someone looking to have a Photoshop tutorial converted to a Paint.NET tutorial. The first thing this tutorial showed was to use Filter -> Render -> Clouds. Currently, there is no “Cloud” effect within Paint.NET, so I set out to figure out how to mathematically generate clouds. After searching the Google Groups, I ran across something called “Perlin Noise,” created by Ken Perlin to generate textures for Tron. More searching led me to a few web sites explaining Perlin Noise and its features. From there I converted a few of the functions to work within the Paint.NET Code Lab, and I present that to you today.

There are five functions in the Paint.NET Code Lab implementation of Perlin Noise: Render, PerlinNoise2d, Smooth, Noise, and Interpolate.

The Render function looks as follows:

void Render(Surface dst, Surface src, Rectangle rect)
{
  for(int y = rect.Top; y < rect.Bottom; y++)
  {
    for (int x = rect.Left; x < rect.Right; x++)
    {
      ColorBgra srcBgra = src[x, y];
      byte c = (byte)(PerlinNoise2d(x, y) * 255);
      dst[x, y] = ColorBgra.FromBgra(srcBgra.B, srcBgra.G, srcBgra.R, c);
    }
  }
}

This is the entry point, from the Code Lab, to the drawing surface in Paint.NET. The Render function takes three parameters, the destination Surface, the source Surface, and the drawing area (or invalid) Rectangle. The function does not return a parameter.

What this function does is loop through each pixel in the drawing area, retrieves the source pixel information (Red, Green, Blue, Alpha), sets a byte (0 – 255) based on the result of the PerlinNoise2d function on the given pixel location, and sets the destination pixel by adjusting the alpha level for a “cloud”-like effect while keeping the source color.

This function can be adjusted depending on what you would like to do. For example, to only draw clouds in a transparent space, you can add an if-statement to check the transparency on the source pixel before setting the destination pixel. Then, line 9 from above (the last line in the for-loop) would become:

dst[x, y] = (0 == srcBgra.A) ? ColorBgra.FromBgra(srcBgra.B, srcBgra.G, srcBgra.R, c) : src[x, y];

Next, we look inside the PerlinNoise2d function. This is where all the magic happens:

double PerlinNoise2d(int x, int y)
{
  double total = 0.0;
  
  double frequency   = .015; // USER ADJUSTABLE
  double persistence = .65;  // USER ADJUSTABLE
  double octaves     = 8;    // USER ADJUSTABLE
  double amplitude   = 1;    // USER ADJUSTABLE
  
  for(int lcv = 0; lcv < octaves; lcv++)
  {
    total = total + Smooth(x * frequency, y * frequency) * amplitude;
    frequency = frequency * 2;
    amplitude = amplitude * persistence;
  }
  
  double cloudCoverage = 0; // USER ADJUSTABLE
  double cloudDensity  = 1; // USER ADJUSTABLE
  
  total = (total + cloudCoverage) * cloudDensity;
  
  if(total < 0) total = 0.0;
  if(total > 1) total = 1.0;
  
  return total;
}

As you can see, there is a lot going on within this function. Simply put, PerlinNoise2d takes in an (x, y) coordinate and returns a double value between 0 and 1, which gets expanded in our Render function to the range of 0 – 255. But, there are a lot of user adjustable variables which can vary the output.

Frequency gives you number of noise values defined between each 2-dimensional point.

Persistence is a constant multiplier adjusting our amplitude in each iteration.

Octaves define the number of iterations over the noise function for a particular point. With each iteration, the frequency is doubled and the amplitude is multiplied by the persistence.

Amplitude is the maximum value added to the total noise value.

Those variables act within the for-loop to generate a cumulative total. There are two other user adjustable variables that act upon the total after the for-loop that help in defining the finished look of your scene.

Cloud Coverage is a constant that gets added (or subtracted) to each total. The default value is zero, meaning that the total should not be altered. Adding values will increase the size of the clouds, while subtracting will reduce them.

Cloud Density is a constant that gets multiplied with the total to increase or decrease the apparent thickness of the clouds. The default value is 1, meaning the totalshould not be altered. Any number between 0 and 1 will reduce the apparent density (making the clouds appear more like fog), while any number greater than 1 will increase the density, and -1 will invert the clouds.

The actual call to the Noise function is buried within the Smooth function called within the for-loop. Since we have double values for our x and y coordinates, our Smoothfunction interpolates the noise value by using the four corners as known data points.

double Smooth(double x, double y)
{
  double n1 = Noise((int)x, (int)y);
  double n2 = Noise((int)x + 1, (int)y);
  double n3 = Noise((int)x, (int)y + 1);
  double n4 = Noise((int)x + 1, (int)y + 1);
  
  double i1 = Interpolate(n1, n2, x - (int)x);
  double i2 = Interpolate(n3, n4, x - (int)x);
  
  return Interpolate(i1, i2, y - (int)y);
}

Say we are given an (x, y) coordinate as shown in the graph. Noise values are only known for integer locations per our Noisefunction below. So, the Smooth function needs to approximate a value for the coordinate we were given based on the known values of the corner points (labeled v1, v2, v3, and v4 in the graph). Once we get our noise values for the corners, we have to interpolate the values to approximate (x, y). In this case, interpolating the values in the x-direction (done by passing the fractional value of x as the third parameter to our Interpolate function), we receive a value that is closer to the right side of the graph. And interpolating in the y-direction (done by passing the fractional value of y as the third parameter to ourInterpolate function), we receive a value that is closer to the top of the graph. After interpolating in the x- and y-directions, we have a noise value for (x, y) that we can return.

The Noise function acts as a semi-random number generator. Instead of returning a random number each time it is called, it returns a random number based on the input parameters. If you pass the same parameters, you get the same result. TheNoise function is as follows:

static Random r = new Random();
int r1 = r.Next(1000, 10000);
int r2 = r.Next(100000, 1000000);
int r3 = r.Next(1000000000, 2000000000);

double Noise(int x, int y)
{
  int n = x + y * 57;
  n = (n<<13) ^ n;
  
  return ( 1.0 - ( (n * (n * n * r1 + r2) + r3) & 0x7fffffff) / 1073741824.0);
}

The Noise function above will return a value between -1 and 1 for each (x, y) coordinate. Since this is only a semi-random number generator, having set values for r1, r2, and r3 (original values: 15731, 789221, 1376312589) will generate the same design each time. I have modified the function to set random values for r1, r2, and r3 so that we can generate a new cloud pattern each time the code is "built" in the Code Lab. The most important part of the Noise function is the return statement. By performing a bitwise AND operation with the Hexadecimal value 0×7FFFFFFF, the largest value possible is 0×80000000 (or 2147483648). Dividing by 1073741824.0 gives us a maximum value of 2.0 and a minimum value of 0.0, subtracting from 1.0, we return a value between -1 and 1.

In the Interpolate function, we are using a standard cosine interpolation formula:

double Interpolate(double x, double y, double a)
{
  double val = (1 - Math.Cos(a * Math.PI)) * .5;
  return  x * (1 - val) + y * val;
}

That completes the walkthrough of my Perlin Noise function for generating 2-dimensional clouds. Enjoy!

Examples

Download the C# Source Code Segment for Paint.NET’s Code Lab

Perlin Noise Sources