├── .gitignore
├── LICENSE
├── README.md
├── example-iss.html
├── example.html
├── globe.js
└── img
├── bump.jpg
├── specular.jpg
└── world.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Mike van Rossum
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Realtime Webgl Globe
2 |
3 | A webgl earth making it easy to add custom shapes at coordinates in realtime.
4 |
5 | 
6 |
7 | [Demo](https://mikevanrossum.nl/stuff/realtime-webgl-globe/example.html)!
8 |
9 | Or check out [this demo](https://github.com/askmike/realtime-webgl-globe/blob/master/example-iss.html) that shows the position of the International Space Station!
10 |
11 | ## Features
12 |
13 | - Makes it easy to add custom shapes to the globe at lat/long positions.
14 | - Interactive (mousewheel scroll & mouse drags).
15 | - Easy API for adding elements on the globe while it's running.
16 |
17 | ## Basic Usage
18 |
19 | var div = document.getElementById('globe');
20 | var urls = {
21 | earth: 'img/world.jpg',
22 | bump: 'img/bump.jpg',
23 | specular: 'img/specular.jpg',
24 | }
25 |
26 | // create a globe
27 | var globe = new Globe(div, urls);
28 |
29 | // start it
30 | globe.init();
31 |
32 | // random data
33 | var data = {
34 | color: '#FF0000',
35 | size: 20,
36 | lat: 52.3747158, // Amsterdam!
37 | lon: 4.8986231, // Amsterdam!
38 | size: 20
39 | };
40 |
41 | // add a block on Amsterdam
42 | globe.addBlock(data);
43 |
44 | ## API
45 |
46 | * * *
47 |
48 | ## Class: Globe
49 | Realtime Globe is a WebGL based earth globe that
50 | makes it super simple to add shapes in realtime
51 | on specific lat/lon positions on earth.
52 |
53 | ### Globe.init()
54 |
55 | Initializes the globe
56 |
57 |
58 |
59 | ### Globe.zoomRelative(delta)
60 |
61 | Zoom the earth relatively to its current zoom
62 | (passing a positive number will zoom towards
63 | the earth, while a negative number will zoom
64 | away from earth).
65 |
66 | **Parameters**
67 |
68 | - **delta**: `Integer`
69 |
70 | **Returns**: `this`
71 |
72 | ### Globe.zoomTo(altitute)
73 |
74 | Transition the altitute of the camera to a
75 | specific distance from the earth's core.
76 |
77 | **Parameters**
78 |
79 | - **altitute**: `Integer`
80 |
81 | **Returns**: `this`
82 |
83 | ### Globe.zoomImmediatelyTo(altitude)
84 |
85 | Set the altitute of the camera to a specific
86 | distance from the earth's core.
87 |
88 | **Parameters**
89 |
90 | - **altitude**: `Integer`
91 |
92 | **Returns**: `this`
93 |
94 | ### Globe.center(pos)
95 |
96 | Transition the globe from its current position
97 | to the new coordinates.
98 |
99 | **Parameters**
100 |
101 | - **pos**: `Object`, the position
102 | - **pos.lat**: `Float`, latitute position
103 | - **pos.lon**: `Float`, longtitute position
104 |
105 | **Returns**: `this`
106 |
107 | ### Globe.centerImmediate(pos)
108 |
109 | Center the globe on the new coordinates.
110 |
111 | **Parameters**
112 |
113 | - **pos**: `Object`, the position
114 | - **pos.lat**: `Float`, latitute position
115 | - **pos.lon**: `Float`, longtitute position
116 |
117 | **Returns**: `this`
118 |
119 | ### Globe.addLevitatingBlock(data)
120 |
121 | Adds a block to the globe. The globe will spawn
122 | just below the earth's surface and `levitate`
123 | out of the surface until it is fully `out` of the
124 | earth.
125 |
126 | **Parameters**
127 |
128 | - **data**: `Object`
129 | - **data.lat**: `Float`, latitute position
130 | - **data.lon**: `Float`, longtitute position
131 | - **data.size**: `Float`, size of the block
132 | - **data.color**: `String`, color of the block
133 |
134 | **Returns**: `this`
135 |
136 | ### Globe.addBlock(data)
137 |
138 | Adds a block to the globe.
139 |
140 | **Parameters**
141 |
142 | - **data**: `Object`
143 | - **data.lat**: `Float`, latitute position
144 | - **data.lon**: `Float`, longtitute position
145 | - **data.size**: `Float`, size of the block
146 | - **data.color**: `String`, color of the block
147 |
148 | **Returns**: `this`
149 |
150 | ### Globe.removeAllBlocks()
151 |
152 | Remove all blocks from the globe.
153 |
154 |
155 | **Returns**: `this`
156 |
157 |
158 |
159 | * * *
160 |
--------------------------------------------------------------------------------
/example-iss.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Realtime WebGL Globe Example
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Realtime WebGL Globe Example
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
71 |
72 |
--------------------------------------------------------------------------------
/globe.js:
--------------------------------------------------------------------------------
1 | // Realtime WebGL globe
2 | // Copyright (c) 2015 Mike van Rossum
3 |
4 | /**
5 | * Realtime Globe is a WebGL based earth globe that
6 | * makes it super simple to add shapes in realtime
7 | * on specific lat/lon positions on earth.
8 | *
9 | * @class Globe
10 | * @param {HTMLElement} container
11 | * @param {Object} urls - URLs of earth images
12 | * @param {String} urls.earth
13 | * @param {String|undefined} urls.bump (optional)
14 | * @param {String|undefined} urls.specular (optional)
15 | */
16 | var Globe = function Globe(container, urls) {
17 | var PI = Math.PI;
18 | var PI_HALF = PI / 2;
19 |
20 | // Three.js objects
21 | var camera;
22 | var scene;
23 | var light;
24 | var renderer;
25 |
26 | var earthGeometry;
27 | var earthPosition;
28 |
29 | // camera's distance from center (and thus the globe)
30 | var distanceTarget = 900;
31 | var distance = distanceTarget;
32 |
33 | // camera's position
34 | var rotation = { x: 2, y: 1 };
35 | var target = { x: 2, y: 1 };
36 |
37 | // holds currently levitating blocks
38 | var levitatingBlocks = [];
39 | // holds all block references
40 | var blocks = [];
41 |
42 | // What gets exposed by calling:
43 | //
44 | // var globe = [new] Globe(div, urls);
45 | //
46 | // attach public functions to this object
47 | var api = {};
48 |
49 | /**
50 | * Initializes the globe
51 | *
52 | */
53 | api.init = function() {
54 | setSize();
55 |
56 | // Camera
57 | camera = new THREE.PerspectiveCamera(30, w / h, 1, 1000);
58 | camera.position.z = distance;
59 |
60 | // Scene
61 | scene = new THREE.Scene();
62 |
63 | // Earth geom, used for earth & atmosphere
64 | earthGeometry = new THREE.SphereGeometry(200, 64, 64);
65 |
66 | // Light, reposition close to camera
67 | light = createMesh.directionalLight();
68 |
69 | // we use this to correctly position camera and blocks
70 | var earth = createMesh.earth(urls);
71 | earthPosition = earth.position;
72 |
73 | // Add meshes to scene
74 | scene.add(earth);
75 | scene.add(createMesh.atmosphere());
76 |
77 | // Add lights to scene
78 | scene.add(new THREE.AmbientLight(0x656565));
79 | scene.add(light);
80 |
81 | // Renderer
82 | renderer = new THREE.WebGLRenderer({antialias: true});
83 | renderer.setSize(w, h);
84 |
85 | // Add scene to DOM
86 | renderer.domElement.style.position = 'absolute';
87 | container.appendChild(renderer.domElement);
88 |
89 | // DOM event handlers
90 | container.addEventListener('mousedown', handle.drag.start, false);
91 | window.addEventListener('resize', handle.resize, false);
92 |
93 | // Scroll for Chrome
94 | window.addEventListener('mousewheel', handle.scroll, false);
95 | // Scroll for Firefox
96 | window.addEventListener('DOMMouseScroll', handle.scroll, false);
97 |
98 | // Bootstrap render
99 | animate();
100 |
101 | return this;
102 | }
103 |
104 | var setSize = function() {
105 | w = container.offsetWidth || window.innerWidth;
106 | h = container.offsetHeight || window.innerHeight;
107 | }
108 |
109 | var createMesh = {
110 |
111 | // @param urls Object URLs of images
112 | //
113 | // {
114 | // earth: String URL
115 | // bump: Sting URL [optional]
116 | // specular: String URL [optional]
117 | // }
118 | //
119 | // See
120 | // @link http://learningthreejs.com/blog/2013/09/16/how-to-make-the-earth-in-webgl/
121 | // @link http://learningthreejs.com/data/2013-09-16-how-to-make-the-earth-in-webgl/demo/index.html
122 | earth: function(urls) {
123 | if(!urls.earth)
124 | throw 'No image URL provided for an earth image';
125 |
126 | var material = new THREE.MeshPhongMaterial();
127 | material.map = THREE.ImageUtils.loadTexture(urls.earth);
128 |
129 | if(urls.bump) {
130 | material.bump = THREE.ImageUtils.loadTexture(urls.bump);
131 | material.bumpScale = 0.02;
132 | }
133 |
134 | if(urls.specular) {
135 | material.specularMap = THREE.ImageUtils.loadTexture(urls.specular);
136 | material.specular = new THREE.Color('grey');
137 | }
138 |
139 | return new THREE.Mesh(earthGeometry, material);
140 | },
141 |
142 | // See
143 | // @link https://github.com/dataarts/webgl-globe/blob/master/globe/globe.js#L52
144 | // @link http://bkcore.com/blog/3d/webgl-three-js-animated-selective-glow.html
145 | //
146 | // Currently has some issues, especially when zooming out (distance > 900)
147 | atmosphere: function() {
148 | var material = new THREE.ShaderMaterial({
149 | vertexShader: [
150 | 'varying vec3 vNormal;',
151 | 'void main() {',
152 | 'vNormal = normalize( normalMatrix * normal );',
153 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
154 | '}'
155 | ].join('\n'),
156 | fragmentShader: [
157 | 'varying vec3 vNormal;',
158 | 'void main() {',
159 | 'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 7.0 );',
160 | 'gl_FragColor = vec4( 0.7, 1.0, 0.7, 1.0 ) * intensity;',
161 | '}'
162 | ].join('\n'),
163 | side: THREE.BackSide,
164 | blending: THREE.AdditiveBlending,
165 | transparent: false
166 | });
167 |
168 | var mesh = new THREE.Mesh(earthGeometry, material);
169 | mesh.scale.set(1.1, 1.1, 1.1);
170 | return mesh;
171 | },
172 |
173 | directionalLight: function() {
174 | return new THREE.DirectionalLight(0xcccccc, 0.5);
175 | },
176 |
177 |
178 | block: function(color) {
179 | return new THREE.Mesh(
180 | new THREE.BoxGeometry(1, 1, 1),
181 | new THREE.MeshLambertMaterial({color: color})
182 | );
183 | }
184 |
185 | }
186 |
187 | // Keep track of mouse positions
188 | var mouse = { x: 0, y: 0 };
189 | var mouseOnDown = { x: 0, y: 0 };
190 | var targetOnDown = { x: 0, y: 0 };
191 |
192 | // DOM event handlers
193 | var handle = {
194 | scroll: function(e) {
195 | e.preventDefault();
196 |
197 | // See
198 | // @link http://www.h3xed.com/programming/javascript-mouse-scroll-wheel-events-in-firefox-and-chrome
199 | if(e.wheelDelta) {
200 | // chrome
201 | var delta = e.wheelDelta * 0.5;
202 | } else {
203 | // firefox
204 | var delta = -e.detail * 15;
205 | }
206 |
207 | api.zoomRelative(delta);
208 |
209 | return false;
210 | },
211 |
212 | resize: function(e) {
213 | setSize();
214 | camera.aspect = w / h;
215 | camera.updateProjectionMatrix();
216 | renderer.setSize(w, h);
217 | },
218 |
219 | // See
220 | // @link https://github.com/dataarts/webgl-globe/blob/master/globe/globe.js#L273-L334
221 | drag: {
222 | start: function(e) {
223 | e.preventDefault();
224 | container.addEventListener('mousemove', handle.drag.move, false);
225 | container.addEventListener('mouseup', handle.drag.end, false);
226 | container.addEventListener('mouseout', handle.drag.end, false);
227 |
228 | mouseOnDown.x = -e.clientX;
229 | mouseOnDown.y = e.clientY;
230 |
231 | targetOnDown.x = target.x;
232 | targetOnDown.y = target.y;
233 |
234 | container.style.cursor = 'move';
235 | },
236 | move: function(e) {
237 | mouse.x = -e.clientX;
238 | mouse.y = e.clientY;
239 |
240 | var zoomDamp = distance / 1000;
241 |
242 | target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
243 | target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
244 |
245 | target.y = target.y > PI_HALF ? PI_HALF : target.y;
246 | target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
247 | },
248 | end: function(e) {
249 | container.removeEventListener('mousemove', handle.drag.move, false);
250 | container.removeEventListener('mouseup', handle.drag.end, false);
251 | container.removeEventListener('mouseout', handle.drag.end, false);
252 | container.style.cursor = 'auto';
253 | }
254 | }
255 | }
256 |
257 | var checkAltituteBoundries = function() {
258 | // max zoom
259 | if(distanceTarget < 300)
260 | distanceTarget = 300;
261 |
262 | // min zoom
263 | else if(distanceTarget > 900)
264 | distanceTarget = 900;
265 | }
266 |
267 | var animate = function() {
268 | requestAnimationFrame(animate);
269 | render();
270 | }
271 |
272 | var render = function() {
273 | levitateBlocks();
274 |
275 | // Rotate towards the target
276 | rotation.x += (target.x - rotation.x) * 0.1;
277 | rotation.y += (target.y - rotation.y) * 0.1;
278 | distance += (distanceTarget - distance) * 0.3;
279 |
280 | // determine camera position
281 | set3dPosition(camera, {
282 | x: rotation.x,
283 | y: rotation.y,
284 | altitude: distance
285 | });
286 |
287 | // Determine light position based
288 | set3dPosition(light, {
289 | x: rotation.x - 150,
290 | y: rotation.y - 150,
291 | altitude: distance
292 | });
293 |
294 | camera.lookAt(earthPosition);
295 | renderer.render(scene, camera);
296 | }
297 |
298 | // @param Object position (2d lat/lon coordinates)
299 | // @return Object coords (x/y coordinates)
300 | //
301 | // Calculates x, y coordinates based on
302 | // lat/lon coordinates.
303 | var calculate2dPosition = function(coords) {
304 | var phi = (90 + coords.lon) * PI / 180;
305 | var theta = (180 - coords.lat) * PI / 180;
306 |
307 | return {
308 | x: phi,
309 | y: PI - theta
310 | }
311 | }
312 |
313 | // @param Mesh object
314 | // @param Object coords (x/y coordinates in 2d space + altitute)
315 | //
316 | // Calculates 3d position and sets it on mesh
317 | var set3dPosition = function(mesh, coords) {
318 | if(!coords)
319 | coords = mesh.userData;
320 |
321 | var x = coords.x;
322 | var y = coords.y;
323 | var altitude = coords.altitude;
324 |
325 | mesh.position.set(
326 | altitude * Math.sin(x) * Math.cos(y),
327 | altitude * Math.sin(y),
328 | altitude * Math.cos(x) * Math.cos(y)
329 | );
330 | }
331 |
332 | // Create a block mesh and set its position in 3d
333 | // space just below the earths surface
334 | var createLevitatingBlock = function(properties) {
335 | // create mesh
336 | var block = createMesh.block(properties.color);
337 |
338 | // calculate 2d position
339 | var pos2d = calculate2dPosition(properties);
340 |
341 | block.userData = {
342 | // set 2d position on earth so we can more
343 | // easily recalculate the 3d position
344 |
345 | x: pos2d.x,
346 | y: pos2d.y,
347 |
348 |
349 | altitude: 200 - properties.size / 1.5,
350 | // speed at which block levitates outside
351 | // earth's core
352 | levitation: .1,
353 |
354 | size: properties.size
355 | }
356 |
357 | // calculate 3d position
358 | set3dPosition(block);
359 |
360 | // rotate towards earth
361 | block.lookAt(earthPosition);
362 |
363 | block.scale.z = properties.size;
364 | block.scale.x = properties.size;
365 | block.scale.y = properties.size;
366 |
367 | block.updateMatrix();
368 |
369 | return block;
370 | }
371 |
372 | // Create a block mesh and set its position in 3d
373 | // space just below the earths surface
374 | var createBlock = function(properties) {
375 | // create mesh
376 | var block = createMesh.block(properties.color);
377 |
378 | // calculate 2d position
379 | var pos2d = calculate2dPosition(properties);
380 |
381 | // add altitute
382 | pos2d.altitude = 200 + properties.size / 2;
383 |
384 | // calculate 3d position
385 | set3dPosition(block, pos2d);
386 |
387 | // rotate towards earth
388 | block.lookAt(earthPosition);
389 |
390 | block.scale.z = properties.size;
391 | block.scale.x = properties.size;
392 | block.scale.y = properties.size;
393 |
394 | block.updateMatrix();
395 |
396 | return block;
397 | }
398 |
399 | // internal function to levitate all levitating
400 | // blocks each tick. Called on render.
401 | var levitateBlocks = function() {
402 | levitatingBlocks.forEach(function(block, i) {
403 |
404 | var userData = block.userData;
405 |
406 | // if entirely outide of earth, stop levitating
407 | if(userData.altitude > 200 + userData.size / 2) {
408 | levitatingBlocks.splice(i, 1);
409 | return;
410 | }
411 |
412 | userData.altitude += userData.levitation;
413 | set3dPosition(block);
414 | block.updateMatrix();
415 | });
416 | }
417 |
418 | // Public functions
419 |
420 | /**
421 | * Zoom the earth relatively to its current zoom
422 | * (passing a positive number will zoom towards
423 | * the earth, while a negative number will zoom
424 | * away from earth).
425 | *
426 | * @param {Integer} delta
427 | * @return {this}
428 | */
429 | api.zoomRelative = function(delta) {
430 | distanceTarget -= delta;
431 | checkAltituteBoundries();
432 |
433 | return this;
434 | }
435 |
436 | /**
437 | * Transition the altitute of the camera to a
438 | * specific distance from the earth's core.
439 | *
440 | * @param {Integer} altitute
441 | * @return {this}
442 | */
443 | api.zoomTo = function(altitute) {
444 | distanceTarget = altitute;
445 | checkAltituteBoundries();
446 |
447 | return this;
448 | }
449 |
450 | /**
451 | * Set the altitute of the camera to a specific
452 | * distance from the earth's core.
453 | *
454 | * @param {Integer} altitude
455 | * @return {this}
456 | */
457 | api.zoomImmediatelyTo = function(altitute) {
458 | distanceTarget = distance = altitute;
459 | checkAltituteBoundries();
460 |
461 | return this;
462 | }
463 |
464 | /**
465 | * Transition the globe from its current position
466 | * to the new coordinates.
467 | *
468 | * @param {Object} pos - the position
469 | * @param {Float} pos.lat - latitute position
470 | * @param {Float} pos.lon - longtitute position
471 | * @return {this}
472 | */
473 | api.center = function(pos) {
474 | target = calculate2dPosition(pos);
475 | return this;
476 | }
477 |
478 | /**
479 | * Center the globe on the new coordinates.
480 | *
481 | * @param {Object} pos - the position
482 | * @param {Float} pos.lat - latitute position
483 | * @param {Float} pos.lon - longtitute position
484 | * @return {this}
485 | */
486 | api.centerImmediate = function(pos) {
487 | target = rotation = calculate2dPosition(pos);
488 | return this;
489 | }
490 |
491 | /**
492 | * Adds a block to the globe. The globe will spawn
493 | * just below the earth's surface and `levitate`
494 | * out of the surface until it is fully `out` of the
495 | * earth.
496 | *
497 | * @param {Object} data
498 | * @param {Float} data.lat - latitute position
499 | * @param {Float} data.lon - longtitute position
500 | * @param {Float} data.size - size of the block
501 | * @param {String} data.color - color of the block
502 | * @return {this}
503 | */
504 | api.addLevitatingBlock = function(data) {
505 | var block = createLevitatingBlock(data);
506 |
507 | scene.add(block);
508 | levitatingBlocks.push(block);
509 | blocks.push(block);
510 |
511 | return this;
512 | }
513 |
514 | /**
515 | * Adds a block to the globe.
516 | *
517 | * @param {Object} data
518 | * @param {Float} data.lat - latitute position
519 | * @param {Float} data.lon - longtitute position
520 | * @param {Float} data.size - size of the block
521 | * @param {String} data.color - color of the block
522 | * @return {this}
523 | */
524 | api.addBlock = function(data) {
525 | var block = createBlock(data);
526 |
527 | scene.add(block);
528 | blocks.push(block);
529 |
530 | return this;
531 | }
532 | /**
533 | * Remove all blocks from the globe.
534 | *
535 | * @return {this}
536 | */
537 | api.removeAllBlocks = function() {
538 | blocks.forEach(function(block) {
539 | scene.remove(block);
540 | });
541 |
542 | blocks = [];
543 |
544 | return this;
545 | }
546 |
547 |
548 | return api;
549 | }
--------------------------------------------------------------------------------
/img/bump.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/bump.jpg
--------------------------------------------------------------------------------
/img/specular.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/specular.jpg
--------------------------------------------------------------------------------
/img/world.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/askmike/realtime-webgl-globe/e215750848b94063ef257121ef5d18432d1249c5/img/world.jpg
--------------------------------------------------------------------------------