The aim of this workshop is understand some applications of Perlin Noise, and use it to create some colourful terrain! Once you can generate this terrain as a 2D image, you could apply this in 3D contexts as well to create Minecraft-like worlds.
Click to generate a new mapWe will be coding in JavaScript but don't worry about knowing the ins and outs of the language; you should be able to pick it up as we go!
To get started, create an account for the web editor
Open a new sketch (File->New)
A sketch is a small program that will work with p5.js to produce a simulation.
Just like a game, your simulation will have frames (an image of your simulation) and p5.js will aim to show 60 frames a second.
You'll be able to code what happens on certain events e.g. just before a frame is shown, just before the simulation starts, when the mouse is clicked.
In this lab, since we're just drawing an image, we'll only concern ourselves with drawing before the simulation starts and when the mouse is clicked.
Your new sketch should have 2 functions:
function setup() {
createCanvas(400, 400);
let redV = 128; // if you're new to js, use "let" to declare variables
let greenV = 12;
let blueV = 128;
background(redV, greenV, blueV); // my favourite colour; purple!
}
RGB values have R,G,B values ranging from 0-255
Mini-Task: Try running the above simulation! Maybe change the variables to display your own favourite colour.
Let's dive straight into generating random noise! We'll need to know a couple things:
Let's tackle (1) first!
function setup() {
createCanvas(400, 400);
// the eyes
point(25, 25);
point(28, 25);
// the smile
point(25, 30);
point(26, 30);
point(27, 30);
point(28, 30);
point(24, 29);
point(23, 28);
point(29, 29);
point(30, 28);
}
We can use the point() function to draw a pixel at specified X,Y coordinates. What about the colour?
function setup() {
createCanvas(400, 400);
// eye colour
stroke(0, 0, 255);
// the eyes
...
// smile colour
stroke(255, 0, 0);
// the smile
...
}
We can use the stroke() function to determine the colour of the pixels drawn from that point on; we can supply it with 3 values to specify an RGB colour. If we're only using grayscale colours, then we can provide just a single value (to represent the brightness or "value" of the colour) e.g. 255 for pure white, 0 for pure black.
Using this knowledge, can you guess what the following code does?
function setup() {
createCanvas(400, 400);
for(let y = 0; y < 400; y++){
for(let i = 0; i <= 255; i++){
stroke(i);
point(i,y);
}
}
}
Mini-Task: Run the above simulation, and see if you guessed what it does correctly!
A key thing to remember about p5.js is that ultimately, it is still JavaScript. Therefore, we can use JavaScript functions and libraries with p5.js.
To handle randomness, we'll use js' Math library; in particular, the function Math.random()! Let's use the JavaScript console to test it out.
for(let i = 0; i < 20; i++){
console.log(Math.random()); // values between 0-1
}
Okay cool, there's only a slight problem now; we get random values in the range 0-1, but the colour values are in the range 0-255. All we need to do is multiply up :)
If you ever have questions about p5 functions, then please refer to the excellent documentation!
It's now your job to replicate the random noise sketch! Use the tools above and the template below to create some random noise.
const width = 400; // these are "constant" variables, as they don't change
const height = 400; // made to be variables since they're used in 2 places (iterating and createCanvas)
function drawRandomNoise(){
for(let x = 0; x < width; x++){
for(let y = 0; y < height; y++){
// FILL ME
}
}
}
function setup() {
createCanvas(width, height);
drawRandomNoise();
}
Generally, taking this out into its own function for this is a good idea; especially since this function's sole purpose is drawing random noise
Okay so we've programmed random noise which is "incoherent" noise - you can have sharp constrasts between neighbouring pixels. If we want nice smooth gradients, then we can use perlin noise!
We can definitely see that the noise is more ordered than random noise, however we want it to be less sharp; we want it to be smoother.
So how do we achieve a smoother noise? We can figure this out by looking at 1D noise, and think about sampling 10 points from perlin noise.
From the above, we can see that the values sampled can be quite different. You can think of this as sampling the 10 points at $x=1, 2, 3, \dots$
What if we sampled points more frequently instead?
From the above, we can see that the values sampled are a lot closer; you can think of this as sampling the 10 points at $x=0.2, 0.4, 0.6 \dots$
So the general takeaway is that we can generate smoother noise, by sampling more frequently. How do we do that? We can just divide x,y before feeding it into the noise function!
Use the tools above to generate some smooth perlin noise! Have a variable called the smoothing factor, and change its value to see how the noise varies.
We now have the tools to create islands! By designating pixel with noise values past a certain threshold as "land" and the rest as "ocean", we can generate islands. Since the noise() function returns a value between 0 and 1, we should have a threshold value between 0 and 1.
const threshold = 0.55;
function getColour(noiseVal){
if(noiseVal > threshold){
return color(239, 221, 111); // sandy yellow (island)
}else{
return color(0, 157, 196); // ocean blue
}
}
Note that the above function returns a "colour" object; if you want to work with large p5.js programs, then you should really be using classes and objects. Now we can use the returned colour object like so:
let colour = getColour(noise(x/sf, y/sf)); // colour object
stroke(colour); // stroke accepts the colour object directly (instead of number values representing a colour)
Create desert island generation by modifying your smooth noise task, with the above tools. It should look something like below:
Sometimes, you want more detailed islands; you want the overall shape of the islands to be the same so you have the same smoothness factor, but you want there to be some more granular detail. You might want your beaches to have some roughness to its terrain. After all, terrain isn't perfectly smooth in nature!
You can add this granular detail by adding layers of perlin noise together; each of these layers being called "octaves". Typically, you'll have a large amplitude perlin noise wave which describes the general overall shape, and then you'll have a smaller amplitude perlin noise waves to describe the finer details. These smaller waves usually have higher frequency, which will give the islands a distinct roughness.
You can increase the number of octaves and how much the smaller waves contribute to the noise by using p5's noiseDetail() function. Read up on the details here.
You can also add more "detail" to the islands by creating multiple threshold levels, like in the example at the top of this webpage! An example of a more complicated threshold colour function:
const snowcapThreshold = 0.75;
const mountainThreshold = 0.6;
const grassThreshold = 0.4;
const shallowWaterThreshold = 0.35;
function getColour(noiseVal){
if(noiseVal > snowcapThreshold){
return color(219, 219, 219); // snowcap white
}else if(noiseVal > mountainThreshold){
return color(112, 112, 112); // mountain gray
}else if (noiseVal > grassThreshold){
return color(75, 139, 59); // grass green
}else if (noiseVal > shallowWaterThreshold){
return color(0, 157, 196); // light blue
}else{
return color(0, 147, 186); // slightly darker blue
}
}
You have everything you need to create your own amazing terrain generation! You can take my generator(s) (displayed at the top of the page) as inspiration, but I'm sure you'll be able to create a better looking generator than mine.
You can view the code for my two generators here:
Tip: Getting good terrain with perlin noise is often a case of just tweaking your parameters (octaves, amplitude/frequency, smooth factor etc) until they're just right.
If you want to make your generator even cooler, then you can explore more procedural generation techniques. Natural next steps include: