WebGL Terrain

This tutorial will walk through creating a 3D terrain from digital elevation data. In WebGL there are a few different ways to do this. Here are some other tutorials that might work better if you have access to some of these outside resources.

MapBox
QGIS Three.js Plugin (will loose resolution)
ArcGIS

The tutorial below is based on the work of Bjørn Sandvik and his terrain tutorial and uses his terrain loader framework.

http://blog.mastermaps.com/2013/10/terrain-building-with-threejs-part-1.html

The source files included in the .zip file were created using QGIS, a free download.

https://qgis.org

Processing Terrain Files in QGIS

A GeoTIFF file was created using digital elevation model files (DEM) for Delani. Denali DEM files can be downloaded here:

https://elevation.alaska.gov/

If you would prefer you can download the resulting GeoTIFF here:

Denali_GeoTIFF.tif

The texture for the terrain uses landcover data, which can be downloaded here:

https://catalog.data.gov/dataset/nlcd-2011-land-cover-alaska

These files were both imported into QGIS. The DEM and landcover layers were both exported to GeoTIFF files with matching boundaries. The GeoTIFF file was transformed into a .bin file using the method described by Bjørn Sandvik in his original tutorial. The GeoTIFF and .bin are both included in the starter .zip.


HTML Page & Three JS

First set up an HTML page that will hold the canvas and load the three.js framework. A starter page already linked to the three.js framework is included in the .zip file.

step_1_pieces.zip

Download the final version as well if you would like.

step_5_terrainmix.zip

Making a Scene

Three.js includes some standard pieces to making a scene. The basics include four things:

  • a scene to view
  • a renderer to visualize the display
  • a camera positioned in space to view the sccene
  • something to look at (an axis for now)

First lets add some variables to use.

// html elements

var appBody = document.getElementById('appbody')

var appWebGL = document.getElementById('webgl')

// width and height

var width = window.innerWidth;

var height = window.innerHeight;

Add the scene.

// 3d space

var scene = new THREE.Scene();

Then add the renderer. The renderer creates a canvas element, which we need to insert into the HTML. The renderer needs the width and height of the scene, which we added as variables. We also have to call and add a method to render the scene each frame.

// renderer to visualize the scene

var renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setSize(width, height);

renderer.setClearColor( 0x000000, 0 );

appWebGL.appendChild(renderer.domElement);

// for rendering the scene every frame.

render();

function render() {

 requestAnimationFrame(render);

 renderer.render(scene, camera);

}

Add a camera to view the scene and have it look at 0,0,0.

// camera to view the scene

var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100000);

camera.position.set(20, 25, 70);

camera.lookAt(new THREE.Vector3(0,0,0));

Add a set of x, y, z axis so we have something to look at.

// axis for visual reference

var axes = new THREE.AxesHelper(200);

scene.add(axes);

scene.background = new THREE.Color( 0xffffff );


Moving Around

Adding a controller makes the scene interactive and lets the user move the camera around. Three.js has three main controllers:

  • trackball controller for orbiting an object
  • orbital controller for locking on a point and rotating around (but keeping horizontal orientation)
  • pointer lock controller for first-person movement

For this example I used an orbital controller. Sometimes with trackball the subject matter can twist into weird orientations and orbital controller prevents.

// controls to navigate the scene

var controls = new THREE.OrbitControls(camera);

The controller must also be updated inside the render method.

...

controls.update();

...

If you would like, the "camera.lookAt" method can come out as well since the controller will automatically look to (0,0,0).


Loading the Terrain

An external framework called TerrainLoader.js from Bjørn Sandvik loads the .bin file and processes for display.

After the .bin file is loaded a callback runs to turn the bin into terrain vertices. A material is created and added that allows us to look at the terrain as a wireframe.

Some important points to note:

  • The plane geometry passes some size parameters, the last two are the resolution of the bin width and height in pixels MINUS ONE.
  • The first two parameters are for width and height, they have to be the same ratio as the bin pixels.
  • Depending on the number of points in the terrain the browser could time out creating a terrain. You may need to create tiles and load multiple terrains.

var terrainLoader = new THREE.TerrainLoader();

terrainLoader.load('textures/Denali_50.bin', function(data) {

 var geometry = new THREE.PlaneGeometry(84.0, 69.0, 839, 689);

 for (var i = 0, l = geometry.vertices.length; i < l; i++) {

  geometry.vertices[i].z = data[i] / 65535 * 5;

 }

 var material = new THREE.MeshPhongMaterial({

  color: 0xdddddd,

  wireframe:true,

 });

 var plane = new THREE.Mesh(geometry, material);

 plane.rotation.x = this.convertDegToRad(-90)

 scene.add(plane);

});


Shaded Relief Texture

Adding a basic shaded relief rendered and exported in QGIS will help us see hte terrain better.

Inside the terrain loader callback load a texture, in this example it's set up as a new variable called "material2".

var textLand = THREE.ImageUtils.loadTexture('textures/Relief.jpg')

textLand.minFilter = THREE.NearestFilter

var material2 = new THREE.MeshBasicMaterial({

 map: textLand

});

In the mesh swap out the material from the wireframe to the loaded texture.

...

var plane = new THREE.Mesh(geometry, material2);

...


Final Terrain

The final terrain is a composite of the land cover exported out of QGIS and the relief worked in through Photoshop. Swap out the loaded texture name.

...

var textLand = THREE.ImageUtils.loadTexture('Relief_Land.jpg')

...