Clik here to view.

In this article we're going to make an interactive WebGL globe. Instead of creating complex geometries from the geojson data to represent the countries, we'll use D3 to generate canvas maps that get converted to textures and applied to a sphere in THREE.js. I've found this is a pretty simple way to get the basic effect with minimal effort. With a few helper functions and some work to get things aligned properly we can make great interactive globes with a very small amount of code. For many common data visualizations like a world choropleth where you want to show country level data or maybe plot points between major world cities, this technique will work well and with some imagination can be used to make some stunning data visualizations.
The code presented here has hard dependencies on D3 and THREE.js and is written in ES6 modules. The final demo and the wireframe demo are loading in the browser using Babel and SystemJS, but you could take the same code and use it anywhere by just taking the modules that you need (ES6 modules make this so much easier!). There's just a few key functions that do the really hard part, so if you know D3 and THREE.js you should be able to extract out just the code you want.
In this article we'll get all the essential elements for creating a WebGL globe which highlights the countries and does some basic transitions. These examples also illustrate how to get data to other HTML elements on the page (the country names in the upper left) which can be tricky to get wired up. In a later tutorial I'll cover how to add more data driven features like displaying arcs between locations and applying a color scale to features on the map.
Demos for this article:
All the code for the demos below is in this Github repository....
Get the Code on Github
Image may be NSFW.
Clik here to view.

Final Interactive Globe
The final product. We'll look at this demo last. This demo has some basic events for mouseovers and clicks, but is fully customizable. With a little experimentation, you can create some really interesting tools for looking at geo-spatial data.
View the demo here
View the main module
Image may be NSFW.
Clik here to view.

Simple D3 Canvas Example
A baseline example of creating a canvas world map in D3. The code for this is just 30 lines long and serves as a good primer for talking about more the complicated examples.
View the demo here
Image may be NSFW.
Clik here to view.

Wrapping a Sphere with a Canvas Image
Building on the previous example, we add a few lines to the code to show how to wrap the canvas image around a sphere in a THREE.js scene. There's no interactivity on this version. We'll add that in the final demo.
View the demo here
Image may be NSFW.
Clik here to view.

Wireframe Example
Before looking at the final product, we'll look at this demo with a wireframe overlaid on top of it. Here you can see how increasing the number of segments on the sphere increases the precision of the mouse events, but at the cost of more processing.
View the demo here
A Basic Canvas Map in D3
The main idea here is that we draw canvas maps using D3 to hidden canvas elements and then create WebGL texture from them. This is a really powerful technique that is used quite frequently in game development. In the final demo what you see is a base map that gets drawn once with all the countries and then another map with just one country (and a different color) gets layered on top to create the highlight effect.
To begin, let's forget about WebGL and look at how you draw a basic canvas world map in D3. For this example, all the code is inside a script tag in the HTML. The only things we're loading before hand are topojson and D3. Here's the code in its entirety...
You can view the demo here
View the HTML source
var projection = d3.geo.equirectangular() //A .translate([512, 256]).scale(163); d3.json('data/world.json', function (err, data) { //B var countries = topojson.feature(data, data.objects.countries); //C var canvas = d3.select("body").append("canvas") .attr({width: "1024px", height: "512px"}); var context = canvas.node().getContext("2d"); var path = d3.geo.path() .projection(projection).context(context); //D context.strokeStyle = "#333"; context.lineWidth = 0.25; context.fillStyle = "#fff"; context.beginPath(); path(countries); //E context.fill(); context.stroke(); // This let's you inspect the resulting image in the console. console.log(canvas.node().toDataURL()); //F });
So that's all that's needed to draw a basic world map on a canvas in D3. A couple of lines could be removed without any impact on the result. At point B above we load some topojson which get's converted to regular geojson at point C. Topojson is like a zipped version of geojson that make it more efficient to move across a network. It's a good idea because geojson can get pretty hefty, but if you have regular geojson you could load that directly.
We're also console logging the url of the image so you can see it in the browser at point F. This is just for demo purposes, obviously. If you click on the link you can see that the map completely covers the canvas element with no white space on the edges. Also note that the canvas dimensions (1024 X 512) are powers of two. THREE.js is able to make some optimizations if you make your image dimensions all powers of two. If you don't do this, THREE.js will warn you in the console.
To make a map suitable for wrapping around a globe, at point A, we specify a equirectangular projection which is essentially no projection. In the final demo we are going to actually wrap our map in 3 dimensional space, so we don't any projection applied to it.
The one thing that is a little tricky here if you are unfamiliar with D3 is that the draw calls are being issued at point E above. We pass in a context at point D in the code and D3 will do the actual drawing of the shapes when we call path. So lot of the "action" is happening in that one line which is easy to overlook (I think).
Wrapping the Canvas Image Around a Sphere
The next step is to get an image of our canvas map and use it as a texture in THREE.js. Building on the code above, we add the following lines to get the canvas image onto our sphere...
View this demo here
View the HTML source
//################################## // Boiler Plate Scene Setup //################################## var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 6000); camera.position.z = 500; var renderer = new THREE.WebGLRenderer({antialias: true}); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var light = new THREE.HemisphereLight('#fff', '#666', 1.5); light.position.set(0, 500, 0); scene.add(light); //############################### // Base Sphere //############################### var waterMaterial = new THREE.MeshBasicMaterial({color: '#555', transparent: true}); var sphere = new THREE.SphereGeometry(200, 100, 100); var baseLayer = new THREE.Mesh(sphere, waterMaterial); // A //################################ // Create Texture from Canvas //################################ var mapTexture = new THREE.Texture(canvas.node()); //B mapTexture.needsUpdate = true; var mapMaterial = new THREE.MeshBasicMaterial({map: mapTexture, transparent: true}); //C var mapLayer = new THREE.Mesh(sphere, mapMaterial); //D var root = new THREE.Object3D(); root.add(baseLayer); root.add(mapLayer); scene.add(root); function render() { root.rotation.y += 0.02; requestAnimationFrame(render); renderer.render(scene, camera); } render();
We are creating a basic THREE.js scene with just one grey sphere in it. In this example, the canvas node D3 is drawing to is set to display none, so it can't be seen. The hidden canvas map is used to create a texture at point B above. The canvas textures are transparent so the grey material of the globe shows through as the "water" features. At point C we use that texture in create a material and then make the mapLayer mesh at point D.
The code for drawing the actual map has now moved to a module. Now we call a function with the geojson we want to draw and a color for the fill. You can send one or many features and it will draw them using the same projections so they can be layered on top of each other. So the "mapTexture" function now looks like this...
import THREE from 'THREE'; import d3 from 'd3'; var projection = d3.geo.equirectangular() .translate([1024, 512]) .scale(325); export function mapTexture(geojson, color) { var texture, context, canvas; canvas = d3.select("body").append("canvas") .style("display", "none") .attr("width", "2048px") .attr("height", "1024px"); context = canvas.node().getContext("2d"); var path = d3.geo.path() .projection(projection) .context(context); context.strokeStyle = "#333"; context.lineWidth = 1; context.fillStyle = color || "#CDB380"; context.beginPath(); path(geojson); if (color) { context.fill(); } context.stroke(); // DEBUGGING - Really expensive, disable when done. // console.log(canvas.node().toDataURL()); texture = new THREE.Texture(canvas.node()); texture.needsUpdate = true; canvas.remove(); return texture; }
That's right, it creates a canvas element and destroys it each time. I played around with this a little, this seems to be much faster than re-using the canvas element. It's incredibly fast.
Making the Globe Interactive
In order to make our globe interactive we need to be able to know where the mouse is on our map when it moves and when there is a click. We can do this by using ray casting. You can check out this short video from the makers of the Unity game engine for the basics on this idea. To work with the code here, you really don't need to get too in-depth on this because THREE.js is doing a lot of the heavy lifting.
Let's look at the wireframe example...
View the wireframe example here
In our case, we only ray cast a single object, our main sphere object. When there is a click or the mouse moves THREE.js is going to determine if the event is on our globe and, if so, give use back the exact face on the sphere that was hit. With the map properly aligned we can easily convert from the location on the sphere and a latitude and longitude on our map.
Looking at the wireframe demo. By creating a second sphere with the exact same number of segments and turning its material's wireframe attribute to true, we can see the individual vertices and triangular faces of the sphere. This represents the "resolution" we have for mouse events on the sphere. More segments means a tighter grid and better accuracy for mouse events. In the demo code, I calculate the mid-point of the face and use that as the point to look up. This is the red dot on the wireframe demo. If you play around a little you can see how it is really moving on a fixed grid and jumps from location to location. You can also see how some smaller entities can be hard to click because of the way the vertices are laid out in the sphere. Making small adjustments (up or down) to the number of segments can resolve those kinds of problems.
Image may be NSFW.Clik here to view.
So the trick here is bumping up the number of segments on the sphere to give better precision for mouse hits. The catch is that if you turn the number of segments up absurdly high it will not be performant. The ray casting is the most computationally expensive part of the code and even dwarfs the cost of generating the maps. You have to think about how much resolution you really need when doing this.
If you only need to be able to click the major countries with fairly good accuracy then you could turn down the number of segments significantly. In the demo I left the number of segments roughly in the middle. You can see that it performs pretty well. There are a few small countries that are hard to click, but in general it's pretty good.
Looking Up the Country From the Event
One last piece of the code that is worth looking at is how to see if a point falls in a particular country geometry. It's import for this operation to be fast. The key is to load up geojson that only has the minimal amount of detail. In the demo code I have pretty good detail on the country geometries and the lookup is blazingly fast. There is, of course, a number of ways to skin this cat, and the optimal solution is going to depend on your application's specifics. Here I present a good general solution that is easy to implement and has good performance.
Here's how it works in the demo. Towards the top of the main.js file you'll see we wrap up the geojson and get back an object with a find and search function...
import { geodecoder } from './common/geoHelpers'; import { mapTexture } from './common/mapTexture'; import { memoize } from './common/utils'; // from main.js... var geo = geodecoder(countries.features); //A var textureCache = memoize(function (cntryID, color) { var country = geo.find(cntryID); //B return mapTexture(country, color); });
At point A above we create a "geodecoder" object (best name I could think of) which then gets used in the memoized "textureCache" at point B and in the event handler for mouse moves. If you look at the geoHelpers.js file you'll see that we get back an object that has the feature data indexed for quick lookup and search...
export var geodecoder = function (features) { let store = {}; for (let i = 0; i < features.length; i++) { store[features[i].id] = features[i]; } return { find: function (id) { return store[id]; }, search: function (lat, lng) { let match = false; let country, coords; for (let i = 0; i < features.length; i++) { country = features[i]; if(country.geometry.type === 'Polygon') { match = pointInPolygon(country.geometry.coordinates[0], [lng, lat]); if (match) { return { code: features[i].id, name: features[i].properties.name }; } } else if (country.geometry.type === 'MultiPolygon') { coords = country.geometry.coordinates; for (let j = 0; j < coords.length; j++) { match = pointInPolygon(coords[j][0], [lng, lat]); if (match) { return { code: features[i].id, name: features[i].properties.name }; } } } } return null; } }; };
So now we can get the geojson for a particular country or search to see if a point lies within any country's boundaries. The search makes uses of this function that determines if the point is inside the polygon....
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html var pointInPolygon = function(poly, point) { let x = point[0]; let y = point[1]; let inside = false, xi, xj, yi, yj, xk; for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { xi = poly[i][0]; yi = poly[i][1]; xj = poly[j][0]; yj = poly[j][1]; xk = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (xk) { inside = !inside; } } return inside; };
Wrapping Up
There's some odd and ends in the code that did not get covered here explicitly, but I think I covered the key points. Take a look at the repo on Github to see how all the pieces work together. As always, comments and suggestions are welcome below. Enjoy!