2014-02-22

WebGL Earth

Since falling down a mountain back in November I'm currently in no shape to enjoy the outdoors. So I have spent the last couple of weekends toying with an ancient game idea of mine. Technology has moved on, so this time around I'm using JavaScript and WebGL on the client and a Go server hosted on App Engine. It's been quite a ride so far, learning three new programming languages, a new environment and tricky maths at the same time. But it's fun and the results look rather promising ;-) This blog post journals some of the epiphanies I had along the way.

The design calls for a three dimensional rendering of planet earth as seen from space. So the first thing I wanted to figure out is where the sun is relative to the earth. I'd like to correctly represent the current time and season. I naively assumed I'd type my query into Google and end up with a simple formula that spits out coordinates for a given date and time. Not so much. Turns out astronomy is rather complicated! Apparently there is no exact analytical solution to the set of equations describing the planets' positions. So you end up using approximations that are only valid within a certain time frame of about a hundred years. For the purposes of this calculation it makes sense to pretend the sun is orbiting the earth instead of the other way around. You plug in a bunch of constants like argument of the perihelion, mean anomaly, eccentricity, semi-major axis and inclination to the ecliptic and churn them through a giant formula. You'll end up with declination and right ascension. Declination is easy enough, it corresponds to latitude. But what is this ascension thing? The celestial coordinate system uses a different reference system than the usual latitude/longitude one. Instead of using the earth's equator as the origin it uses the imaginary plane of the earth's orbit around the sun. Similarly instead of putting the longitude zero point at Greenwich it is instead located wherever the March equinox is that year. Fair enough, so where is that? So you learn about Sideral Time and compute the Greenwich Mean Sideral Time to convert from one coordinate system to another. Now JavaScript throws some wrenches into your machinery by counting months starting from zero (March is 2 - seriously, who does that?!) and you confuse radians vs degrees a bunch of times and finally you end up with the lat/lon coordinates for the sun for any given UTC time. Phew! If it wasn't for this awesome write up by Paul Schlyter I'd have given up on the endeavor.

Next up is rendering the surface of the earth. Luckily the Nasa Earth Observatory provides an excellent high resolution data set for free. The Blue Marble has painstakingly been stitched together from thousands of individual satellite images and corrected for distortions, seams, color inconsistencies and cloud cover. Applying this texture to a sphere already looks quite good. It would be nice to see mountains though, so I learn how to write GLSL shaders in order to apply bump mapping. Shaders are small programs that run on the graphics hardware. Vertex shaders transform the geometry while fragment shaders work with pixel data. This is my first contact with the technology and I'm impressed how easy and expressive the language is. It's based on C syntax which I'm very familiar with, but adds powerful primitives for performing linear algebra and parallel processing of texture maps. Bump mapping takes a gray scale texture map as input and interprets the pixel values as elevation data, computing light reflections from that. I'm immediately dissatisfied with the results as the transition from the day side of the planet to the night side causes stark discontinuities. So I ditch bump mapping in favor of normal mapping. It's the same idea but instead of storing elevation values in a gray scale texture map you use a three channel color map to store the three components of the surface normal. In essence you are storing the first derivative or slope instead of the absolute values. The results are much smoother.

Bump map and normal map of the same area in Europe
No bump mapping vs normal mapping in South America

I'm using Phong Shading, a relatively accurate light reflection model. However it can cause overly bright highlights which make the material seem like smooth plastic. To avoid this effect I employ specular texture maps. Generally these indicate the reflectivity of every pixel. For the earth a simple binary map is enough - bodies of water are highly reflective while landmasses are not. You can see a highlight on the ocean, lakes and rivers but not on land.

Next up is the dark side of the planet. When the sun goes down electric lights go on. I use a texture map of the major metropolitan areas on the planet and gradually blend this in on the dark side.

Lastly I overlay a layer of clouds that slowly circles the globe. Since on an average day about 70% of the planet are covered in clouds I have chosen an artificially clear day. This is supposed to be a game eventually after all and realism shouldn't hurt playability. Having the entire planet obscured by clouds is no fun.

It was about three o'clock in the afternoon in Switzerland when I took this screenshot.

To add one final touch to the appearance of the earth I add another shader pass to render an atmosphere. Atmospheric scattering is primarily caused by two effects: Rayleigh Scattering and Mie Scattering. Rayleigh Scattering is caused by small molecules in the air and scatters light of shorter wavelengths much more than longer ones. This is the reason the sky seems blue to an observer - the short wavelength blue light is scattered all over the place and reaches you from all sides. Mie Scattering is caused by larger dust or water particles and is responsible for rainbows or a halo around the sun. Sean O'Neil has published an excellent article in GPU Gems implementing a stochastic approximation of these effects on graphics hardware. It shoots a bunch of sampling rays through the atmosphere and derives a color value from that. The effect is quite dramatic and beautiful.

Sun peeking just around the corner, causing someone on earth to enjoy a beautiful dusk.

Of course the earth isn't alone in space so I use Google image search to find a nice photograph of the Milky Way and tape that on the inside of a sky sphere, producing the starry background you see in the screen shots. Everything until this point is rendered with a perspective camera, causing all lines to converge onto a distant vanishing point. For all intents and purposes the sun is infinitely far away and shouldn't scale based on the observer's position. If I put the sun into the 3D scene it would either have to be much too close or cause numerical instability because of the hugely different scales involved in rendering pixels on the earth's surface vs the sun. So instead I add another scene on top of the 3D scene. This one uses an orthographic projection, thus having no perspective distortion. I render a two dimensional sprite for the sun onto this one. I've also added some lens flares for flair. Lens flares are reflections within the lens of a camera if you point it more or less directly at a light source. For these to work I need to do some fancy math to figure out whether the light source is obscured or not. Generic implementations of lens flares typically resort to using a stochastic process based on occlusion maps calculated on the graphics hardware. Since my scene is rather simple I can get by with calculating the exact ray casting solution.

Finally I render yet another layer on top of the existing 3D and 2D cameras. This last layer is a regular HTML/CSS based website. I use jQuery UI on this to create a user interface with buttons and text as one is used to.

Of course all of this work is rather useless if I can't serve it to players (hopefully that'll include you! ;-)). I decided to use Google App Engine as my host. Google's cloud platform offers a ton of APIs, a free hosting tier and infinite scalability. I dislike Python and Java, which leaves Go as an App Engine supported language choice on the server side. My experience with it is minimal, but so far I'm impressed with its simplicity and elegance. People much more knowledgeable than me keep raving about its virtues, so it must be on to something.

App Engine uses Bigtable as its storage backend. This trades off the convenience a regular SQL database offers with its ACID guarantees for near infinite scalability, a weak query language and eventual consistency. Makes programming against it a little tricky since you are operating at a much lower level and have to be mindful of your transactions.

Anyway. Of course you don't have to take my word for any of this but can try the earth yourself. It requires decent hardware and a good browser (sorry, Internet Explorer doesn't count). That said, I've run it on a phone, a tablet and various laptops and PCs.

Here you go: Operation Survival - Globe Rendering Test.