Phil Ingrey bio photo

Phil Ingrey

'The dumbest smart person I know' - a friend

Email Twitter LinkedIn Github CV

I recently stated using three.js to play around with webgl and here are the first fruits - procedurally generating planets with javascript. The demo is avalible here.

Method:

I ended up using an oldie-but-goodie method based on hemisphere perturbation: you choose a random hemisphere and slightly nudge it outwards and nudge the other hemisphere inwards. Do this a lot of times and you have a reasonable looking planet:

1, 5, 50 and 500 iterations of perturbation showing the development of coastlines

This method has one drawback - one side is the relief of the other, i.e. if one point is above the water level then the opposite point will be underwater. This can be fixed but takes more iterations to look authentic.

All in all, whilst this is certainly not the fastest method, its quick to setup and and easy to understand.

Lessons learnt:

The main issue is that it can take a while to generate the planet and the browser becomes unresponsive during that time. The solution? Move the code across to a web worker. As a result the main code becomes clearer but debugging becomes more of a pain: for debugging web workers I'd recommend chrome - errors in the web worker still display in the console!

Code:

The code to generate the planet geometry is in this script. Here I'll go through the main three.js code. Firstly we have the basic three.js set up:

var WIDTH = 400,
  HEIGHT = 400;

// set some camera attributes
var VIEW_ANGLE = 45,
  ASPECT = WIDTH / HEIGHT,
  NEAR = 0.1,
  FAR = 10000;

// create a WebGL renderer, camera
// and a scene
var renderer = new THREE.WebGLRenderer();
var camera =
  new THREE.PerspectiveCamera(
    VIEW_ANGLE,
    ASPECT,
    NEAR,
    FAR);

var scene = new THREE.Scene();

// add the camera to the scene
scene.add(camera);

// the camera starts at 0,0,0
// so pull it back
camera.position.z = 200;

// create a point light
var pointLight =
  new THREE.PointLight(0xFFFFFF);

// set its position
pointLight.position.x = 10;
pointLight.position.y = 50;
pointLight.position.z = 250;

// add to the scene
scene.add(pointLight);

// start the renderer
renderer.setSize(WIDTH, HEIGHT);

// attach the render-supplied DOM element
document.body.appendChild( renderer.domElement );

Then set up the textures, the idea is to have a water sphere and a land sphere overlapping so when the land sinks past a certain radius you see the water (i.e. it defines a sea level):

// set up the sphere vars
var radius = 50,
    segments = 256,
    rings = 256;

// earth
var landTexture = new THREE.Texture();
var landloader = new THREE.ImageLoader();

landloader.addEventListener( 'load', function ( event ) {
    landTexture.image = event.content;
    landTexture.needsUpdate = true;
} );
landloader.load( './textures/terrain/backgrounddetailed6.jpg' );

// water
var waterTexture = new THREE.Texture();
var waterloader = new THREE.ImageLoader();

waterloader.addEventListener( 'load', function ( event ) {
    waterTexture.image = event.content;
    waterTexture.needsUpdate = true;
} );
waterloader.load( './textures/water.jpg' );

// create a new mesh with
// sphere geometry
var geometry;
var material;
var sphere;

material = new THREE.MeshLambertMaterial( { map: landTexture, overdraw: true } );
material.color.setHex(0xFFFFFF);

// Water
var geometryw = new THREE.SphereGeometry( radius, segments, rings );
var materialw = new THREE.MeshLambertMaterial( { map: waterTexture, overdraw: true } );
materialw.color.setHex(0x0000FF);
var spherew = new THREE.Mesh( geometryw, materialw );

Now all that's left is the land geometry, here's where the web worker comes in:

// load up the web worker
var worker = new Worker('./world_gen.js');

// define what happens when we get a message

worker.addEventListener('message', function(e) {
    if(e.data.msg == "progress"){
        // update the progress bar
        document.getElementById('debug').innerHTML = (e.data.perc+'').slice(0,4)+"% complete";
    } else if(e.data.msg == "finished"){
        // If this isn't the first render remove the last one
        if(sphere) {scene.remove(sphere);}

        // calculation is finished - update the geometry
        document.getElementById("debug").innerHTML = "";
        for(var i=0;i<e.data.verts.length;i++){
            geometry.vertices[i].x = e.data.verts[i].x;
            geometry.vertices[i].y = e.data.verts[i].y;
            geometry.vertices[i].z = e.data.verts[i].z;
        }

        sphere = new THREE.Mesh( geometry, material );

        scene.add(sphere);
    } else {
        console.log(e);
    }

},false);

// finally, when the start button is clicked fire a message to the web worker to get started
document.getElementById("startb").addEventListener("click",function(e){
    scene.add(spherew);

    geometry = new THREE.SphereGeometry( radius, segments, rings );

    worker.postMessage({"cmd": "run", "offcenter":document.getElementById("offcentercb").checked ,"verts": geometry.vertices, "radius": radius, "N": document.getElementById('itttext').value});
});

Then all that's left is to add a slow rotation and it's done.