on webgl, Writings, Tutorials, ThreeJS, Code

Coding an infinite 3D world with three.js [Part 2]

Part 1 of this tutorial left off with just one column of tiles, and the ability to move along the Z axis infinitely. Here, we're going to build the full grid, and enable the same infinite movement across the X axis.


Step 1: Build up a grid

Just by changing the X position of the tiles, they will apear to the side of the camera:

Now that we already have one row moving forwards, we can create two more rows either side of it to make a total of 9 tiles in 3 rows x 3 columns. The changes now are that in the constructor at the top of the terrainMatrix.js file, I have provided definitions for the tile height, width and row number to make the rest of the code easier to understand: 

  function TerrainMatrix(){

    this.floor = [];
    this.tileHeight=100;
    this.tileWidth=100;
    this.tileRowNumber = 3;

  }

Then, inside createTerrainMatrix(), all we need to do to add two extra rows is wrap the main contents with a for loop that will force the funciton to execute three times. However in addition to this, we provide an alternative X position for each of the three rows.

The alternative X position is created towards the top of the function where an xPos is defined. Just in side the loop, we set this X position for each of the three row iterations: 

var xPos=0;  
          //we want a 3 by 3 matrix
          for(var x = 1; x<4; x+=1){
            console.log(x)
            if(x1){
              xPos= -this.tileWidth;
            }
            else if(x2){
              xPos= this.tileWidth;
            }
            else if (x3){
              xPos = 0
            }

Setting the X Position 

  • The first row (iteration of the loop) sets X to minus the tile width so it appears on the left
  • The second row sets X to 0 so it appears in the center where the camera is
  • The third row sets X to plus the tile width, so it appears to the right of the camera

Bringing it all together, the rest of the function is exactly the same, except the X position is altered with. ground.position.x = xPos.

  /**  
   * Terrain functions
   */
  TerrainMatrix.prototype={

    constructor: TerrainMatrix,

    /**
     * createTerrainMatrix
     * @TODO: create the matrix of terrains - need to add 9 bits of terrain
     */
    createTerrainMatrix:function(scene, perlinNoise){

          var xPos=0;
          //we want a 3 by 3 matrix
          for(var row = 0; row<3; row+=1){
            if(row0){
              xPos= -this.tileWidth;
            }
            else if(row1){
              xPos= this.tileWidth;
            }
            else if (x2){
              xPos = 0
            }

            //every 100px on the z axis, add a bit of ground
            for ( var z= this.tileHeight; z > (this.tileHeight * -this.tileRowNumber); z-=this.tileHeight ) {

              //Create the perlin noise for the surface of the ground
                var perlinSurface = new PerlinSurface(perlinNoise, this.tileWidth, this.tileHeight);
              var ground = perlinSurface.surface;
              //rotate 90 degrees around the xaxis so we can see the terrain
              ground.rotation.x = -Math.PI/-2;
              // Then set the z position to where it is in the loop (distance of camera)
              ground.position.z = z;
              ground.position.y -=4;

              ground.position.x =xPos;

              //add the ground to the scene
              scene.add(ground);
              //finally push it to the floor array
              this.floor.push(ground);
            }
          }

    },

Step 2: Be less memory intensive

One major alteration is required here for this to work. At the moment, we're creating terrain using PlaneGeometry in the PerlinSurface function: 

//create the plane geometry  
     this.geometry = new THREE.PlaneGeometry(width,height,300,300);

This is too memory intensive to create 9 pieces of terrain. Therefore we need to modify this function to use BufferGeometry, which luckily for us can be done in one line:

      var bufferGeometry = new THREE.BufferGeometry().fromGeometry( this.geometry)  

Here, we use the fromGeometry() method to convert PlaneGeometry to BufferGemetry.
At this point we've got a 3 x 3 grid, and are able to move forward and backwards, but not left and right (without reaching the edge).


Step 3: Moving along the X axis

Very similar code monitoring the Z axis can be used to detect when the camera moves left or right (along the X axis), allowing us to add terrain appropriately along the X axis.

Here is the code to be added to the moveWithCamera() function under the last if clause: 

//x positions  
          else if((this.floor[i].position.x - this.tileWidth)>camera.position.x){

            this.floor[i].position.x-=(this.tileWidth*2);
          }
          //if the camera has moved past the entire square in the opposite direction, move the square the opposite way
          else if((this.floor[i].position.x + this.tileWidth)<camera.position.x){

            this.floor[i].position.x+=(this.tileWidth*2);
          }

It's exactly the same as the Z position checks, but monitors the X positions, and uses the tileWidth rather than tileHeight.

Now the user is trapped, as whatever direction they move, more terrain is 'created'.


Well done - but how can this be improved?

You should now have an infinite terrain, but I'm sure this could be improved. After creating this tutorial, I have experimented with the size and number of tiles used to create this effect, and it can be done using just a 2 x 2 grid. Creating and updating just 4 tiles is much less intensive on memory than 9.