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

11 comments:

Anonymous said...

Nice example! Although I would always recommend using semi-lossless formats for textures this big, especially for bump maps, because the JPG artifacts get bumped too, and it's noticeable in your demo ;)

Anonymous said...

Very cool, how would you go about plotting GPS based locations on globe. Any thoughts on adding geometry for visualization purposes.

Bjørn Sandvik said...

Anonymous 1: I agree, but the original texture was not available in a lossless format.

Anonymous 2: See this blog post.

Anonymous said...

Fantastic - thank you!

I'm trying to display my version full screen (maximized browser) and the planet is displaying fine but the stars seem very blurry/'zoomed in'. Do I need to use a very much large starfield image instead?

Anonymous said...

Hi, Thanks a lot for this tutorial !

I am however having some troubles in executing your code (from gitHub). It seems everything works fine, except that the textures aren't displayed (I only see a black sphere on a black background with some lightning effects).

I am quite new to threejs and I could'nt find any solution to solve this problem... Do you have any idea ?

Note that I work on windows7, using chrome and the latest release of threejs, i.e. revision 68

Anonymous said...

Neat, but doesn't work on iOS :(.

Marvin Danig said...

I get the following error when I do this:

Error: ERROR: Quaternion's .setFromEuler() now expects a Euler rotation rather than a Vector3 and order. Please update your code.

When I update the code it says:

SecurityError: The operation is insecure.

And hangs the browser.

MIMATA said...

Hello,

No solution for IOS ? It works except the texture whitch is not applied normally...

Marchal said...

A mobile solution would be perfect, tried lots of things nothing changed.

Any suggestions?

On a mobile browsern the textures are messed up ... :(

Marchal said...

SOLUTION FOR MOBILE:

change all images (width) from 4096 to 2048 and you are good to go on your mobile browser!

You're welcome!

Anonymous said...

Hi, I'd like to try coding the spinning globe but I don't know where to start. The first thing you tell us to code is:
var width = window.innerWidth,


After copying this and all the rest of the code on your page it does not work. Don't I need some supporting HTML?. Don't I need to point to three.js in a script tag? I downloaded three.js and put it on my server and made a script reference to it. I also placed all your code in a function, and added a button to call the code when pressed. But it crashes as soon as it encounters the "var scene =" line.

WebGL works on my browser. I have no trouble seeing the spinning globe on your page. Where do I start? Thanks!