on webgl, Writings, Tutorials, ThreeJS, Code

Seeing Sounds [Part 2]: Creating a terrain

This is the second part of the Seeing Sounds three.js series. Part 1 looked at creating a flying star field, and in this part we'll create the terrain underneath it. At some point, we'll bring the two parts together, and then add some extra spheres that will animate with the music.

The outcome of this tutorial is a textured terrain with lighting as below:

Step 1: Adding a basic terrain

This step will show you what we'll use to create a basic, flat terrain. Setting the scene is very similar to my getting started guide, and part 1 of this series, so you should be quite familiar with that. The main difference in the code from part 1 is that instead of adding a sphere, we're adding a floor.

So, if you look at the code in part 1, we'll be replacing the addSphere function with an addGround function in this tutorial. Let's get right to it:

FIrst set up the html page as usual:

<html lang="en">  
<head>
  <title>Three.js starter tutorial</title>
  <meta charset="utf-8">
</head>
<body style="margin:0px;">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.min.js"></script>

<script>
    //tutorial code will go here
</script>

</body>
</html>

Then add the following code between the empty script tags. You'll notice this is almost exactly the same as the code from part 1:

    //Declare three.js variables  
    var camera, scene, renderer;

    //assign three.js objects to each variable
    function init(){

        //camera
        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 5;

        //scene
        scene = new THREE.Scene();

        //renderer
        renderer = new THREE.WebGLRenderer();
        //set the size of the renderer
        renderer.setSize( window.innerWidth, window.innerHeight );

        //add the renderer to the html document body
        document.body.appendChild( renderer.domElement );
    }

    function addGround(){

    //create the ground material
        var groundMat = new THREE.MeshBasicMaterial( {color: 0xffff00 }  );

        //create the plane geometry
        var geometry = new THREE.PlaneGeometry(400,400);

        //create the ground form the geometry and material
        var ground = new THREE.Mesh(geometry,groundMat);
    ground.position.y = -1.9; //lower it
    ground.rotation.x = -Math.PI/2;
        ground.doubleSided = true;
        //add the ground to the scene
        scene.add(ground);

    }

    function render() {
        //get the frame
        requestAnimationFrame( render );

        //render the scene
        renderer.render( scene, camera );

    }

    init();
    addGround();
    render();

Here's what we get:

The main difference is the addGround function:

  • We start off by creating the material in line 25, giving it a yellow colour, and ensuring it's double sided.
  • Line 29 is one of the most important - here we use PlaneGeometry, which is basically three.js's implementation of a flat surface.
  • Then on line 32, we use the Mesh function as in part 1, to combine the geometry with the material for the ground.
  • Line 33 just lowers the position of the ground, and line 34 rotates it 90 degrees so that we can see it.
  • Finally, in line 37, we add the ground to the scene

That was quite simple - in the next step we'll look at creating the bumpy effect on the terrain.

Step 2: Make it bumpy with Perlin noise

To create a bumpy terrain, we need to add some random noise to the plane from step 1, so that vertexes are created and distributed randomly but evenly.

Perlin Noise

A common way to achieve the natural appearances described above is to use a Perlin noise generator, which uses random numbers to generate natural looking textures. Somebody has kindly made a JavaScript version available here on GitHub, so we'll use that.

For speed, we'll just paste the Perlin noise code at the bottom of our script and look at it as a black box. Now to actually use it, we need to set up a couple quick variables at the top of our file:

var date = new Date();  
var pn = new Perlin('rnd' + date.getTime());
  • In line 1 we create a new Date object and assign it to the variable date. This is just used for a random number in line 2, as it's new each time.
  • In line 2, we declare the variable pn, and then as an argument to the Perlin class, we provide the getTime function of the date object we just created. This ensures the Perlin object created and assigned to pn will generate the natural appearance we want.

Create the vertices

We're now going to use the pn variable created above to generate vertices across our plane. We can do this using a for loop inside our addGround function, before we add the ground to the scene:

//make the terrain bumpy  
        for (var i = 0, l = geometry.vertices.length; i < l; i++) {
          var vertex = geometry.vertices[i];
          var value = pn.noise(vertex.x / 10, vertex.y /10, 0);
          vertex.z = value *10;
        }

Here, we loop through the vertices of our PlaneGeometry, and for each vertex, add some noise using pn.noise.

That's the main bit we need to add to create the vertices. The full addGround function should now look like this:

    function addGround() {

  //create the ground material using Mesh Basic Material
  var groundMat = new THREE.MeshBasicMaterial({
    color: 0xffff00
  });

  //create the plane geometry
  var geometry = new THREE.PlaneGeometry(120, 100, 100, 100);

  //create the ground form the geometry and material
  var ground = new THREE.Mesh(geometry, groundMat);
  ground.position.y = -1.9; //lower it
  ground.doubleSided = true;

  //make the terrain bumpy
  for (var i = 0, l = geometry.vertices.length; i < l; i++) {
    var vertex = geometry.vertices[i];
    var value = pn.noise(vertex.x / 10, vertex.y / 10, 0);
    vertex.z = value * 10;
  }

  //create the ground form the geometry and material
  var ground = new THREE.Mesh(geometry, groundMat);
  //rotate 90 degrees around the xaxis so we can see the terrain
  ground.rotation.x = -Math.PI / -2;

  //add the ground to the scene
  scene.add(ground);
}

What else has changed? Apart from the loop to create the bumpy terrain, we've altered the geometry variable (line 9) by changing the parameters passed in so that the width and height segments are adjusted too - instead of just the width and height. You can read more about the PlaneGeometry parameters here.

Make it double sided

As seen in the demo output at the top of this step, the terrain produced doesn't look right, as the underside of the vertices have no colour. We can change this with one edit to the Mesh (ground material) in line 5 above. Instead of just including a colour to the material, we make it double sided:

  //create the ground material using MeshLambert Material  
  var groundMat = new THREE.MeshBasicMaterial({
    color: 0xffff00, side: THREE.DoubleSide
  });

By adding side: THREE.DoubleSide, we now get this:

 

Step 3: Light and Texture

We're nearly there now, we just need to change the texture of the mesh, and add some light to the scene to create the final terrain.

Add some light

We need to add some light to the scene because the material we will be changing the ground to needs light for it to be seen. Therefore, we'll add an addLight function, and also a call to it just before we call addGround:

    function addLight(){

      //use directional light
      var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.9);
      //set the position
      directionalLight.position.set(10, 2, 20);
      //enable shadow
      directionalLight.castShadow = true;
      //enable camera
      directionalLight.shadowCameraVisible = true;

      //add light to the scene
      scene.add( directionalLight );
    }

This code is fairly straightforward, we're using three.js's directional light, setting its position, enabling shadow, and adding it to our scene. Read more about directional light here.

Change the mesh

Next, in the addGround function where the ground material is defined, we're going to use MeshLambertMaterial instead of MeshBasicMaterial. We'll use the same arguments, so you just need to switch the name inside the addGroud function:

  //create the ground material using MeshLambert Material  
  var groundMat = new THREE.MeshLambertMaterial({
    color: 0xffff00, side: THREE.DoubleSide
  });

If you were to run your code with this change, you'd now get this:

The problem here is that the light has not been computed properly, so add these two lines before adding the ground to the scene to finish it all off:

//ensure light is computed correctly  
geometry.computeFaceNormals();  
geometry.computeVertexNormals();

With those changes, here's the output we finish with:

Well done

Nice work, we've covered quite a few new concepts in this part. The next part of this series is not really a proper tutorial, we're just going to combine the star field code from part one with the terrain code from this part.

You can get the full source code here on GitHub