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
p
n, and then as an argument to the Perlin class, we provide thegetTime
function of the date object we just created. This ensures the Perlin object created and assigned topn
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