Saturday 28 September 2013

Creating a WebGL Earth with three.js

This blog post will show you how to create a WebGL Earth with three.js, a great JavaScript library which helps you to go 3D in the browser. I was surprised how easy it seemed when reading a blog post by Jerome Etienne. So I decided to give it a try using earth textures from one of my favourite cartographers, Tom Patterson.


WebGL is a JavaScript API for rendering interactive 3D graphics in modern web browsers without the use of plug-ins. Three.js is built on top of WebGL, and allows you to create complex 3D scenes with a few lines of JavaScript. If your browser supports WebGL you should see a rotating Earth below:


[ Fullscreen ]

To be able to display something with three.js, you need three things: a scene, a camera and a renderer.

var width  = window.innerWidth,
    height = window.innerHeight;

var scene = new THREE.Scene();

var camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 1000);
camera.position.z = 1.5;

var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);

The scene is the container used to store and keep track of the objects (earth and stars) we want to render. The camera determines what we'll see when we render the scene. I'm using a PerspectiveCamera, which is what we usually associate with seeing the world. The renderer is responsible to render the scene in the browsers. Three.js supports different renderers like WebGL, Canvas, SVG and CSS 3D. We use window width and height to allow our earth to fill the browser window.

Next we have to turn on the light:

First: Ambient light - Second: Directional light - Third: Ambient and directional

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

var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5,3,5);
scene.add(light);

Three.js support different light sources that have specific behaviour and uses. I'm using ambient and directional light:
  • Ambient light: Basic light which is applied globally.  The dimmed ambient light shows areas away from the sun.
  • Directional light: Light that mimics the sun. All the light rays we receive on Earth are parallel to each other. 

Ok, we got the scene, camera, renderer and light sorted. It's time to model our Earth using sphere geometry and material, which is referred to as a mesh in three.js:

new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshPhongMaterial({
    map: THREE.ImageUtils.loadTexture('images/2_no_clouds_4k.jpg'),
    bumpMap: THREE.ImageUtils.loadTexture('images/elev_bump_4k.jpg'),
    bumpScale:   0.005,
    specularMap: THREE.ImageUtils.loadTexture('images/water_4k.png'),
    specular: new THREE.Color('grey')   })
);

The sphere is created using THREE.SphereGeometry. The first parameter is the radius, and the second and third parameter is the number of width and height segments. The sphere is drawn as a polygon mesh, and by adding more segments it will be less "blocky" - and take more time to render.

A sphere rendered with 8, 16 and 32 width/height segments. 

Next, we use THREE.MeshPhongMaterial to wrap map data around the sphere. This material is used to create shiny materials, and we use it to make the ocean reflective.

I'm using map data from shadedrelief.com. This is a great collection of shaded relief maps by cartographer Tom Patterson. Natural Earth III is collection of raster map data tailored towards visualising Earth from space. Compared to photographs of Earth, Natural Earth III offers brighter colours, fewer clouds and distinct environmental zones. The maps are very pleasant to look at.

Let`s start with a texture map of the Earth without clouds:


I reduced the image size to 4096 x 2048 px, which was the maximum texture size for the GPU of my computer. If you want more detailed textures you need to slice up the Earth.

Second, I`m using a bump map to enhance the view of the mountains:


Bump mapping is a technique to simulate bumps and wrinkles on the surface of an object. The result is an apparently bumpy surface rather than a smooth surface although the surface of the underlying object is not actually changed. I'm sorry, you can't tilt the camera to see 3D mountains with this technique. You can adjust the bump effect (how much the map affects lighting) with the bumpScale parameter.

The original image (left) contains shaded relief, so the bump map effect is limited on this texture (right).   

Lastly, I want to make the ocean and lakes reflective by applying a land/water mask. This specular map defines the surface's shininess. Only the sea is specular because water reflects water more than earth. You can control the specular color with specular parameter.




Adding a specular map to make the ocean reflective (right).

Our Earth looks good, but I still miss some clouds. 64 percent of Earth's surface is obscured by clouds on an average day, so this cloud texture received Photoshop edits to remove clouds from land areas.


I couldn't use this JPEG directly in three.js, so I uesd this technique to make a transparent PNG (available on GitHub). I then created a new sphere mesh with a slightly larger radius:

new THREE.Mesh(
  new THREE.SphereGeometry(0.503, 32, 32),
  new THREE.MeshPhongMaterial({
    map: THREE.ImageUtils.loadTexture('images/fair_clouds_4k.png'),
    transparent: true
  })
);

Our planet on a very clear day.

The last thing we'll add to our scene is a galaxy starfield:

Starfield from the "Learing Three.js" website.

The starfield is created by adding a large sphere around the Earth and project the star texture on the backside or inside:

new THREE.Mesh(
  new THREE.SphereGeometry(90, 64, 64), 
  new THREE.MeshBasicMaterial({
    map: THREE.ImageUtils.loadTexture('images/galaxy_starfield.png'), 
    side: THREE.BackSide
  })
);

Earth in space.

As a final touch, let's add some interaction:

var controls = new THREE.TrackballControls(camera);

render();

function render() {
  controls.update();
  sphere.rotation.y += 0.0005;
  clouds.rotation.y += 0.0005;
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}

I've added the trackball controls plugin by Eberhard Graether,  which allows you to rotate, zoom and pan the scene. The render function is called at a specific interval defined by the browser using the requestAnimationFrame function. The sphere and clouds are rotating around its axis by increasing the y parameter.

Here you can see the final result!

A long blog post, but only 84 lines of code, - available on Github