Thursday, August 1, 2013

Random Terrain Generation Using Perlin Noise


Noise... I know It doesn't sound good, but the noise I will talk about is something a little different, actually an "useful" noise.

What Is Perlin Noise?
Noise is not only something that is auditory, and Perlin Noise is not an auditory noise, but a visual one. On empty channels of our oldy televisions, we used to see repeatedly moving black and white points on the screen, that are called "static" or "noise". Perlin Noise takes one of those pointy pictures, stacks them on top of another a few times this way it generates a cloud-like grayscale image out of the noise. It sounds a little complex huh?

Stack them on top of another?
Well, this doesn't sound good, either, but if you don't know what Perlin Noise is, you will be surprised when you see the results. First of all, I will show you a 240x240 pixel sized black-white noise. Then I will choose a smaller (actually half) sized area inside it. Then choose even smaller one (quarter), and smaller (1/8), and so on.



The figure on the left shows the areas I choose, and on the right is my 240x240 noise. Now let's choose an area of 240x240 pixels from the noise, which will cover the whole noise, and name it as n0. Then choose a smaller one, 120x120 pixels, and name it n1. Following this method, we will pick our n2, n3, n4 ,n5 and n6.




Now we will expand all the smaller squares into the size of the largest one: 240x240, which means we will have 7 images at same size. Then we will put these images on top of another, however we want all the images to be visible a the same time. To accomplish this, we will change the alpha (transparency) values of our images. Most graphic editors use alpha values between 0 and 255, but in Game Maker this interval is 0 and 1. If an image has an alpha value of 0, that means the image is totally invisible, and 1 means it is visible. We have 7 images, therefore if we give each image an alpha value of 0.14, when we stack them up the image will have a total of 0.98 alpha, which is clearly visible. After changing alpha values and stacking the images, the resulting image is like the one on the right, which is actually likable.

How to create terrain from Perlin Noise?
I've explained how does a Perlin Noise created and what it looks like. To create a terrain out of this grayscale image, we will assign different values for different shades of grey. Think about any pixel on the Perlin Noise; we will check how "white" that pixel is and assign a value between 0 and 255 accordingly. Considering 0 is totally black and 255 white, we will decide the sea level; let's make it 140 for now. From now on, we know that any pixel that has a white-ness below 140 will be considered as sea and painted blue. And any pixel above 140 will be ground and painted green. This way, we will be created a terrain-looking image from plain noise. This method can be applied in 3D too, but of course it will be more complex. Minecraft uses Perlin Noise to generate random 3D maps.


Generating Perlin Noise Via Game Maker
Firstly we should create a noise, which is pretty easy.Create a spriteless control object, it will be needed later. Next create a 1x1 white sprite and create an "pixel" object with this sprite. For the object, add "Create" event and insert an "Execude Code". Inside the code write the following:
image_blend = choose(c_white,c_black);
This will make the pixel object automatically paint itself to either white or white after created. Like I told at Useful Code Groups page, fill your room with pixel object using control object you created before. Now create a square room, and put only your control object here. When you start your game, you would see your created noise. By adding a "Restart Game" command into your control object for any key on the keyboard, you can restart game and see that it is totally random.

Next step is dividing our noise into smaller pieces and save each piece as a new sprite. Doing this is a little complex, I hope I can explain clearly. We said that our first noise is n0, we can assign it as a sprite with this code:
global.noise0 = sprite_create_from_screen(0,0,room_width,room_height,false,false,0,0);
This command takes an area from the screen and saves it as a new sprite for the name "global.noise0". Now we have a global sprite whose left-top corner is at (x,y)=(0,0) and size is the same as the room. Making the name "global" is important, this way we will be able to call it from any object.

Repeating the same thing we will create our noise1, but this time left-top point should be random and size of the square should be the half of the noise0's. Now be careful: imagine that we divided the image n0 into four equal pieces. If our smaller n1 square's left-top corner stays inside the left-top piece of n0, then the n1 square will always be inside n0. See the left figure below: there is our n1 red square with a star at left-top corner. Now imagine you hold that star inside the yellowish area and move the red square anywhere with it. If star doesn't leave the yellowish area, red square never gets out of the n0. Same logic works differently for other smaller squares, see image on the right for noise2.

Then when defining the new square, we should assign the left-top corner in a random place between 0 and A/2, and make the size of the square A/2. We can use irandom(x) command to assign random wholenumbers.

global.noise1 = sprite_create_from_screen(irandom(room_width/2),irandom(room_height/2),room_width/2,room_height/2,false,false,0,0);

When creating noise2, we should be careful that side's width will be A/4 and motion area for the left-top corner will be larger since size of the square will be smaller (check figure on the right above). Now the area for the left-top corner will be between 0 and 3*A/4. If you are good at math, here is a general formula for this considering N is noise number:

global.noise(N)= sprite_create_from_screen(irandom((2^(N) - 1)*room_width/(2^(N)),irandom((2^(N) - 1)*room_height/(2^(N))),room_width/(2^(N)),room_height/(2^(N)),false,false,0,0);

Using this formula, we achieve the following codes below:
global.noise0 = sprite_create_from_screen(0,0,room_width,room_height,false,false,0,0);
global.noise1 = sprite_create_from_screen(irandom(room_width/2),irandom(room_height/2),room_width/2,room_height/2,false,false,0,0);
global.noise2 = sprite_create_from_screen(irandom(3*room_width/4),irandom(3*room_height/4),room_width/4,room_height/4,false,false,0,0);
global.noise3 = sprite_create_from_screen(irandom(7*room_width/8),irandom(7*room_height/8),room_width/8,room_height/8,false,false,0,0);
global.noise4 = sprite_create_from_screen(irandom(15*room_width/16),irandom(15*room_height/16),room_width/16,room_height/16,false,false,0,0);
global.noise5 = sprite_create_from_screen(irandom(31*room_width/32),irandom(31*room_height/32),room_width/32,room_height/32,false,false,0,0);
global.noise6 = sprite_create_from_screen(irandom(63*room_width/64),irandom(63*room_height/64),room_width/64,room_height/64,false,false,0,0);

If you don't want to write too long, you can write the code for noise0 and then use a FOR LOOP for the rest of the noises. Something like: for (N=1;N<7;N+=1){GENERAL FORMULA}

We created sprites from the noise, now we don't need all these pixel objects, create a new room or delete them all. with your control object. Now it's time to stack these sprites with changing their alpha values and sizes; then we need draw_sprite_stretched_ext command. Technically the alpha value for each should be 0.14, but the values I gave below gives the best results:
draw_sprite_stretched_ext(global.noise0,0,0,0,room_width,room_height,c_white,0.2);
draw_sprite_stretched_ext(global.noise1,0,0,0,room_width,room_height,c_white,0.2);
draw_sprite_stretched_ext(global.noise2,0,0,0,room_width,room_height,c_white,0.3);
draw_sprite_stretched_ext(global.noise3,0,0,0,room_width,room_height,c_white,0.3);
draw_sprite_stretched_ext(global.noise4,0,0,0,room_width,room_height,c_white,0.5);
draw_sprite_stretched_ext(global.noise5,0,0,0,room_width,room_height,c_white,0.5);
draw_sprite_stretched_ext(global.noise6,0,0,0,room_width,room_height,c_white,0.4);

Numbers at the end of the each code are alpha values. Giving different numbers will alter your perlin noise, for example making alpha for noise0 will make your noise more ribbed.

Perlin Gürültüsünden Arazi Elde Etmek
Now we will choose a sea level and paint the noise accordingly. This step will be hard for both Game Maker and your computer. I'm sure there are simplier methods to do these, but my capacity is not enough for now -_-'. I use a room with sizes 320x320, which makes 102400 pixel object total. Making 102400 object re-color themselves takes 3 - 5 minutes. It takes long because while controlling the "whiteness" of a pixel, Game Maker directly uses your monitor to work and achieve data about the pixel. Of course, if your computer is faster, the time will be shorter.

There are a lot of alternatives for coloring. I've created another control object to create a height map, too. It runs itself after coloring finished and gives some height visually. This is the code I use:

if (p < room_width)  //If my control variable is smaller then room width;
{
for (i=0;i<room_width;i+=1) //scans the room horizontally
{
for (j=p;j<(p+1);j+=1)  //scans the pixels at line i
{
pointed = color_get_red(draw_getpixel(i,j));   //checks the color of the pixel at coordinates (i,j)
global.tema[i,j] = pointed;  //saves (i,j) pixels color value to the "tema" matrix for height map

if (pointed<140)   //if that value is below 140
{instance_create(i,j,water);}   //creates a water object
else
{instance_create(i,j,ground);}   //if not, creates a ground object instead
}
}
p += 1;  //restarts the process by adding 1 to the control variable
alarm[0] = 2;
}
else
{global.terra = 1;} //if control variable is larger than the room width, starts the height map

Using my "tema" matrix I define the height, and accordingly make water and ground objects lighter or darker. I also put a collision check command inside the ground objects that will transform them into sand objects if they are near water. Here are the results:



I think it looks good, but it is really slow. I'm working on a more comfortable and fast system about this these days, and it goes pretty well. I will write about it in my next entry. You can send email to me if you have questions, requests or recommendations, I appreciate it. :)

You can find the GMK file here:
Perlin_Gurultusu_Rastgele_Arazi.gmk

No comments:

Post a Comment