CS409/CS809 Tutorial 11: Perlin Noise

Setting up an OpenGL Project

1.      Start Microsoft Visual Studio 2013.  Select File / New / Project.  Under “Project Type”, choose Templates / Visual C++. Then choose a Win32 Console Application.  Change the name to Tutorial11 or some suitable name and change the location to a hard drive located on the computer (typically C or D).  You will need to remember this location for steps 3 and 5.  Make sure the “Create folder for project” checkbox is not checked and that the “Add to source control” is not checked.  Click OK.

·         If the Chose Location window keeps popping up again, you aren’t allowed to save files at the location you chose.  Select a different location, such as the desktop.

·         Do not put your program on one of the external drives like H.  The connection is slow, and your program may not compile correctly.

2.      Click Next> or Application Settings (they go to the same place).  Check the “Empty Project” box.  Make sure the “Security development lifecycle check” is not checked.  Click Finish.

3.      Go to http://www2.cs.uregina.ca/~anima/409/Terms/201810/Tutorials/index.html and download the Tutorial11.zip and ObjLibrary.zip files to your project directory.

4.      Unzip the 2 zip files.  You will have a folder called ObjLibrary and 8 other files.

5.      In Visual C++, go to Project/Add Existing Item… and select main11.cpp (and holding control, continue to select) and GetGlut.h.

6.      Ensure that Solution Explorer is visible (View / Solution Explorer). In the Solution Explorer, select Tutorial11 (the project name) and right click it.  Choose Add / New Filter.  A new folder icon should appear in the Solution Explorer.  Name it ObjLibrary.  (This creates an imaginary folder in the project, but it would not create a Windows folder on the computer).

7.      In the Solution Explorer, right click on the ObjLibrary folder icon and select Add / Existing item.  Go into the ObjLibrary folder on Windows and select all files.  The files should appear in the Solution Explorer inside the ObjLibrary folder.  You should get 26 files, although you will not be using most of them for this tutorial.

8.      Compile and run the original program.  You should see 2 windows: a text console and a graphical window labeled “Perlin Noise”.  There graphical window is covered with a grid of orange squares.  Exit the windows.  Closing the text window will close the graphical window, and usually vice versa.

9.      For the remainder of the instructions, it is assumed that you compile and run regularly so that you can see what each change does.

Generate a Grid of Pseudorandom Values

10.  Add a global constant to represent the grid size:
const float GRID_SIZE = 8.0f;

11.  Add code to the valueNoise function to determine which grid cell the parameter position is in:
  int x0 = (int)(floor(x / GRID_SIZE));
  int y0 = (int)(floor(y / GRID_SIZE));
We need the floor function so that negative coordinates are always rounded down instead of truncated (rounded towards 0).  Try returning the different grid cells from the valueNoise functions:
  return x0 / 2.0f;
and:
  return y0 / 2.0f;
You should see vertical or horizontal bands of coloured squares.  When the noise value is within the range [-1, 1], colours from red (-1) to yellow (+1) are displayed.  When the noise value is smaller than -1.0 or larger than 1.0, the squares are purple or green.

12.  Add some global constants to use while calculating the pseudorandom values:
const unsigned int SEED_X1 = 1273472206;
const unsigned int SEED_X2 = 4278162623;
const unsigned int SEED_Y1 = 1440014778;
const unsigned int SEED_Y2 =  524485263;
const unsigned int SEED_Q0 = 1498573726;
const unsigned int SEED_Q1 = 3476519523;
const unsigned int SEED_Q2 = 3905844518;

13.  Add a function to calculate a pseudorandom unsigned integer for a grid cell.  It will combine the x0 and y0 values into a single value:
unsigned int pseudorandom (int x, int y)
{
  unsigned int n = (SEED_X1 * x) +

                   (SEED_Y1 * y);
  unsigned int quad_term = SEED_Q2 * n * n +
                           SEED_Q1 * n     +
                           SEED_Q0;
  return quad_term +
         (SEED_X2 * x) +

         (SEED_Y2 * y);
}
Remember to add the function prototype:
unsigned int pseudorandom (int x, int y);

14.  Call the pseudorandom function from the valueNoise function:
  unsigned int value00 = pseudorandom(x0, y0);
Try returning the pseudorandom value, scaled to the range [-1, 1]:
  return ((float)(value00) / UINT_MAX) * 2.0f - 1.0f;
You should see a grid of large squares, each composed of 8x8 smaller squares of a single random colour from red to yellow.

15.  Add a function to simplify scaling the return value:
float unsignedIntTo01 (unsigned int n)
{
  return ((float)(n) / UINT_MAX) * 2.0f - 1.0f;
}
Remember to add the function prototype:
float unsignedIntTo01 (unsigned int n);

16.  Update the valueNoise function to use the unsignedIntTo01 function::
  return unsignedIntTo01(value00);

Calculating Value Noise

17.  Add code to determine where the parameter position is within the grid cell:
  float x_frac = x / GRID_SIZE - x0;
  float y_frac = y / GRID_SIZE - y0;
Try returning the different fractions from the valueNoise function:
  return x_frac;
and:
  return y_frac;
You should see a repeating fade from orange to yellow.  There is no red because the fractions are always in the range [0, 1).

18.  Calculate pseudorandom values for the four corners of the grid cell:
  unsigned int value00 = pseudorandom(x0,     y0);
  unsigned int value01 = pseudorandom(x0,     y0 + 1);
  unsigned int value10 = pseudorandom(x0 + 1, y0);
  unsigned int value11 = pseudorandom(x0 + 1, y0 + 1);

19.  Add some temporary variables to make calculating the corner values faster and easier to read:
  int x1 = x0 + 1;
  int y1 = y0 + 1;
  unsigned int value00 = pseudorandom(x0, y0);
  unsigned int value01 = pseudorandom(x0, y1);
  unsigned int value10 = pseudorandom(x1, y0);
  unsigned int value11 = pseudorandom(x1, y1);

20.  Combine the 4 corner values using bilinear interpolation:
  unsigned int value0 = (unsigned int)(value00 * (1.0f - y_frac)) +
                        (unsigned int)(value01 *         y_frac);
  unsigned int value1 = (unsigned int)(value10 * (1.0f - y_frac)) +
                        (unsigned int)(value11 *         y_frac);
  unsigned int value = (unsigned int)(value0 * (1.0f - x_frac)) +
                       (unsigned int)(value1 *         x_frac);
Scale the interpolated value to the range [-1, 1] and return it:
  return unsignedIntTo01(value);
You should see colours fading between red and yellow here and there.  The yellow and red spots will correspond to where the yellow and red squares were after step 12.

21.  Add a fade function to provide sinusoidal interpolation:
float fade (float n)
{
  return (1 - cos(n * 3.14159265f)) * 0.5f;
}
Remember to add the function prototype:
float fade (float n);

22.  Calculate the fade values in the valueNoise function after the x_frac and y_frac variables:
  float x_fade1 = fade(x_frac);
  float y_fade1 = fade(y_frac);
  float x_fade0 = 1.0f - x_fade1;
  float y_fade0 = 1.0f - y_fade1;

23.  Use the fade values to interpolate between the corner values:
  unsigned int value0 = (unsigned int)(value00 * y_fade0) +
                        (unsigned int)(value01 * y_fade1);
  unsigned int value1 = (unsigned int)(value10 * y_fade0) +
                        (unsigned int)(value11 * y_fade1);
  unsigned int value = (unsigned int)(value0 * x_fade0) +
                       (unsigned int)(value1 * x_fade1);
The resulting noise should look more curved and have results farther from 0 (more red and yellow, less orange).

Calculating Perlin Noise

24.  Make a copy of the valueNoise function and name it perlinNoise.  Remember to add a function prototype:
float perlinNoise (float x, float y);
Remove the
value* variables from the perlinNoise function.  For now, it should always return 0.0f.

25.  Add an #include for Vector2 and a using declaration for the ObjLibrary at the top of the file:
#include "ObjLibrary/Vector2.h"
using namespace ObjLibrary;
A Vector2 has, among other things, fields to represent x and y components and an initializing constructor.

26.  Add a function to calculate a pseudorandom unit vector, called a lattice vector, for a cell:
Vector2 lattice (int x, int y)
{
  unsigned int value = pseudorandom(x, y);
  float radians = (float)(value);  // very random
  return Vector2(cos(radians), sin(radians));
}
Remember to add the function prototype:
Vector2 lattice (int x, int y);

27.  Add Vector2s to the perlinNoise function to store the lattice vector to each corner:
  Vector2 lattice00 = lattice(x0, y0);
  Vector2 lattice01 = lattice(x0, y1);
  Vector2 lattice10 = lattice(x1, y0);
  Vector2 lattice11 = lattice(x1, y1);

28.  Add Vector2s to the perlinNoise function to store the direction vector to each corner:
  Vector2 direction00(     - x_frac,      - y_frac);
  Vector2 direction01(     - x_frac, 1.0f - y_frac);
  Vector2 direction10(1.0f - x_frac,      - y_frac);
  Vector2 direction11(1.0f - x_frac, 1.0f - y_frac);
Calculate the dot products for each corner of the grid cell in the perlinNoise function:
  float value00 = (float)(lattice00.dotProduct(direction00));
  float value01 = (float)(lattice01.dotProduct(direction01));
  float value10 = (float)(lattice10.dotProduct(direction10));
  float value11 = (float)(lattice11.dotProduct(direction11));

29.  Interpolate between the dot products to calculate the final noise value:
  float value0 = value00 * y_fade0 +
                 value01 * y_fade1;
  float value1 = value10 * y_fade0 +
                 value11 * y_fade1;
  float value = value0 * x_fade0 +
                value1 * x_fade1;
This interpolation is different because the values are floats instead of unsigned ints.  The noise value is the final interpolated value:
  return value;
Note that the final interpolated value is already a float in the range [-1, 1], so we don't have to convert it.

30.  Change the display function to call perlinNoise instead of valueNoise:
                 float noise_at = perlinNoise(x0, y0);
You should see smaller, more irregular colour fades.  They will also stay closer to orange.

Exercises

31.  Change the grid size to 16.  Then to 2.  Then to 4.  Then back to 8.  What effect does this have on the noise?

32.  Change the fade function to use -2n^3 + 3n^2 as the interpolation formula.  Then change it to use 6n^5 - 15n^4 + 10n^3.  Then change it to just return n.  Choose whichever formula you think produces the nicest-looking noise.

33.  Convert the noise field to a class called NoiseField with its own .h and .cpp files.  The grid size and seed constants should become member variables (not constants).  Add an initializing constructor or init function to set these variables.  Convert the perlinNoise and valueNoise functions to public member functions and pseudorandom, unsignedIntTo01, fade, lattice, and dotProduct to private helper functions.  Update main.cpp to use a NoiseField variable.

34.  Calculate fractal perlin noise.  Replace the NoiseField variable with an array of 4 NoiseFields.  Initialize each one with different seed values and grid sizes of 16, 8, 4, and 2.  In the draw function, calculate the sum of the noise fields.  You will see spots of purple and green, corresponding to values less than -1.0 and more than 1.0.  Why does this happen?

·         You can generate seed values with the rand function.  However, you will have to convert them from the range [0, RAND_MAX] to the range [0, UINT_MAX]:
  double random01 = rand() / (RAND_MAX + 1.0);
  unsigned int seed = (unsigned int)(random01 * UINT_MAX);

·         Note: If you ever need high quality random numbers, you can generate them (in hexadecimal) here:
https://www.random.org/integers/?num=14&min=0&max=65535&col=2&base=16&format=plain&rnd=new.  You will have to prefix 0x to the numbers and remove the tabs from the middle.

35.  Add another float member variable to the NoiseField class to represent the noise amplitude.  The value noise and perlin noise functions should return results multiplied by the amplitude.  Set the amplitudes for your 4 layers of noise to 1, 0.5, 0.25, and 0.125.