Terraforming with Code, an Introduction to Terrain Generation for use in Games

This post is heavily inspired by Daniel Shiffman’s coding challenge in which he managed to generate random terrain in less than 60 lines of JavaScript. You can find a link to the video in which he does the same in another language, Processing, here. Daniel Shiffman is a wonderful teacher, innovator, and maven within the programming world and contributes much of his time to help shape the minds of programmers everywhere, newcomers and seasoned veterans alike.

Now onto the real meat of this post – 3D terrain generation. For this example of procedural generation, we will be looking at Daniel Shiffman’s example of terrain generation using a library that he created for rendering graphics on the web, P5.js. P5.js is a lightweight graphics library that leverages the HTML5 canvas element to easily create complex images within the browser. It can create both 2D and 3D graphics as we will see shortly.

Without getting into details about how to setup the environment for P5.js, let’s assume we have a fully configured environment in which we can start out work from.

The first thing that we need to do is create the required setup()and draw()functions that P5.js looks for and runs. The setup function is used to instantiate a new environment in which we will draw. Our draw function will be called continuously, multiple times per second. Typically we call our draw function between 30 and 60 times every second, but the amount of computations required for each iteration will ultimately decide how many times it is called. Knowing that, we can safely say that the code will run differently depending on the host system running it, i.e. your computer. The required function stubs will look as follows.

function setup() {
    // setup code goes here
}

function draw() {
    // draw code goes here
}

Next, we’ll need some information about the environment in which we aim to create. Since we are looking to generate terrain that our camera scrolls over, we should create our environment with more space than our actual canvas viewport accounts for. This is done as follows.

var cols, rows;
var scl = 20;
var w = 1400;
var h = 1000;

var flying = 0;

var terrain = [];

function setup() {
  createCanvas(600, 600, WEBGL);
  cols = w / scl;
  rows = h/ scl;

  for (var x = 0; x < cols; x++) {
    terrain[x] = [];
    for (var y = 0; y < rows; y++) {
      terrain[x][y] = 0; //specify a default value for now
    }
  }
}

Now we have created our environment which will allow us to see in a perfect 600×600 pixel square, but our actual generated terrain will stretch over 1400 pixels wide by 1000 pixels high. This allows us to stretch out far enough that we ensure the entire canvas has enough terrain on it. We also determine the number of columns and rows to generate by dividing the height and width by a scale factor of 20. This leaves us with nice even numbers to work with. Finally the data structure to hold our terrain, in this case a multi-dimensional array, is initialized with empty values.

Our draw function is where the real bulk of the work takes places and is what actually generates and displays the terrain on our screen. It is important to note that while setup()is called once and only once, draw()is called many times a second. This distinction means that once we have our data structure set up, we can begin procedurally generating our terrain within draw().

After completion, our draw()function will look something like this:

function draw() {

  flying -= 0.1;
  var yoff = flying;
  for (var y = 0; y < rows; y++) {
    var xoff = 0;
    for (var x = 0; x < cols; x++) {
      terrain[x][y] = map(noise(xoff, yoff), 0, 1, -100, 100);
      xoff += 0.2;
    }
    yoff += 0.2;
  }


  background(0);
  translate(0, 50);
  rotateX(-PI/3);
  fill(200,200,200, 50);
  translate(-w/2, -h/2);
  for (var y = 0; y < rows-1; y++) {
    beginShape(TRIANGLE_STRIP);
    for (var x = 0; x < cols; x++) {
      vertex(x*scl, y*scl, terrain[x][y]);
      vertex(x*scl, (y+1)*scl, terrain[x][y+1]);
    }
    endShape();
  }
}

The first thing we see being done here is we decrement our previously declared flying variable and assign its new value to a new variable called yoff, which stands for y-offset. This y-offset will be used to move the grid that we generate towards the bottom of our screen which will give the impression that our camera is moving over the terrain. Next we something similar to our x-offset and the immediately call a P5.js function, noise(), that implements the Perlin Noise algorithm to produce a range of values that are close in relation to the previously generated value. This is important because Perlin Noise is an algorithm that mimics nature. Without this algorithm implemented, we would have terrain that looked unnatural – we might have one point be 300 pixels high while the next point is -500 pixels below its sibling. Perlin Noise ensures that our terrain more closely simulates nature rather than just being completely random.

Below that code we find a number of P5.js library functions that begin drawing things to our screen. The background(0)call simply makes the background black, translate(0, 50) moves the entire buffer that we are drawing to upwards, and rotate(-PI/3)rotates our buffer along the x axis with respect to the mathematical constant pi. fill(200, 200, 200, 50) states that our buffer should output everything to the canvas in a gray color with the opacity (transparency) set to 50%. A final translate(-w/2, -h/2)translates the buffer halfway in each direction as to draw from the center of the canvas every time rather than the default which is the upper left-hand corner of the canvas.

Finally our actual terrain is generated in the nested forloops that create a triangle strip, which can be thought of as a mesh. The inner forloop then creates a shape at each position in the terrain array we created earlier, specifying each vertex of the shape in space with regard to the values generated by our Perlin Noise calls earlier.

The final product can be found hosted on my site, here.

Leave a Reply

Your email address will not be published. Required fields are marked *