Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <canvas id="c"></canvas>


<p class="label--dragInstruct js-label--dragInstruct">Click to drag, wheel to zoom</p>

<p class="credit">
  <a href="https://www.twitter.com/sebastianlenton" target="_blank">
    @sebastianlenton
  </a>
</p>
              
            
!

CSS

              
                * {
  box-sizing: border-box; }

html, body {
  height: 100%;
  overflow: hidden; }

body {
  margin: 0;
  overflow: hidden;
  touch-action: none; }

#c {
  width: 100%;
  height: 100%;
  display: block;
  cursor: pointer; }



/* instruction label */
p.label--dragInstruct {
  background: rgba(0,0,0,0.3);
  padding: .7rem 1rem;
  color: white;
  font-family: sans-serif;
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 2;
  text-align: center;
  line-height: 1.5;
  user-select: none;
  opacity: 1;
  transition: opacity 1.5s;
  pointer-events: none;
  top: 75%;
  left: 50%;

  &.fadeOut {
    opacity: 0;
  }
}



/** credit **/

.credit {
  padding: 0;
  position: absolute;
  bottom: 1.5rem;
  right: 1rem;
  margin-bottom: 0;
  font-weight: bold;
  text-shadow: .1rem .1rem 0 darken(#84AFC8,25%);
  font-size: 1.2rem;
  z-index: 999;
  transform: none;
  line-height: 0;
  
  a {
    color: white;
    text-decoration: none;
    display: block;
    transition: transform .25s;
    transform: scale(1);
    
    &:hover {
      transform: scale(1.2)  translate(-.75rem, -.15rem);
    }
  }
}
              
            
!

JS

              
                /********************************************************
PerlinNoiseGenerator - Outputs a square perlin noise map

********************************************************/

const PerlinNoiseGenerator = function() {

  this.seed = 49
  this.zoom = 3.8;              //zoom subtracted from map start (0) and map end (mapSizeMax), so can't be more than mapSizeMax / 2
  this.resolution = DEFAULT_PERLIN_MAP_RESOLUTION;

  this.lastSeed = this.seed;  //this is to do with DAT.gui- to prevent a reseed if the same value gets entered (the change event fires if you even click into the field...)
  this.lastZoom = this.zoom;
  this.lastResolution = this.resolution;

  this.mapSizeMax = 10;       //map runs from 0 to 10

  this.mapData = [];

  if (this.resolution > MAX_PERLIN_MAP_RESOLUTION) {
    this.resolution = MAX_PERLIN_MAP_RESOLUTION;
  }


  //return an array containing a noise map
  this.outputMap = function() {

    this.mapData = [];

    //clamp zoom if bigger than workable
    if(this.zoom >= this.mapSizeMax / 2) {
      this.zoom = this.mapSizeMax / 2;
    }

    noise.seed(this.seed);

    const mapSize = (this.mapSizeMax - this.zoom) - (0 + this.zoom);
    const units = mapSize / this.resolution;

    for(let q=0;q<this.resolution;q++) {
      const mapRow = [];

      for(let r=0;r<this.resolution;r++) {
        const currentValue = noise.perlin2(
          (q * units) - (mapSize / 2),
          (r * units) - (mapSize / 2),
        );
        mapRow.push(currentValue);
      }

      this.mapData.push(mapRow);
    }

  }
}


/********************************************************

Skysphere
Create, draw and change canvas that holds gradient texture for sky sphere

********************************************************/

const SkyTexture = function (width, height) {
  this.canvas;
  this.ctx;
  this.width = width;
  this.height = height;

  this.topColor = 'rgb(135,196,196)';
  this.bottomColor = 'rgb(255,152,158)';

  this.init = function () {
    this.canvas = document.createElement('canvas');
    this.canvas.id = 'skyTexture';
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    this.ctx = this.canvas.getContext('2d');
    //document.body.appendChild(this.canvas);     // don't need to do this!
  }

  this.render = function() {
    // Create gradient
    const grd = this.ctx.createLinearGradient(0, 0, 0, this.height);
    grd.addColorStop(0.1, this.topColor);
    grd.addColorStop(.9, this.bottomColor);

    // Fill with gradient
    this.ctx.fillStyle = grd;
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  }
}


/********************************************************

Scene, renderer

Note this is called "landscape" but it actually renders the entire scene

********************************************************/


const Landscape = function () {
  'use strict';

  this.canvas;
  this.renderer;
  this.scene;
  this.camera;
  this.light;

  this.worldSize = 10;                //in units, this is the central square that's always visible regardless of aspect
  this.cameraDistance = 14;
  this.cameraZoom = .75;
  this.cameraZoomMax = 2;
  this.cameraZoomMin = .5;

  this.rotX = 0;
  this.rotY = 0;
  this.rotZ = 0;

  this.map = [];

  this.resolution;                    //will be set when the map gets loaded
  this.MaxResolution = MAX_PERLIN_MAP_RESOLUTION;
  this.objectPool_count = this.MaxResolution * this.MaxResolution;
  this.objectPool = [];

  this.fogOn = true;
  this.fogOn = true;
  this.fogColor = [5,53, 128];
  this.fogNearDefault = 10;
  this.fogFarDefault = 24;

  this.landBase;
  this.landBaseSize = this.worldSize * .75;
  this.landbaseVisibility = true;
  this.landbaseColor = [0, 15, 54];

  this.skySphere;
  // skysphere colors in skyTexture.js

  this.topColor = [19, 255, 85];
  this.bottomColor = [35, 54, 38];

  this.controls;                        //will be set to orbitcontrols

  this.perlinMinMax = .65;              //approx the min/max val of the noise generator (-.75 to .75), not precise as unsure what the actual value is

  this.heightMultiplier = .5;           //the amount the height will be multiplied by (for taller/shorter output)

  this.renderRequested = false;         //gets set to true if a frame is to be rendered (see https://threejsfundamentals.org/threejs/lessons/threejs-rendering-on-demand.html)


  /********************************************************
  SETUP
  ********************************************************/

  this.init = function () {
    this.canvas = document.querySelector('#c');
    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      alpha: false
    });
    this.scene = new THREE.Scene();
    this.scene.fog = new THREE.Fog(this.fogColor, 0, 0);        //the near and far values get set in this.setFog
    this.setFog();
    this.setupCamera();
    this.setFOV();
    this.createLandBase();
    this.createObjectPool();
    this.setAllObjectsInvisible();
    this.setupControls();
    this.mouseWheelHandler();
  }


  /*** setup camera ***/

  this.setupCamera = function () {
    const fov = 90;
    const aspect = 1;
    const near = 0.1;
    const far = 100;
    this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    this.camera.position.x = this.cameraDistance / 1.5;
    this.camera.position.y = 4;
    this.camera.position.z = this.cameraDistance / 1.5;

  }


  /*** setup OrbitControls (note have disabled default dolly in favour of a zoom effect, see zoom handler below) ***/

  this.setupControls = function () {
    this.controls = new THREE.OrbitControls(this.camera, this.canvas);
    this.controls.enablePan = false;
    this.controls.minDistance = 2.6;
    this.controls.maxDistance = this.cameraDistance + 10;
    this.controls.minPolarAngle = .7;
    this.controls.maxPolarAngle = 2.1;
    this.controls.target.set(0, 0, 0);
    this.controls.autoRotate = true;
    this.controls.enableDamping = true;
    this.controls.enableZoom = false;

    /*** event listener for disengaging auto rotate  ***/
    this.controls.domElement.addEventListener('mousedown', disengageAutoRotate.bind(this));
    this.controls.domElement.addEventListener('touchstart', disengageAutoRotate.bind(this));

    function disengageAutoRotate() {
      this.controls.autoRotate = false;
      this.controls.update();
    }

    /** event listener for orbitControls change- will trigger a render whenever OrbitControls changes */
    const that = this;

    this.controls.addEventListener('change', function () {
      that.requestRender();
    });

    this.controls.update();
  }


  /*** set fog colour, near, far depending on whether fog is on or off ***/

  this.setFog = function () {
    this.scene.fog.color.setRGB(this.fogColor[0], this.fogColor[1], this.fogColor[2]);
    this.scene.fog.color.set(`rgb(${Math.round(this.fogColor[0])}, ${Math.round(this.fogColor[1])}, ${Math.round(this.fogColor[2])})`);

    if (this.fogOn) {
      this.scene.fog.near = this.fogNearDefault;
      this.scene.fog.far = this.fogFarDefault;
    } else {
      this.scene.fog.near = 1000;
      this.scene.fog.far = 1000;
    }
  }


  /*** create the square that everything sits on, and position it correctly ***/

  this.createLandBase = function () {
    const landBase_geom = new THREE.PlaneGeometry(this.landBaseSize, this.landBaseSize);
    const landBase_mat = new THREE.MeshBasicMaterial({
      side: THREE.DoubleSide,
      visible: this.landbaseVisibility          //TODO don't set colour etc if material is invisible
    });

    this.landBase = new THREE.Mesh(landBase_geom, landBase_mat);
    this.setLandbaseColor(this.landbaseColor);

    this.scene.add(this.landBase);
    this.landBase.rotation.x = THREE.Math.degToRad(90);     //wha, three has it's own math utils: https://threejs.org/docs/#api/en/math/Math
    this.landBase.position.y = -1;
  }


  this.setLandbaseColor = function () {
    this.landBase.material.color.set(`rgb(${Math.round(this.landbaseColor[0])}, ${Math.round(this.landbaseColor[1])}, ${Math.round(this.landbaseColor[2])})`);
  }


  /*** create pool of objects and parent them to landBase ***/

  this.createObjectPool = function () {

    const plane_mat = new THREE.MeshBasicMaterial({
      vertexColors: THREE.VertexColors
    });

    for (let i = 0; i < this.objectPool_count; i++) {
      const plane_geom = new THREE.PlaneGeometry(this.landBaseSize, this.landBaseSize);
      const plane_mesh = new THREE.Mesh(plane_geom, plane_mat);

      this.objectPool.push(plane_mesh);
      this.landBase.add(this.objectPool[i]);
      this.objectPool[i].rotation.x = THREE.Math.degToRad(-90);

      this.objectPool[i].geometry.faces[0].vertexColors.push(new THREE.Color(0xff00ff));
      this.objectPool[i].geometry.faces[0].vertexColors.push(new THREE.Color(0xff00ff));
      this.objectPool[i].geometry.faces[0].vertexColors.push(new THREE.Color(0xff00ff));

      this.objectPool[i].geometry.faces[1].vertexColors.push(new THREE.Color(0xff00ff));
      this.objectPool[i].geometry.faces[1].vertexColors.push(new THREE.Color(0xff00ff));
      this.objectPool[i].geometry.faces[1].vertexColors.push(new THREE.Color(0xff00ff));
    }

  }


  /*** set all objects invisible (prior to making X visible) ***/

  this.setAllObjectsInvisible = function () {
    for (let i = 0; i < this.objectPool_count; i++) {
      this.objectPool[i].visible = false;
    }
  }


  /*** set X objects visible ***/

  this.setObjectsVisible = function () {
    for (let i = 0; i < this.resolution * this.resolution; i++) {
      this.objectPool[i].visible = true;
    }
  }


  /********************************************************
  SKYSPHERE
  ********************************************************/

  this.createSkySphere = function(texture) {
    texture = new THREE.CanvasTexture(texture);
    const skySphere_geom = new THREE.SphereGeometry(this.worldSize, 32);
    const skySphere_mat = new THREE.MeshBasicMaterial({
      side: THREE.BackSide,
      map: texture,
      fog: false
    });

    this.skySphere = new THREE.Mesh(skySphere_geom, skySphere_mat);
    this.scene.add(this.skySphere);
  }

  /**
   * Turns out all you need to do is set an update flag. You don't need to build a new material etc.
   */
  this.updateSkySphere = function() {
    this.skySphere.material.map.needsUpdate = true;
  }


  /**
   * scale skySphere to fit window size
   * note 1 corresponds to world size, which should be the size of the central square
   */
  this.scaleSkySphereWindow = function() {

    let widthScale = 1;
    const aspect = this.canvas.clientWidth / this.canvas.clientHeight;

    //if landscape
    if (aspect > 1) {
      widthScale = aspect * 1.1;
    } else {      //if portrait
      widthScale = ( widthScale / aspect ) * 1.1;
    }

    this.skySphere.scale.x = widthScale;
    this.skySphere.scale.y = widthScale;
    this.skySphere.scale.z = widthScale;

  }


  /********************************************************
  EVENTS
  ********************************************************/

  /*** mouse camera zoom event handler (wheel only, sorry touch devices) ***/

  this.mouseWheelHandler = function () {
    const that = this;
    document.body.addEventListener('wheel', function (e) {
      e.preventDefault();
      that.handleCameraZoom(e.deltaY);
      that.requestRender();
    }, { passive: false });
  }



  /********************************************************
  LOADING OF PERLIN MAP
  ********************************************************/

  /*** copy in a perlin noise map array
  hmm, does it actually need to make its own copy, given (in the current implementation) the map is just being read from, not changed? ***/
  this.loadMap = function (arrMap) {
    this.map = [];

    for (let m = 0; m < arrMap.length; m++) {
      const mapRow = [];

      for (let n = 0; n < arrMap[0].length; n++) {
        const valToCopy = arrMap[m][n];
        mapRow.push(valToCopy);
      }

      this.map.push(mapRow);
    }

    this.resolution = this.map.length;

  }



  /********************************************************
  MANAGEMENT OF CHILD OBJECTS
  ********************************************************/

  /*** set child x/y positions ***/

  this.setChildPolygonPositions = function () {

    let counter = 0;
    const widthForScale = 1 / this.resolution;
    const widthOfElem = this.landBaseSize / this.resolution;
    const gapModifier = 1.09;          //overdraw on width to get rid of very fine gaps between polygons

    for (let i = 0; i < this.resolution; i++) {
      for (let j = 0; j < this.resolution; j++) {

        //scale down each poly
        this.objectPool[counter].scale.set(widthForScale * gapModifier, widthForScale, widthForScale);

        //position it, plus offset to compensate for the fact it's midhandled and starts from 0
        this.objectPool[counter].position.x = j * (this.landBaseSize / this.resolution) - (this.landBaseSize / 2) + (widthOfElem / 2);
        this.objectPool[counter].position.y = (i * (this.landBaseSize / this.resolution)) - (this.landBaseSize / 2) + (widthOfElem / 2);

        counter++;
      }
    }
  }


  /*** set child heights and subsequently z positions (since polygons are midhandled) ***/

  this.setChildPolygonHeights = function () {

    let counter = 0;

    for (let m = 0; m < this.resolution; m++) {
      for (let n = 0; n < this.resolution; n++) {
        let height = this.normaliseMapValue(this.map[n][m]) * this.heightMultiplier;

        //only apply the scale if height above 0 (otherwise, generates a console warning)
        //if below 0, set to a really small scale value
        if (height <= 0) {
          height = .0001;
        }

        this.objectPool[counter].scale.y = height;
        this.objectPool[counter].position.z = (-(this.landBaseSize * height) / 2) - .0001;          //slight offset to stop a few from poking through

        counter++;
      }
    }

  }


  /*** set child polygon colours ***/

  this.setChildPolygonColors = function () {

    //quickly normalise cols as Dat.gui doesn't round its output from colour picker
    this.landbaseColor[0] = Math.round(this.landbaseColor[0]);
    this.landbaseColor[1] = Math.round(this.landbaseColor[1]);
    this.landbaseColor[2] = Math.round(this.landbaseColor[2]);

    let counter = 0;

    for (let m = 0; m < this.resolution; m++) {
      for (let n = 0; n < this.resolution; n++) {

        const height = this.normaliseMapValue(this.map[n][m]);

        const heightTopColor = this.getTopColor(height);

        const topColThree = new THREE.Color("rgb(" +
          heightTopColor[0] + ", " +
          heightTopColor[1] + ", " +
          heightTopColor[2] + ")"
        );

        const bottomColThree = new THREE.Color("rgb(" +
          this.landbaseColor[0] + ", " +
          this.landbaseColor[1] + ", " +
          this.landbaseColor[2] + ")"
        );

        this.objectPool[counter].geometry.faces[0].vertexColors[0].copy(topColThree);             //top left
        this.objectPool[counter].geometry.faces[0].vertexColors[1].copy(bottomColThree);              //bottom left
        this.objectPool[counter].geometry.faces[0].vertexColors[2].copy(topColThree);               //top right

        this.objectPool[counter].geometry.faces[1].vertexColors[0].copy(bottomColThree);            //bottom
        this.objectPool[counter].geometry.faces[1].vertexColors[1].copy(bottomColThree);              //bottom
        this.objectPool[counter].geometry.faces[1].vertexColors[2].copy(topColThree);               //top right

        this.objectPool[counter].geometry.colorsNeedUpdate = true;

        counter++;
      }
    }

  }


  /*** normalise the map values to start at 0 (output from the noise generator is between -1 and 1)
  Output from this function will be between 0 and this.perlinMinMax * 2. ***/

  this.normaliseMapValue = function (value) {
    const perlinMinMax = this.perlinMinMax;

    let output = value + perlinMinMax;

    if (output < 0) {
      output = 0;
    }

    return output;
  }


  /*** take in a normalised map value and convert to top colour, LERPed between top and bottom colour ***/

  this.getTopColor = function (value) {

    value = value / (this.perlinMinMax * 2);

    //an arr to be returned
    let arrRGB = [0, 0, 0];

    arrRGB = arrRGB.map((color, index) => {
      let valToReturn = Math.round(this.bottomColor[index] + ((this.topColor[index] - this.bottomColor[index]) * value));
      //clamp, as sometimes will be below or above threshold... TODO work out exactly why (I think it's because occasionally the noise generator generates values that are slightly out of bounds)
      return THREE.Math.clamp(valToReturn, 0, 255);
    });

    return arrRGB;
  }



  /********************************************************
  REALTIME UPDATING/RENDERING
  ********************************************************/

  this.handleCameraZoom = function (quant) {
    this.cameraZoom -= (quant / 500);
    if (this.cameraZoom > this.cameraZoomMax) {
      this.cameraZoom = this.cameraZoomMax;
    } else if (this.cameraZoom < this.cameraZoomMin) {
      this.cameraZoom = this.cameraZoomMin;
    }

    this.camera.zoom = this.cameraZoom;

    this.camera.updateProjectionMatrix();
  }


  /*** rotate the child polys in opposite direction to camera rotation (effectively the become billboards on one axis) ***/

  this.updateChildPolygonRotation = function () {
    let counter = 0;

    for (let i = 0; i < this.resolution * this.resolution; i++) {
      this.objectPool[counter].rotation.y = this.controls.getAzimuthalAngle();
      counter++;
    }
  }


  /*** resize renderer (when window size changes) ***/

  this.resizeRendererToDisplaySize = function () {
    const pixelRatio = window.devicePixelRatio;

    if (pixelRatio > 2) {
      pixelRatio = 2;
    }

    const width = this.canvas.clientWidth * pixelRatio | 0;       //CSS width of canvas
    const height = this.canvas.clientHeight * pixelRatio | 0;     //CSS height of canvas
    const needResize = this.canvas.width !== width || this.canvas.height !== height;      //drawingbuffer width/height of canvas

    if (needResize) {
      this.renderer.setSize(width, height, false);
    }

    return needResize;
  }


  /*** set new camera FOV (when renderer size changes) ***/

  this.setFOV = function () {
    this.camera.aspect = this.canvas.clientWidth / this.canvas.clientHeight;

    if (this.camera.aspect < 1) {
      const vFOV = 2 * Math.atan((this.worldSize / this.camera.aspect) / (2 * this.cameraDistance));
      this.camera.fov = THREE.Math.radToDeg(vFOV);
    } else {
      const vFOV = 2 * Math.atan(this.worldSize / (2 * this.cameraDistance));
      this.camera.fov = THREE.Math.radToDeg(vFOV);
    }

    this.camera.zoom = this.cameraZoom;

    this.camera.updateProjectionMatrix();
  }


  /*** per-frame activity ***/

  this.frame = function (time) {
    this.renderRequested = false;

    this.updateChildPolygonRotation();

    if (this.resizeRendererToDisplaySize()) {
      this.setFOV();
      this.scaleSkySphereWindow();
    }

    this.controls.update();

    this.renderer.render(this.scene, this.camera);
  }


  /*** check if frame has been requested ***/

  this.requestRender = function () {
    if (!this.renderRequested) {
      this.renderRequested = true;
      window.requestAnimationFrame(this.frame.bind(this));
    }
  }

  //also setup window resize event
  window.addEventListener('resize', this.requestRender.bind(this));

}


/********************************************************

Main

********************************************************/

//global resolution max
const MAX_PERLIN_MAP_RESOLUTION = 100;
const DEFAULT_PERLIN_MAP_RESOLUTION = 32;

const skyTexture = new SkyTexture(512, 512);
skyTexture.init();
skyTexture.render();

const perlinNoiseGenerator = new PerlinNoiseGenerator();
perlinNoiseGenerator.outputMap();

const landscape = new Landscape();
landscape.init();
landscape.createSkySphere(skyTexture.ctx.canvas);
landscape.scaleSkySphereWindow();
landscape.loadMap(perlinNoiseGenerator.mapData);
landscape.setObjectsVisible();
landscape.setChildPolygonPositions();
landscape.setChildPolygonHeights();
landscape.setChildPolygonColors();

landscape.frame();



/**** code to hook up GUI ***/

const gui = new dat.GUI();

//set folders
const folderSettingsLand = gui.addFolder('Visualisation Settings');
const folderColorsLand = gui.addFolder('Visualisation Colours');
const folderColorsBG = gui.addFolder('Background Colours');
const folderSettingsColorsFog = gui.addFolder('Fog Settings');
const folderMisc = gui.addFolder('Misc Settings');


//set controls
const seedChange = folderSettingsLand.add(perlinNoiseGenerator, 'seed', 0, 65535).step(1).name('Seed');
const zoomChange = folderSettingsLand.add(perlinNoiseGenerator, 'zoom', .1, 5).name('Scale');
const resolutionChange = folderSettingsLand.add(perlinNoiseGenerator, 'resolution', 4, MAX_PERLIN_MAP_RESOLUTION).step(1).name('Resolution');

const childColorTop = folderColorsLand.addColor(landscape, 'topColor').name('Land Higher');
const childColorBottom = folderColorsLand.addColor(landscape, 'bottomColor').name('Land Lower');
const landBaseColor = folderColorsLand.addColor(landscape, 'landbaseColor').name('Land Base');

const bgColorTop = folderColorsBG.addColor(skyTexture, 'topColor').name('BG Top');
const bgColorBottom = folderColorsBG.addColor(skyTexture, 'bottomColor').name('BG Bottom');

const fogColor = folderSettingsColorsFog.addColor(landscape, 'fogColor').name('Fog Colour');
const fogOnOff = folderSettingsColorsFog.add(landscape, 'fogOn');

const autoRotateOnOff = folderMisc.add(landscape.controls, 'autoRotate').listen();


//open folders from start
folderSettingsLand.open();
//folderColorsLand.open();
//folderColorsBG.open();
//folderSettingsColorsFog.open();
//folderMisc.open();


seedChange.onChange(function() {
  perlinNoiseGenerator.outputMap();
  landscape.loadMap(perlinNoiseGenerator.mapData);
  landscape.setChildPolygonHeights();
  landscape.setChildPolygonColors();
  landscape.requestRender();
});


zoomChange.onChange(function() {
  perlinNoiseGenerator.outputMap();
  landscape.loadMap(perlinNoiseGenerator.mapData);
  landscape.setChildPolygonHeights();
  landscape.setChildPolygonColors();
  landscape.requestRender();
});


resolutionChange.onChange(function() {
  perlinNoiseGenerator.outputMap();
  landscape.loadMap(perlinNoiseGenerator.mapData);
  landscape.setAllObjectsInvisible();
  landscape.setObjectsVisible();
  landscape.setChildPolygonPositions();
  landscape.setChildPolygonHeights();
  landscape.setChildPolygonColors();
  landscape.requestRender();
});


landBaseColor.onChange(function() {
  landscape.setLandbaseColor();
  landscape.setChildPolygonColors();
  landscape.requestRender();
});

childColorTop.onChange(function() {
  landscape.setChildPolygonColors();
  landscape.requestRender();
});

childColorBottom.onChange(function() {
  landscape.setChildPolygonColors();
  landscape.requestRender();
});

fogColor.onChange(function() {
  landscape.setFog();
  landscape.requestRender();
});

fogOnOff.onChange(function() {
  landscape.setFog();
  landscape.requestRender();
});

bgColorTop.onChange(function() {
  skyTexture.render();
  landscape.updateSkySphere();
  landscape.requestRender();
});

bgColorBottom.onChange(function() {
  skyTexture.render();
  landscape.updateSkySphere();
  landscape.requestRender();
});

autoRotateOnOff.onChange(function () {
  landscape.requestRender();
  landscape.controls.update();
});

gui.close();


/** VERY quick last minute code to add a label that fades out when you start dragging the model around */
function removeInstructions() {
  const $instructions = document.querySelector('.js-label--dragInstruct');

  if ($instructions) {
    $instructions.classList.add('fadeOut');
  }
}

window.addEventListener('click', removeInstructions);
window.addEventListener('wheel', removeInstructions);
window.addEventListener('touchstart', removeInstructions);
              
            
!
999px

Console