Have you ever noticed how procedural many things in nature appear? Clouds are a perfect example of this. Even though all clouds are unique, they really just look like slight variations of one another. Wanna make some clouds so you can stay glued to your computer without having to go outside to look at them? I will show you how to procedurally generate clouds and render them using OpenGL.
We will use Perlin Noise to make clouds. Ken Perlin developed Perlin Noise in the 80s. A Perlin Noise function is a seeded pseudo random number generator. The noise function will always give the same result for the same seed. The random values between two seeds will also smoothly interpolate between one another. These two features make Perlin Noise perfect for the procedural generation of anything with a pseudo random appearance. Clouds are an ideal example! Ken Perlin has written a great tutorial on Perlin Noise. It is available at http://www.noisemachine.com/talk1/.
I will not explain too much math in this tutorial. The code will be without any optimization or unusual techniques, so you can grasp the general steps involved in a fast breezy manner. I am assuming you know how to initialize OpenGL and already understand texture mapping. The code presented is easily portable.
Let us get started!
A basic noise map 32x32
float map32[32 * 32];
We start by declaring an array of size 32*32 as our basic noise map. This noise map is a building block to form.....
The cloud map
float map256[256 * 256];
The cloud map will hold our cloud.
Random noise generator
Next, we set random noise values ranging from -1 to 1 into our 32*32 map. First, we need a noise generator. Here is a popular one.
float Noise(int x, int y, int random)
{
int n = x + y * 57 + random * 131;
n = (n<<13) ^ n;
return (1.0f - ( (n * (n * n * 15731 + 789221) +
1376312589)&0x7fffffff)* 0.000000000931322574615478515625f);
Note the function takes in a random integer to generate different noise patterns.
Set noise to map
Now our function to set noise for the 32*32 noise map:
void SetNoise(float *map)
{
float temp[34][34];
We declare a temporary array to make our function cleaner. Why does the array hold 34*34 elements instead of 32*32? This is because we will need extra elements for side and corner mirroring, as we shall see soon.
int random=rand() % 5000;
for (int y=1; y<33; y++)
for (int x=1; x<33; x++)
{
temp[x][y] = 128.0f + Noise(x, y, random)*128.0f;
}
Here we insert the noise values one by one into the temporary array. Each time the function is called, a different set of noise values is generated. The color values of our cloud range from 0 to 256.
Seamless cloud
for (int x=1; x<33; x++)
{
temp[0][x] = temp[32][x];
temp[33][x] = temp[1][x];
temp[x][0] = temp[x][32];
temp[x][33] = temp[x][1];
}
temp[0][0] = temp[32][32];
temp[33][33] = temp[1][1];
temp[0][33] = temp[32][1];
temp[33][0] = temp[1][32];
We mirror the side and corner elements so our final cloud will be seamless without any ugly borders showing.
Smooth...
for (int y=1; y<33; y++)
for (int x=1; x<33; x++)
{
float center = temp[x][y]/4.0f;
float sides = (temp[x+1][y] + temp[x-1][y] + temp[x][y+1] + temp[x][y-1])/8.0f;
float corners = (temp[x+1][y+1] + temp[x+1][y-1] + temp[x-1][y+1] + temp[x-1][y-1])/16.0f;
map32[((x-1)*32) + (y-1)] = center + sides + corners;
}
}
Finally, we take the values from the center, corners and sides. We weigh and average them up to give the noise a smoother look. This is our first smoothing process.
Making the noise less blocky
The current noise map consists of a bunch of pixels with random values. There is too much noise change between neighboring pixels, though. To solve this problem we will use a second smoothing process known as interpolation. We will average the value of each pixel value with that of its neighbors' values.
float Interpolate(float x, float y, float *map)
{
int Xint = (int)x;
int Yint = (int)y;
float Xfrac = x - Xint;
float Yfrac = y - Yint;
Parameter x and y are float indices between two neighboring integer indices. We obtain them by scaling integer indices with an octave factor, this we shall see later.
int X0 = Xint % 32;
int Y0 = Yint % 32;
int X1 = (Xint + 1) % 32;
int Y1 = (Yint + 1) % 32;
Next, we define neighboring integer indices. We applied modulus because the noise map row and column consist of only 32 dots.
float bot = map[X0*32 + Y0] + Xfrac * (map[X1*32 + Y0] - map[X0*32 + Y0]);
float top = map[X0*32 + Y1] + Xfrac * (map[X1*32 + Y1] - map[X0*32 + Y1]);
return (bot + Yfrac * (top - bot));
}
Finally we get noise values from the neighboring indices, average them with delta and return a blunted noise value located at float x and y indices.
Overlap the octaves
Now we will make a couple of noise layers called octaves. The first octave is a blowup of a single 32*32 noise map to a 256*256 map. The second octave is a blowup of four 32*32 maps to four 128*128 maps which are tiled together. This process goes on for higher octaves.
The octaves are then overlapped together to give our cloud more turbulence. We will use four octaves for our cloud. You can use more octaves if you like.
void OverlapOctaves(float *map32, float *map256)
{
for (int x=0; x<256*256; x++)
{
map256[x] = 0;
}
We start working with the 256*256 map by clearing its old values
for (int octave=0; octave<4; octave++)
for (int x=0; x<256; x++)
for (int y=0; y<256; y++)
{
float scale = 1 / pow(2, 3-octave);
float noise = Interpolate(x*scale, y*scale , map32);
Here we scale the x and y indices with the values of 1/8, 1/4, 1/2 and 1 consisting of four octaves. The scaled x, y indices and 32*32 map are then sent as parameters for interpolation to return a smoother noise value.
map256[(y*256) + x] += noise / pow(2, octave);
}
}
The octaves are added together with the proper weight factors.
You could replace pow(2, i) with 1<
Filter the noise with exponential function
This is the last function we need to get a cloud! We use an exponential filter to make the 256*256 noise map look more like a cloud.
void ExpFilter(float *map)
{
float cover = 20.0f;
float sharpness = 0.95f;
for (int x=0; x<256*256; x++)
{
float c = map[x] - (255.0f-cover);
if (c<0) c = 0;
map[x] = 255.0f - ((float)(pow(sharpness, c))*255.0f);
}
}
Putting it all together
float map32[32 * 32];
float map256[256 * 256];
void Init()
{
SetNoise(map32);
}
void LoopForever()
{
OverlapOctaves(map32, map256);
ExpFilter(map256);
}
Moving & rendering the clouds in OpenGL
At last the code to render the cloud in OpenGL:
void DrawGLScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
LoopForever(); //Our cloud function
char texture[256][256][3]; //Temporary array to hold texture RGB values
for(int i=0; i<256; i++) //Set cloud color value to temporary array
for(int j=0; j<256; j++)
{
float color = map256[i*256+j];
texture[j][0]=color;
texture[j][1]=color;
texture[j][2]=color;
}
unsigned int ID; //Generate an ID for texture binding
glGenTextures(1, &ID); //Texture binding
glBindTexture(GL_TEXTURE_2D, ID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, texture);
glMatrixMode(GL_TEXTURE); //Let's move the clouds from left to right
static float x;
x+=0.01f;
glTranslatef(x,0,0);
glEnable(GL_TEXTURE_2D); //Render the cloud texture
glBegin(GL_QUADS);
glTexCoord2d(1,1); glVertex3f(0.5f, 0.5f, 0.);
glTexCoord2d(0,1); glVertex3f(-0.5f, 0.5f, 0.);
glTexCoord2d(0,0); glVertex3f(-0.5f, -0.5f, 0.);
glTexCoord2d(1,0); glVertex3f(0.5f, -0.5f, 0.);
glEnd();
SwapBuffers(hDC);
}
Animating the clouds
That will be in part 2. I hope you enjoyed this tutorial. Let us take a break and play some games!