Banner

Tuesday, 15 October 2013

Terrain visualization with three.js and Oculus Rift

My good colleague, Harald K. Jansson, owns an Oculus Rift headset, and he couldn’t resist creating a virtual reality version of my 3D map of Jotunheimen. It’s very impressive, especially when you know that everything runs in the browser.

If you’re lucky enough to own a Oculus Rift headset you can try yourself by clicking on the image below.



The demo is built using two Oculus plugins:

We’ve also switched from TrackballControls to FirstPersonControls which allows you to fly through the landscape. Mouse navigation is disabled as it didn`t work well with the Oculus control, but you can move around with your keyboard:
  • A / “arrow left”: Move left
  • D / “arrow right”: Move right
  • W / “arrow up”: Move forward
  • S / “arrow down”: Move backward
  • R: Move up
  • F: Move down

The code is avaiable on GitHub. Enjoy!

Monday, 14 October 2013

Textural terrains with three.js

This is the third blog post in my "terrain mapping with three.js" series. With the terrain geometry in place, it's time to create a texture and drape it over the terrain.

In the first blog post in this series, we created a digital elevation model (DEM) for Jotunheimen. We can use this DEM to create color relief and hillshade that will enhance the view of our terrain (this blog post explains the details).

gdaldem color-relief jotunheimen.tif color-relief.txt jotunheimen-relief.tif



Creating nice-looking hillshades got easier with GDAL 1.10. By adding the "combined" option to gdaldem you get a slope-enhanced shaded relief:

gdaldem hillshade -combined jotunheimen.tif jotunheimen-hillshade.tif


I'm combining color relief and hillshade with Mapnik, and at the same time adding colors for lakes, glaciers and trees using open data from the Norwegian Mapping Authority (this blog post explains the details).

nik2img.py jotunheimen-texture.xml jotunheimen-texture.png --projected-extent 432000 6790000 492000 6850000 -d 4096 4096

The texture styles are available on GitHub. The final texture (converted to JPG to save some bandwith) looks like this:


Adding the texture to the terrain mesh is dead easy: Use THREE.ImageUtils.loadTexture to load the texture and add it to the map property of the material:

var material = new THREE.MeshPhongMaterial({
  map: THREE.ImageUtils.loadTexture('../assets/jotunheimen-texture.jpg')
});

We also need to add some light to see the texture:

scene.add(new THREE.AmbientLight(0xeeeeee));

That's all! Our 3D terrain now looks like this (click to see in WebGL):

View of Jotunheimen from the south.

View of Jotunheimen from the north.  Galdhøpiggen, the highest mountain in Norway (2469 m), is among the glaiers in the middle. 

The code is available on Github. In the next blog post, we'll create a virtual reality version of our terrain.

Wednesday, 9 October 2013

Terrain building with three.js

In my last blog post, we converted a digital elevation model (DEM) to a WebGL-friendly format (i.e. easy to transfer and parse by JavaScript in the browser). In this blog post, we'll use the elevation data to build a terrain mesh with three.js

First we need to transfer the terrain data to the browser. The elevation values are stored in a binary file as 16-bit unsigned integers. This page explains how you can send and receive binary data using JavaScript typed arrays. I've created a TerrainLoader by adapting the XHRLoader. You can also use this function:

function loadTerrain(file, callback) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'arraybuffer';
  xhr.open('GET', file, true);
  xhr.onload = function(evt) {    
    if (xhr.response) {
      callback(new Uint16Array(xhr.response));
    }
  };  
  xhr.send(null);
}

Loading elevation data with the TerrainLoader is easy:

var terrainLoader = new THREE.TerrainLoader();
terrainLoader.load('../assets/jotunheimen.bin', function(data) {
  console.log(data);
});

The loader will return the elevation data as an array:

[37068, 38613, 39605, 40451, 39655, 38843, 38843, 38857, 39042, 39316, 40165, 40738, 40369, 39483, 37175, 34492, 32436, 31600, 33473, 37514, ...]

To preserve as much detail as possible, we were scaling the floating point elevation values (0-2470) to the full range of a 16-bit unsigned integer (0-65535). You'll find the elevation value with this formula: 

var height = Math.round(value / 65535 * 2470);

[1397, 1455, 1493, 1525, 1495, 1464, 1464, 1465, 1471, 1482, 1514, 1535, 1521, 1488, 1401, 1300, 1223, 1191, 1262, 1414, ...]


From heights stored in an array to a triangle mesh.

Creating a triangle mesh from our elevation data is straightforward, and three.js does the heavy work for us. Use THREE.PlaneGeometry to create a ground plane:

var geometry = new THREE.PlaneGeometry(60, 60, 9, 9);

The first two arguments are the width and height of the plane geometry. The next two arguments are the number of width and height segments. It will create this triangle mesh (click to see in WebGL):

The vertices are at the corners of the triangles. Each vertex has a X/Y/Z position. The red line is the X axis,  the green line is the Y axis and blue line is the Z axis (height). 

The width and height segments should be the width and height of your elevation grid minus one. The binary file we created is 200 x 200, so our plane geometry is 199 x 199:

var geometry = new THREE.PlaneGeometry(60, 60, 199, 199);

three.js stores all vertices in an array (geometry.vertices) similar to our array of height values (row by row, top to bottom). This makes it very easy to change the Z position:

for (var i = 0, l = geometry.vertices.length; i < l; i++) {
  geometry.vertices[i].z = data[i] / 65535 * 25;
}

If we render this geometry we get this result (click to see in WebGL):
Jotunheimen wireframe

var geometry = new THREE.PlaneGeometry(60, 60, 199, 199);

for (var i = 0, l = geometry.vertices.length; i < l; i++) {
  geometry.vertices[i].z = data[i] / 65535 * 10;
}

var material = new THREE.MeshPhongMaterial({
  color: 0xdddddd, 
  wireframe: true
});

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

Besseggen wireframe

So we got our 3D landscape with just a few lines of code (available on GitHub). In the next blog post we'll add some texture to make it more realistic.

Romsdalseggen ridge in August. 

Sunday, 6 October 2013

Converting terrain data to a WebGL-friendly format

Three.js is a very promising tool if you want to add a third dimension to your web maps. In my last blog post, I showed how easy it was to create a WebGL Earth in the browser. Today, I'm starting a new blog series about terrain building with three.js.


Last year, I wrote a blog series about all the fun you can do with digital terrain data:
  1. Digital terrain modelling and mapping
  2. Creating hillshades with gdaldem
  3. Creating color relief and slope shading with gdaldem
  4. Terrain mapping with Mapnik
  5. Land cover mapping with Mapnik
  6. Using custom projections with TileCache, Mapnik and Leaflet
  7. Creating contour lines with GDAL and Mapnik
  8. Showing GPS tracks with Leaflet
I will continue using map data from Jotunheimen, a mountainous area of Norway. The 29 highest mountains in Norway are all in Jotunheimen, as well as the deepest valley, Utladalen. It's a great spot for 3D terrain mapping. But the same techniques applies to all terrains, - you only need some terrain data. Instead of Leaflet, we're going to use WebGL and three.js to render the maps. 

Last friday, 27th September 2013, was a milestone in the mapping history of Norway. The Norwegian Mapping Authority released its topographic datasets to the public, free of charge. Included was also a digital elevation model (DEM) of the Norwegian mainland, at 10 meters resolution. You can download the data on this page (unfortunately only in Norwegian), under a CC BY 3.0 licence. The terrain files created in this blog post are also available on GitHub.

Norway is divided into 50 x 50 km tiles, and you can select the areas you want to download on a map.
We need 4 tiles to cover Jotunheimen.  

The DEM files are using the USGS file format for terrain data, and I've selected the UTM 32N projection for my data. I'm using gdalbuildvrt to create a combined virtual dataset (jotunheimen.vrt) of the DEM files: 

gdalbuildvrt jotunheimen.vrt 6704_1_10m_z32.dem 6704_4_10m_z32.dem 6804_2_10m_z32.dem 6804_3_10m_z32.dem

Then we can use gdalwarp to clip the DEM to the area of interest and convert to GeoTIFF

gdalwarp -te 432000 6790000 492000 6850000 jotunheimen.vrt jotunheimen.tif

Use gdalinfo to see the properties of this image:

gdalinfo -mm jotunheimen.tif

Driver: GTiff/GeoTIFF
Files: jotunheimen.tif
Size is 6000, 6000
Coordinate System is:
PROJCS["WGS 84 / UTM zone 32N",
    GEOGCS["WGS 84",
        DATUM["WGS_1984",
            SPHEROID["WGS 84",6378137,298.257223563,
                AUTHORITY["EPSG","7030"]],
            AUTHORITY["EPSG","6326"]],
        PRIMEM["Greenwich",0],
        UNIT["degree",0.0174532925199433],
        AUTHORITY["EPSG","4326"]],
    PROJECTION["Transverse_Mercator"],
    PARAMETER["latitude_of_origin",0],
    PARAMETER["central_meridian",9],
    PARAMETER["scale_factor",0.9996],
    PARAMETER["false_easting",500000],
    PARAMETER["false_northing",0],
    UNIT["metre",1,
        AUTHORITY["EPSG","9001"]],
    AUTHORITY["EPSG","32632"]]
Origin = (432000.000000000000000,6850000.000000000000000)
Pixel Size = (10.000000000000000,-10.000000000000000)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (  432000.000, 6850000.000) (  7d42'39.90"E, 61d46'36.81"N)
Lower Left  (  432000.000, 6790000.000) (  7d43'59.45"E, 61d14'18.21"N)
Upper Right (  492000.000, 6850000.000) (  8d50'54.02"E, 61d46'58.29"N)
Lower Right (  492000.000, 6790000.000) (  8d51' 3.39"E, 61d14'39.21"N)
Center      (  462000.000, 6820000.000) (  8d17' 9.19"E, 61d30'42.34"N)
Band 1 Block=6000x1 Type=Float32, ColorInterp=Gray
    Computed Min/Max=1.900,2467.800


What does it tell us?
  • The image or grid size is 6000 x 6000 pixels or cells.
  • The coordinate system is UTM zone 32N using the WGS 84 spheroid.
  • The coordinate unit is meters.
  • Each pixel or grid cell covers an area of 10 x 10 meters.
  • The extent of the area is defined by four coordinate pairs, covering an area of 3,600 km² (60 x 60 km).
  • The image is single band and the elevation data is stored as 16-bit integers. The minimum elevation is 1.9 meters and the maximum is 2,467.8 meters.

We got the data and the information we need to start terrain modelling. Our first task is to transfer the terrain data to the browser. As most web browsers don't support TIFF-files, it's common to convert the DEM into a heightmap. It can be thought of as a grayscale image where the intensity of each pixel represents the height at that position. Black indicates the minimum height and white the maximum height. It's easy to create a heightmap with gdal_translate:

gdal_translate -scale 0 2470 0 255 -outsize 200 200 -of PNG jotunheimen.tif jotunheimen.png

This command will create a PNG where the height values are reduced to 256 shades of gray. I'm also reducing the size from 6000 x 6000 px to only 200 x 200 px to save our GPU. Each pixel or elevation value is now covering an area of 300 meters.  


We can then transfer the image to the browser, and read the height values directly from the image. This can be sufficient for many uses, but I'm afraid my terrain will look "blocky" with only 256 height values. It's possible to use colour channels to get a wider height span with PNGs, but I couldn't find an easy way to achieve this with GDAL. By reading these notes, I saw that I could use a format called ENVI. In this format the height values are provided as a sequence of raw bytes, and I'm storing the values as 16-bit unsigned integers:

gdal_translate -scale 0 2470 0 65535 -ot UInt16 -outsize 200 200 -of ENVI jotunheimen.tif jotunheimen.bin

To preserve as much detail as possible, I'm scaling the floating point elevation values to the full range of a 16-bit unsigned integer (0-65535). I'm also creating a heightmap in full 10 m resolution of the Besseggen mountain ridge (2 x 2 km): 

gdalwarp -te 484500 6818000 486500 6820000 jotunheimen.vrt besseggen.tif

gdal_translate -scale 982 1742 0 255 -of PNG besseggen.tif besseggen.png

gdal_translate -scale 982 1905 0 65535 -ot UInt16 -of ENVI besseggen.tif besseggen.bin



Lastly, I'm creating a tiny heightmap of only 10 x 10 px for testing purposes: 

gdal_translate -scale 982 1905 0 255 -outsize 10 10 -of PNG besseggen.tif besseggen10.png

gdal_translate -scale 982 1905 0 65535 -outsize 10 10 -ot UInt16 -of ENVI besseggen.tif besseggen10.bin




In the next blog post, we'll start playing with our terrain data in three.js.

Autumn in Nordmarka in Oslo. Photo: Bjørn Sandvik, 5th October 2013.