docs: function (updateGeometry) {
  
      var resolution = 64; // number of divisions of the ground mesh
  
      if (updateGeometry) {
        var visibleground = this.data.ground != 'none';
        this.ground.setAttribute('visible', visibleground);
        if (!visibleground) {
          return;
        }
  
        if (!this.groundGeometry) {
          this.groundGeometry = new THREE.PlaneGeometry(this.STAGE_SIZE + 2, this.STAGE_SIZE + 2, resolution - 1, resolution - 1);
        }
        var perlin = new PerlinNoise();
        var verts = this.groundGeometry.vertices;
        var numVerts = this.groundGeometry.vertices.length;
        var frequency = 10;
        var inc = frequency / resolution;
  
        for (var i = 0, x = 0, y = 0; i < numVerts; i++) {
          if (this.data.ground == 'flat') {
            verts[i].z = 0;
            continue;
          }
  
          var h;
          switch (this.data.ground) {
            case 'hills': {
              h = Math.max(0, perlin.noise(x, y, 0));
              break;
            }
            case 'canyon': {
              h = 0.2 + perlin.noise(x, y, 0) * 0.8;
              h = Math.min(1, Math.pow(h, 2) * 10);
              break;
            }
            case 'spikes': {
              h = this.random(i) < 0.02 ? this.random(i + 1) : 0;
              break;
            }
            case 'noise': {
              h = this.random(i) < 0.35 ? this.random(i + 1) : 0;
              break;
            }
          }
  
          h += this.random(i + 2) * 0.1; // add some randomness
  
          // flat ground in the center
          var xx = x * 2 / frequency - 1;
          var yy = y * 2 / frequency - 1;
          var pa = this.data.playArea;
          xx = Math.max(0, Math.min(1, (Math.abs(xx) - (pa - 0.9)) * (1 / pa) ));
          yy = Math.max(0, Math.min(1, (Math.abs(yy) - (pa - 0.9)) * (1 / pa) ));
          h *= xx > yy ? xx : yy;
          if (h < 0.01) h = 0; // stick to the floor
  
          // set height
          verts[i].z = h;
  
          // calculate next x,y ground coordinates
          x += inc;
          if (x >= 10) {
            x = 0;
            y += inc;
          }
        }
  
        this.groundGeometry.computeFaceNormals();
        if (this.data.flatShading) {
          this.groundGeometry.computeFlatVertexNormals();
        }
        else {
          this.groundGeometry.computeVertexNormals();
        }
  
        this.groundGeometry.verticesNeedUpdate = true;
        this.groundGeometry.normalsNeedUpdate = true;
      }
  
      // apply Y scale. There's no need to recalculate the geometry for this. Just change scale
      this.ground.setAttribute('scale', {z: this.data.groundYScale});
  
      // update ground, playarea and grid textures.
      var groundResolution = 2048;
      var texMeters = 20; // ground texture of 20 x 20 meters
      var texRepeat = this.STAGE_SIZE / texMeters;
  
      if (!this.groundCanvas || this.groundCanvas.width != groundResolution) {
        this.gridCanvas = document.createElement('canvas');
        this.gridCanvas.width = groundResolution;
        this.gridCanvas.height = groundResolution;
        this.gridTexture = new THREE.Texture(this.gridCanvas);
        this.gridTexture.wrapS = THREE.RepeatWrapping;
        this.gridTexture.wrapT = THREE.RepeatWrapping;
        this.gridTexture.repeat.set(texRepeat, texRepeat);
  
        this.groundCanvas = document.createElement('canvas');
        this.groundCanvas.width = groundResolution;
        this.groundCanvas.height = groundResolution;
        this.groundTexture = new THREE.Texture(this.groundCanvas);
        this.groundTexture.wrapS = THREE.RepeatWrapping;
        this.groundTexture.wrapT = THREE.RepeatWrapping;
        this.groundTexture.repeat.set(texRepeat, texRepeat);
  
        // ground material diffuse map is the regular ground texture and the grid texture
        // is used in the emissive map. This way, the grid is always equally visible, even at night.
        this.groundMaterialProps = {
          map: this.groundTexture,
          emissive: new THREE.Color(0xFFFFFF),
          emissiveMap: this.gridTexture
        };
  
        // use .shading for A-Frame < 0.7.0 and .flatShading for A-Frame >= 0.7.0
        if (new THREE.Material().hasOwnProperty('shading')) {
          this.groundMaterialProps.shading = this.data.flatShading ? THREE.FlatShading : THREE.SmoothShading;
        } else {
          this.groundMaterialProps.flatShading = this.data.flatShading;
        }
  
        this.groundMaterial = new THREE.MeshLambertMaterial(this.groundMaterialProps);
      }
  
      var groundctx = this.groundCanvas.getContext('2d');
      var gridctx = this.gridCanvas.getContext('2d');
  
      this.drawTexture(groundctx, groundResolution, texMeters);
  
      gridctx.fillStyle = '#000000';
      gridctx.fillRect(0, 0, groundResolution, groundResolution);
      this.drawGrid(gridctx, groundResolution, texMeters);
  
      this.groundTexture.needsUpdate = true;
      this.gridTexture.needsUpdate = true;
  
      if (updateGeometry) {
        var mesh = new THREE.Mesh(this.groundGeometry, this.groundMaterial);
        this.ground.setObject3D('mesh', mesh);
      }
      else {
        this.ground.getObject3D('mesh').material = this.groundMaterial;
      }
  
      this.ground.setAttribute('shadow', {
        cast: false,
        receive: this.data.shadow
      });
    }