├── .gitignore ├── LICENSE ├── README.md ├── app ├── assets │ ├── img │ │ ├── favicon.png │ │ ├── github.svg │ │ ├── globe.svg │ │ ├── info-circle.svg │ │ ├── loading.gif │ │ └── thumb.jpg │ └── index.html ├── initialize.js ├── js │ ├── bordersLayer.js │ ├── camera.js │ ├── components │ │ ├── checkboxOption.js │ │ ├── projections.js │ │ ├── rangeSlider.js │ │ └── renderModes.js │ ├── controlRange.js │ ├── controller.js │ ├── coordinates.js │ ├── landMaskLayer.js │ ├── layerCanvas.js │ ├── octahedronSphere.js │ ├── renderer.js │ ├── scene.js │ ├── shaders.js │ ├── utils.js │ └── vectorArray.js ├── shaders │ ├── day.frag.glsl │ ├── dayAndNight.frag.glsl │ ├── dayAndNightSimple.frag.glsl │ ├── elevation.frag.glsl │ ├── functions │ │ ├── atmosphere.glsl │ │ ├── brdf.glsl │ │ ├── diffuseLambert.glsl │ │ ├── exposure.glsl │ │ ├── gamma.glsl │ │ ├── heightDerivative.glsl │ │ ├── inverse.glsl │ │ ├── isNan.glsl │ │ ├── nightAmbient.glsl │ │ ├── perturbNormal.glsl │ │ ├── terrainBumpScale.glsl │ │ ├── texture2DCubic.glsl │ │ ├── tonemap.glsl │ │ └── transpose.glsl │ ├── night.frag.glsl │ ├── plane.vert.glsl │ └── sphere.vert.glsl └── styles │ ├── _map.scss │ ├── _range.scss │ └── styles.scss ├── demo.jpg ├── package.json ├── script ├── make-vectors.sh └── process-images.sh ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | .cache 16 | .project 17 | .settings 18 | .tmproj 19 | nbproject 20 | Thumbs.db 21 | .exrc 22 | 23 | # NPM packages folder 24 | node_modules/ 25 | 26 | # Brunch output folder 27 | public/ 28 | 29 | # Temporary data folder 30 | tmpdata/ 31 | 32 | # Generated files 33 | app/assets/data/ 34 | dist/ 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Globe Viewer 2 | [![Globe Viewer Example](/demo.jpg?raw=true)](http://visualperspective.github.io/globe-viewer/index.html) 3 | 4 | Renders the globe in different ways using WebGL ([Live Demo](http://visualperspective.github.io/globe-viewer/index.html)). Uses fixed data sources for now, though this is meant as a base for visualizations that add other data sets. 5 | 6 | ## Installing and Running 7 | 8 | Requires: 9 | 10 | * a recent version of nodejs and yarn 11 | * topojson (yarn global add topojson) 12 | * ImageMagick 13 | 14 | To install, clone the repo and download and prepare the data (this may take a few minutes): 15 | ``` 16 | yarn install 17 | yarn make-vectors 18 | yarn process-images 19 | ``` 20 | 21 | To run: 22 | ``` 23 | yarn start 24 | ``` 25 | 26 | The viewer should now be available at `http://localhost:3333` 27 | 28 | ## Data Sources 29 | 30 | ##### Land color (app/assets/data/color-\*) 31 | Natural Earth Cross Blended Hypsometry 32 | http://www.naturalearthdata.com/downloads/10m-cross-blend-hypso/cross-blended-hypso/ 33 | 34 | ##### Topography and bathymetry (app/assets/data/topo-bathy-\*) 35 | Blue Marble 36 | http://visibleearth.nasa.gov/view_cat.php?categoryID=1484 37 | 38 | ##### Night Sky Lights (app/assets/data/lights-\*) 39 | Blue Marble 40 | http://visibleearth.nasa.gov/view_cat.php?categoryID=1484 41 | 42 | ##### Land, Ocean, Rivers, and Borders (app/assets/data/vectors.json) 43 | Natural Earth Physical Vectors 44 | http://www.naturalearthdata.com/downloads/50m-physical-vectors/ 45 | 46 | These physical vectors can be downloaded and processed 47 | using ./scripts/make-vectors.sh The resulting json is then 48 | drawn to an offscreen 49 | canvas in the browser using vectorLayer.js 50 | 51 | ## Other Globe Visualizations / Platforms on the Web 52 | 53 | https://github.com/dataarts/webgl-globe 54 | 55 | https://cesiumjs.org/ 56 | 57 | https://earth.nullschool.net/ 58 | 59 | http://hint.fm/wind/ 60 | 61 | http://alteredqualia.com/xg/examples/earth_bathymetry.html 62 | 63 | http://alteredqualia.com/xg/examples/earth_seasons.html 64 | -------------------------------------------------------------------------------- /app/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualPerspective/globe-viewer/8309d718037aec8fbaba47bce9d426d130db415a/app/assets/img/favicon.png -------------------------------------------------------------------------------- /app/assets/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/assets/img/info-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualPerspective/globe-viewer/8309d718037aec8fbaba47bce9d426d130db415a/app/assets/img/loading.gif -------------------------------------------------------------------------------- /app/assets/img/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualPerspective/globe-viewer/8309d718037aec8fbaba47bce9d426d130db415a/app/assets/img/thumb.jpg -------------------------------------------------------------------------------- /app/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Globe Viewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |

Globe Viewer

23 | 31 |
32 |
33 |
34 | 37 | 38 | 41 | 42 | 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |
54 | 57 | 58 | 61 | 62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /app/initialize.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import * as twgl from 'twgl.js/dist/4.x/twgl-full' 3 | import styles from 'styles/styles.scss' 4 | import Controller from 'js/controller' 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | let gl = twgl.getWebGLContext( 8 | document.querySelector(".map-canvas canvas") 9 | ) 10 | 11 | d3.json('data/vectors.json').then(vectors => { 12 | new Controller(gl, vectors) 13 | }) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /app/js/bordersLayer.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import * as topojson from 'topojson' 3 | import _ from 'lodash' 4 | 5 | import { 6 | compositeOperation, 7 | dispatchEvent 8 | } from './utils' 9 | 10 | export default class BordersLayer { 11 | constructor(gl, vectors, layerCanvas) { 12 | this.options = { countries: { enabled: false } } 13 | this.layerCanvas = layerCanvas 14 | this.countries = topojson.feature(vectors, vectors.objects.countries) 15 | this.draw() 16 | } 17 | 18 | draw() { 19 | let ctx = this.layerCanvas.ctx 20 | ctx.fillStyle = '#000' 21 | ctx.fillRect(0, 0, this.layerCanvas.width, this.layerCanvas.height) 22 | 23 | // countries 24 | if (this.options.countries.enabled) { 25 | ctx.beginPath() 26 | this.layerCanvas.path(this.countries) 27 | ctx.lineWidth = 1.0 * this.layerCanvas.scale 28 | ctx.strokeStyle = '#fff' 29 | ctx.stroke() 30 | } 31 | 32 | dispatchEvent('borders-updated') 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/js/camera.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import hammer from 'hammerjs' 3 | import * as twgl from 'twgl.js/dist/4.x/twgl-full' 4 | 5 | import ControlRange from './controlRange' 6 | import { toRadians } from './utils' 7 | 8 | const m4 = twgl.m4 9 | 10 | export default class Camera { 11 | constructor(gl) { 12 | this.gl = gl 13 | this.fov = 50 14 | 15 | this.longitude = new ControlRange(0, -180, 180, true) 16 | this.latitude = new ControlRange(0, -90, 90) 17 | this.zoom = new ControlRange(0.5, 0, 1) 18 | 19 | this.dragging = false 20 | this.dragStart = undefined 21 | this.mousePosition = undefined 22 | 23 | this.hammer = new Hammer(gl.canvas) 24 | this.hammer.get('pinch').set({ enable: true }) 25 | 26 | document.addEventListener('mousemove', (e) => { 27 | this.handleMouseMove(e) 28 | }) 29 | 30 | gl.canvas.addEventListener('mousedown', () => { 31 | this.dragging = true 32 | }) 33 | 34 | document.addEventListener('selectstart', (e) => { 35 | if (this.dragging === true) { 36 | e.preventDefault() 37 | } 38 | }) 39 | 40 | document.addEventListener('mouseup', () => { 41 | this.dragging = false 42 | }) 43 | 44 | gl.canvas.addEventListener('scroll', () => { 45 | return false 46 | }) 47 | 48 | this.hammer.on('pan', (e) => { 49 | if (e.pointerType != 'mouse') { 50 | this.handlePan(e.velocityX * 8, e.velocityY * 8) 51 | } 52 | 53 | return false 54 | }) 55 | 56 | this.hammer.on('pinchstart', (e) => { 57 | this.lastZoom = this.zoom.value 58 | this.pinching = true 59 | }) 60 | 61 | this.hammer.on('pinchend', (e) => { 62 | this.pinching = false 63 | }) 64 | 65 | this.hammer.on('pinch', (e) => { 66 | if (this.pinching) { 67 | this.zoom.changeTo(this.lastZoom + (e.scale - 1) * 0.5) 68 | } 69 | }) 70 | 71 | window.addEventListener('wheel', (e) => { 72 | let amount = -e.deltaY * 0.001 73 | 74 | // Deal with Firefox mousewheel speed being slower 75 | if (e.mozInputSource === 1 && e.deltaMode === 1) { 76 | amount *= 50 77 | } 78 | 79 | if (e.target == gl.canvas) { 80 | this.zoom.changeBy(amount) 81 | e.preventDefault() 82 | } 83 | }, { passive: false }) 84 | } 85 | 86 | handleMouseMove(e) { 87 | let newMousePosition = { x: e.screenX, y: e.screenY } 88 | 89 | if (this.mousePosition !== undefined && this.dragging) { 90 | let deltaX = newMousePosition.x - this.mousePosition.x 91 | let deltaY = newMousePosition.y - this.mousePosition.y 92 | this.handlePan(deltaX * 0.3, deltaY * 0.3) 93 | e.stopPropagation() 94 | } 95 | 96 | this.mousePosition = newMousePosition 97 | } 98 | 99 | handlePan(deltaX, deltaY) { 100 | let zoomFactor = 1 - (this.zoom.value * 0.8) 101 | 102 | this.longitude.changeBy(-deltaX * zoomFactor ) 103 | this.latitude.changeBy(deltaY * zoomFactor) 104 | } 105 | 106 | getValues(projection) { 107 | if (projection == 'sphere') { 108 | return this.getSphereValues() 109 | } 110 | else { 111 | return this.getPlaneValues() 112 | } 113 | } 114 | 115 | getSphereValues() { 116 | let aspect = this.gl.canvas.width / this.gl.canvas.height 117 | let fov = toRadians(30) / _.clamp(aspect, 0.0, 1.0) 118 | let projection = m4.perspective(fov, aspect, 0.01, 10) 119 | let eye = [0, 0, -(5.5 - this.zoom.value * 4.0)] 120 | let camera = m4.identity() 121 | m4.rotateY(camera, toRadians(this.longitude.value + 180), camera) 122 | m4.rotateX(camera, toRadians(this.latitude.value), camera) 123 | 124 | eye = m4.transformPoint(camera, eye) 125 | let up = m4.transformPoint(camera, [0, 1, 0]) 126 | let target = [0, 0, 0] 127 | let view = m4.inverse(m4.lookAt(eye, target, up)) 128 | 129 | return { 130 | view: view, 131 | projection: projection, 132 | eye: eye 133 | } 134 | } 135 | 136 | getPlaneValues() { 137 | let aspect = this.gl.canvas.width / this.gl.canvas.height 138 | let fov = toRadians(30) / _.clamp(aspect, 0.0, 1.0) 139 | let projection = m4.perspective(fov, aspect, 0.01, 10) 140 | let target = [ 141 | this.longitude.value / 180, 142 | 0, 143 | -this.latitude.value / 180 144 | ] 145 | 146 | let eye = [ 147 | target[0], 148 | 2 - this.zoom.value * 1.75, 149 | target[2] 150 | ] 151 | 152 | let up = [0, 0, -1] 153 | let view = m4.inverse(m4.lookAt(eye, target, up)) 154 | 155 | return { 156 | view: view, 157 | projection: projection, 158 | eye: eye 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/js/components/checkboxOption.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import _ from 'lodash' 3 | 4 | export default function registerCheckboxOption( 5 | controller, 6 | propertyMap 7 | ) { 8 | Vue.component('checkbox-option', Vue.extend({ 9 | data: function () { 10 | return propertyMap[this.property].data 11 | }, 12 | props: [ 13 | 'label', 14 | 'property' 15 | ], 16 | template: ` 17 | 21 | `, 22 | watch: { 23 | 'enabled': { 24 | handler: function () { 25 | window.requestAnimationFrame(() => { 26 | window.requestAnimationFrame(() => { 27 | controller.layerUpdated(propertyMap[this.property].layer) 28 | }) 29 | }) 30 | } 31 | } 32 | } 33 | })) 34 | } 35 | -------------------------------------------------------------------------------- /app/js/components/projections.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | export default function registerProjections( 4 | controller, 5 | scene 6 | ) { 7 | Vue.component('projections', Vue.extend({ 8 | data: function () { 9 | return scene 10 | }, 11 | computed: {}, 12 | props: [], 13 | template: ` 14 |
15 |
16 |
18 | 23 |
24 |
26 | 31 |
32 |
33 |
34 | `, 35 | watch: { 36 | 'projection': { 37 | handler: () => { controller.updated() } 38 | } 39 | } 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /app/js/components/rangeSlider.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import _ from 'lodash' 3 | 4 | export default function registerRangeSlider( 5 | controller, 6 | propertyMap 7 | ) { 8 | Vue.component('range-slider', Vue.extend({ 9 | beforeCreate: function () { 10 | this.updateFormatted = function (vm) { 11 | vm.formatted = propertyMap[vm.property].formatted(vm) 12 | } 13 | }, 14 | created: function () { 15 | this.updateFormatted(this) 16 | }, 17 | data: function () { 18 | return _.extend( 19 | propertyMap[this.property].data, 20 | { 'formatted': '' } 21 | ) 22 | }, 23 | props: [ 24 | 'label', 25 | 'property', 26 | 'vertical' 27 | ], 28 | template: ` 29 |
30 |
31 | 32 | {{formatted}} 33 |
34 |
35 | 40 |
41 |
42 | `, 43 | watch: { 44 | 'value': { 45 | handler: function () { 46 | this.updateFormatted(this) 47 | controller.updated() 48 | } 49 | } 50 | } 51 | })) 52 | } 53 | -------------------------------------------------------------------------------- /app/js/components/renderModes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | export default function registerRenderModes( 4 | controller, 5 | scene 6 | ) { 7 | Vue.component('render-modes', Vue.extend({ 8 | data: function () { 9 | return scene 10 | }, 11 | computed: {}, 12 | props: [], 13 | template: ` 14 |
15 |
16 |
18 | 23 |
24 | 27 | 28 | 31 | 32 |
33 |
34 |
36 | 41 |
42 |
44 | 49 |
50 |
52 | 57 |
58 | 61 | 62 |
63 |
64 |
65 |
66 | `, 67 | watch: { 68 | 'renderMode': { 69 | handler: () => { controller.updated() } 70 | } 71 | } 72 | })) 73 | } 74 | -------------------------------------------------------------------------------- /app/js/controlRange.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default class ControlRange { 4 | constructor(value, min, max, wrap = false) { 5 | this.value = value 6 | this.min = min 7 | this.max = max 8 | this.wrap = wrap 9 | } 10 | 11 | changeBy(amount) { 12 | this.changeTo(this.value + amount) 13 | } 14 | 15 | changeTo(value) { 16 | if (this.wrap) { 17 | this.setWrap(value) 18 | } 19 | else { 20 | this.setClamp(value) 21 | } 22 | } 23 | 24 | setClamp(value) { 25 | this.value = _.clamp(value, this.min, this.max) 26 | } 27 | 28 | setWrap(value) { 29 | if (value > this.max) { 30 | let remainder = value % this.max 31 | value = this.min + remainder 32 | } 33 | else if (value < this.min) { 34 | value = this.max - (this.min - value) 35 | } 36 | 37 | this.value = value 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/js/controller.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import moment from 'moment' 3 | import numeral from 'numeral' 4 | import _ from 'lodash' 5 | import Stats from 'stats.js' 6 | 7 | import Scene from './scene' 8 | import Renderer from './renderer' 9 | import Camera from './camera' 10 | import LayerCanvas from './layerCanvas' 11 | import LandMaskLayer from './landMaskLayer' 12 | import BordersLayer from './bordersLayer' 13 | import registerRangeSlider from './components/rangeSlider' 14 | import registerCheckboxOption from './components/checkboxOption' 15 | import registerRenderModes from './components/renderModes' 16 | import registerProjections from './components/projections' 17 | 18 | export default class Controller { 19 | constructor(gl, vectors) { 20 | this.layerCanvas = new LayerCanvas(gl) 21 | this.layers = { 22 | landmask: new LandMaskLayer(gl, vectors, this.layerCanvas), 23 | borders: new BordersLayer(gl, vectors, this.layerCanvas) 24 | } 25 | 26 | this.scene = new Scene(gl, this.layerCanvas, this.layers) 27 | this.renderer = new Renderer(gl, this.scene) 28 | 29 | this.camera = new Camera(gl) 30 | 31 | let noFormat = () => '' 32 | 33 | let degrees = (vm) => { 34 | return numeral(vm.value).format('0.00') + '°' 35 | } 36 | 37 | let hour = () => { 38 | return this.scene.calculatedMoment().format('h:mm a') + ' UTC' 39 | } 40 | 41 | let day = () => { 42 | return this.scene.calculatedMoment().format('YYYY-MM-DD') 43 | } 44 | 45 | let multiple = (vm) => { 46 | if (vm.value === 1) { 47 | return '(Realistic) 1x' 48 | } 49 | else { 50 | return numeral(vm.value).format('0.00') + 'X' 51 | } 52 | } 53 | 54 | let propertyMap = { 55 | 'latitude': { 56 | data: this.camera.latitude, 57 | formatted: degrees 58 | }, 59 | 'longitude': { 60 | data: this.camera.longitude, 61 | formatted: degrees 62 | }, 63 | 'zoom': { 64 | data: this.camera.zoom, 65 | formatted: noFormat 66 | }, 67 | 'hourOfDay': { 68 | data: this.scene.hourOfDay, 69 | formatted: hour 70 | }, 71 | 'dayOfYear': { 72 | data: this.scene.dayOfYear, 73 | formatted: day 74 | }, 75 | 'elevationScale': { 76 | data: this.scene.elevationScale, 77 | formatted: multiple 78 | }, 79 | 'rivers': { 80 | data: this.layers['landmask'].options.rivers, 81 | layer: 'landmask' 82 | }, 83 | 'countries': { 84 | data: this.layers['borders'].options.countries, 85 | layer: 'borders' 86 | } 87 | } 88 | 89 | registerRangeSlider(this, propertyMap) 90 | registerCheckboxOption(this, propertyMap) 91 | registerRenderModes(this, this.scene) 92 | registerProjections(this, this.scene) 93 | 94 | //this.enableStats() 95 | 96 | this.vue = new Vue({ el: '.map-controls' }) 97 | 98 | this.updateQueued = false 99 | _.each(this.layers, (layer, name) => { 100 | _.defer(() => { this.layerUpdated(name) }) 101 | }) 102 | 103 | window.addEventListener('resize', () => { this.updated() }) 104 | window.addEventListener('texture-loaded', () => { this.updated() }) 105 | } 106 | 107 | enableStats() { 108 | this.stats = new Stats() 109 | this.stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom 110 | this.stats.dom.style.left = 'auto' 111 | this.stats.dom.style.right = '0' 112 | document.body.appendChild(this.stats.dom) 113 | } 114 | 115 | layerUpdated(layer) { 116 | this.layers[layer].draw() 117 | } 118 | 119 | updated() { 120 | if (!this.updateQueued) { 121 | this.updateQueued = true 122 | window.requestAnimationFrame(() => { 123 | this.renderFrame() 124 | this.updateQueued = false 125 | if (!this.loaded) { 126 | this.loaded = true 127 | let loading = document.querySelector('.loading') 128 | loading.parentNode.removeChild(loading) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | renderFrame() { 135 | if (this.stats) { 136 | this.stats.begin() 137 | } 138 | 139 | this.renderer.render( 140 | window.performance.now(), 141 | this.scene, 142 | this.camera, 143 | this.renderer 144 | ) 145 | 146 | if (this.stats) { 147 | this.stats.end() 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/js/coordinates.js: -------------------------------------------------------------------------------- 1 | import { toRadians } from './utils' 2 | 3 | // https://www.npmjs.com/package/mod-loop 4 | function loop(value, divisor) { 5 | let n = value % divisor 6 | return n < 0 ? (divisor + n) : n 7 | } 8 | 9 | function getJulianFromUnix(time) { 10 | return ((time / 1000) / 86400.0) + 2440587.5 11 | } 12 | 13 | // Returns equatorial coordinates for the sun at 14 | // a given time, based on: 15 | // http://aa.usno.navy.mil/faq/docs/SunApprox.php 16 | // http://www.stargazing.net/kepler/sun.html 17 | export default function sunCoordinates(time) { 18 | let D = getJulianFromUnix(time) - 2451545 19 | let g = 357.529 + 0.98560028 * D 20 | let L = 280.459 + 0.98564736 * D 21 | 22 | let lambda = L + 23 | 1.915 * Math.sin(toRadians(g)) + 24 | 0.020 * Math.sin(toRadians(2 * g)) 25 | 26 | let e = 23.439 - 0.00000036 * D 27 | let y = Math.cos(toRadians(e)) * Math.sin(toRadians(lambda)) 28 | let x = Math.cos(toRadians(lambda)) 29 | 30 | let rightAscension = Math.atan2(y, x) 31 | let declination = Math.asin( 32 | Math.sin(toRadians(e)) * Math.sin(toRadians(lambda)) 33 | ) 34 | 35 | let gmst = 18.697374558 + 24.06570982441908 * D 36 | let hourAngle = (gmst / 24 * Math.PI * 2) - rightAscension 37 | 38 | return { 39 | hourAngle: hourAngle, 40 | declination: declination 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/js/landMaskLayer.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import * as topojson from 'topojson' 3 | import _ from 'lodash' 4 | 5 | import { 6 | compositeOperation, 7 | dispatchEvent 8 | } from './utils' 9 | 10 | export default class LandMaskLayer { 11 | constructor(gl, vectors, layerCanvas) { 12 | this.options = { rivers: { enabled: false } } 13 | this.layerCanvas = layerCanvas 14 | this.land = topojson.feature(vectors, vectors.objects.land) 15 | this.lakes = topojson.feature(vectors, vectors.objects.lakes) 16 | this.rivers = topojson.feature(vectors, vectors.objects.rivers) 17 | } 18 | 19 | draw() { 20 | let ctx = this.layerCanvas.ctx 21 | ctx.fillStyle = '#000' 22 | ctx.fillRect(0, 0, this.layerCanvas.width, this.layerCanvas.height) 23 | 24 | // land 25 | ctx.beginPath() 26 | this.layerCanvas.path(this.land) 27 | ctx.fillStyle = '#fff' 28 | ctx.fill() 29 | ctx.lineWidth = 1.0 * this.scale 30 | ctx.strokeStyle = '#fff' 31 | ctx.stroke() 32 | 33 | // lakes 34 | ctx.beginPath() 35 | this.layerCanvas.path(this.lakes) 36 | ctx.fillStyle = '#000' 37 | ctx.fill() 38 | ctx.lineWidth = 1.0 * this.layerCanvas.scale 39 | ctx.strokeStyle = '#000' 40 | ctx.stroke() 41 | 42 | // rivers 43 | if (this.options.rivers.enabled) { 44 | ctx.beginPath() 45 | this.layerCanvas.path(this.rivers) 46 | ctx.lineWidth = 1.0 * this.layerCanvas.scale 47 | ctx.strokeStyle = '#000' 48 | ctx.stroke() 49 | } 50 | 51 | dispatchEvent('landmask-updated') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/js/layerCanvas.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import _ from 'lodash' 3 | 4 | export default class LayerCanvas { 5 | constructor(gl) { 6 | if (gl.getParameter(gl.MAX_TEXTURE_SIZE) >= 8192) { 7 | this.width = 8192 8 | this.height = 4096 9 | this.scale = 1.0 10 | } 11 | else { 12 | this.width = 4096 13 | this.height = 2048 14 | this.scale = 0.5 15 | } 16 | 17 | this.projection = d3.geoEquirectangular() 18 | .scale(this.height / Math.PI) 19 | .translate([this.width / 2, this.height / 2]) 20 | 21 | //this.canvas = d3.select('body').append('canvas') 22 | this.canvas = d3.select(document.createElement('canvas')) 23 | .attr('width', this.width) 24 | .attr('height', this.height) 25 | 26 | // based on https://bost.ocks.org/mike/map/ 27 | this.ctx = this.canvas.node().getContext('2d') 28 | this.path = d3.geoPath().projection(this.projection).context(this.ctx) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/js/octahedronSphere.js: -------------------------------------------------------------------------------- 1 | import { v3 } from 'twgl.js/dist/4.x/twgl-full' 2 | import _ from 'lodash' 3 | 4 | import { 5 | Vec2Array, 6 | Vec3Array 7 | } from './vectorArray' 8 | 9 | // icosphere-like sphere, except based on octahedron 10 | // which gives a clean vertical seam for texturing 11 | // based on: 12 | // https://github.com/hughsk/icosphere/blob/master/index.js 13 | 14 | export default function octahedronSphere(divisions) { 15 | let initialPoints = new Float32Array(_.flatten([ 16 | [0,1,0], [0,0,-1], [-1,0,0], 17 | [0,1,0], [-1,0,0], [0,0,1], 18 | [0,1,0], [0,0,1], [1,0,0], 19 | [0,1,0], [1,0,0], [0,0,-1], 20 | [0,-1,0], [-1,0,0], [0,0,-1], 21 | [0,-1,0], [0,0,1], [-1,0,0], 22 | [0,-1,0], [1,0,0], [0,0,1], 23 | [0,-1,0], [0,0,-1], [1,0,0] 24 | ])) 25 | 26 | let pointLODs = [new Vec3Array(initialPoints)] 27 | for (let i = 0; i < divisions; i++) { 28 | let current = pointLODs[i] 29 | let split = new Vec3Array( 30 | new Float32Array(current.data.length * 4) 31 | ) 32 | 33 | for (let j = 0; j < current.length; j+=3) { 34 | splitTriangle(current, split, j) 35 | } 36 | 37 | pointLODs.push(split) 38 | } 39 | 40 | let points = pointLODs[pointLODs.length - 1] 41 | let pointUvs = new Vec2Array(new Float32Array(points.length * 2)) 42 | 43 | for (let i = 0; i < points.length; i+=3) { 44 | calculateUvs(pointUvs, points, i) 45 | } 46 | 47 | let index = 0 48 | let indices = new Int16Array(points.length) 49 | let indexedPoints = new Vec3Array(new Float32Array(points.data.length)) 50 | let indexedUvs = new Vec2Array(new Float32Array(pointUvs.data.length)) 51 | let pointMap = {} 52 | 53 | let i = 0 54 | for (i; i < points.length; i++) { 55 | let point = points.get(i) 56 | let uv = pointUvs.get(i) 57 | let key = [point[0], point[1], point[2], uv[0], uv[1]].join(',') 58 | let existingIndex = pointMap[key] 59 | if (existingIndex === undefined) { 60 | pointMap[key] = index 61 | indexedPoints.set(index, point) 62 | indexedUvs.set(index, uv) 63 | indices[i] = index 64 | index += 1 65 | } 66 | else { 67 | indices[i] = existingIndex 68 | } 69 | } 70 | 71 | return { 72 | indices: indices.subarray(0, i), 73 | position: indexedPoints.data.subarray(0, index * 3), 74 | texcoord: indexedUvs.data.subarray(0, index * 2), 75 | elevation: new Float32Array(index) 76 | } 77 | } 78 | 79 | function splitTriangle(points, target, offset) { 80 | let a = points.get(offset) 81 | let b = points.get(offset + 1) 82 | let c = points.get(offset + 2) 83 | let ab = Array.prototype.slice.call(v3.normalize(v3.add(a, b))) 84 | let bc = Array.prototype.slice.call(v3.normalize(v3.add(b, c))) 85 | let ca = Array.prototype.slice.call(v3.normalize(v3.add(c, a))) 86 | 87 | target.setRange(offset * 4, [ 88 | a, ab, ca, 89 | ab, bc, ca, 90 | ab, b, bc, 91 | ca, bc, c 92 | ]) 93 | } 94 | 95 | function calculateUvs(pointUvs, points, offset) { 96 | let a = uvPoint(points.get(offset)) 97 | let b = uvPoint(points.get(offset + 1)) 98 | let c = uvPoint(points.get(offset + 2)) 99 | 100 | let min = Math.min(a[0], b[0], c[0]) 101 | let max = Math.max(a[0], b[0], c[0]) 102 | 103 | // Seam triangle will span almost the whole range from 0 to 1. 104 | // Fix seam by clamping the high values to 0. 105 | if (max - min > 0.5) { 106 | a[0] = a[0] == 1 ? 0 : a[0] 107 | b[0] = b[0] == 1 ? 0 : b[0] 108 | c[0] = c[0] == 1 ? 0 : c[0] 109 | } 110 | 111 | pointUvs.setRange(offset, [a, b, c]) 112 | } 113 | 114 | function uvPoint(p) { 115 | return [ 116 | (Math.atan2(p[0], p[2]) / (2 * Math.PI)) + 0.5, 117 | 1.0 - ((Math.asin(p[1]) / Math.PI) + 0.5) 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /app/js/renderer.js: -------------------------------------------------------------------------------- 1 | import * as twgl from 'twgl.js/dist/4.x/twgl-full' 2 | import Shaders from './shaders.js' 3 | 4 | const m4 = twgl.m4 5 | 6 | export default class Renderer { 7 | constructor(gl, scene) { 8 | this.gl = gl 9 | 10 | gl.clearColor(0, 0, 0, 0) 11 | 12 | this.derivatives = gl.getExtension('OES_standard_derivatives') 13 | this.anisotropic = gl.getExtension('EXT_texture_filter_anisotropic') 14 | 15 | this.uniforms = {} 16 | for (name in scene.textures) { 17 | let texture = scene.textures[name] 18 | this.setupGlobeTexture(gl, texture) 19 | this.uniforms[name] = texture 20 | } 21 | 22 | this.shaders = new Shaders(gl) 23 | 24 | gl.enable(gl.DEPTH_TEST) 25 | gl.enable(gl.CULL_FACE) 26 | 27 | window.addEventListener('texture-loaded', (e) => { 28 | this.uniforms[e.detail.texture + 'Size'] = new Float32Array([ 29 | e.detail.width, 30 | e.detail.height 31 | ]) 32 | }) 33 | 34 | this.updateCanvasSize(gl) 35 | window.addEventListener('resize', () => { 36 | this.updateCanvasSize(gl) 37 | }) 38 | } 39 | 40 | setupGlobeTexture(gl, texture) { 41 | gl.bindTexture(gl.TEXTURE_2D, texture) 42 | 43 | if (this.anisotropic) { 44 | gl.texParameterf( 45 | gl.TEXTURE_2D, 46 | this.anisotropic.TEXTURE_MAX_ANISOTROPY_EXT, 47 | 16 48 | ) 49 | } 50 | 51 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) 52 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 53 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) 54 | } 55 | 56 | render(time, scene, camera) { 57 | let gl = this.gl 58 | 59 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 60 | 61 | let model = m4.identity() 62 | let light = scene.getSunVector() 63 | 64 | Object.assign( 65 | this.uniforms, 66 | camera.getValues(scene.projection), 67 | scene.getElevationScales(), 68 | { 69 | model: model, 70 | time: time, 71 | lightDirection: m4.transformPoint(light, [-1, 0, 0]), 72 | flatProjection: scene.projection == 'plane' 73 | } 74 | ) 75 | 76 | let program = this.shaders.getProgram(scene.projection, scene.renderMode) 77 | 78 | gl.useProgram(program.program) 79 | twgl.setBuffersAndAttributes( 80 | gl, program, scene[scene.projection + 'Buffer'] 81 | ) 82 | twgl.setUniforms(program, this.uniforms) 83 | 84 | gl.drawElements( 85 | gl.TRIANGLES, 86 | scene[scene.projection + 'Buffer'].numElements, 87 | gl.UNSIGNED_SHORT, 88 | 0 89 | ) 90 | } 91 | 92 | updateCanvasSize(gl) { 93 | let width = gl.canvas.parentNode.offsetWidth 94 | let height = gl.canvas.parentNode.offsetHeight 95 | 96 | if (width + 'px' != gl.canvas.style.width || 97 | height + 'px' != gl.canvas.style.height) { 98 | // set the display size of the canvas 99 | gl.canvas.style.width = width + "px" 100 | gl.canvas.style.height = height + "px" 101 | 102 | // set the size of the drawingBuffer 103 | // https://www.khronos.org/webgl/wiki/HandlingHighDPI 104 | let devicePixelRatio = window.devicePixelRatio || 1 105 | 106 | // Slightly lower res on retina-ish displays 107 | if (devicePixelRatio > 1 && width > 1500) { 108 | devicePixelRatio -= 0.5 109 | } 110 | 111 | gl.canvas.width = Math.floor(width * devicePixelRatio) 112 | gl.canvas.height = Math.floor(height * devicePixelRatio) 113 | 114 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/js/scene.js: -------------------------------------------------------------------------------- 1 | import * as twgl from 'twgl.js/dist/4.x/twgl-full' 2 | import moment from 'moment' 3 | import _ from 'lodash' 4 | 5 | import { dispatchEvent } from './utils' 6 | import ControlRange from './controlRange' 7 | import octahedronSphere from './octahedronSphere' 8 | import sunCoordinates from './coordinates.js' 9 | 10 | const m4 = twgl.m4 11 | 12 | export default class Scene { 13 | constructor(gl, layerCanvas, layers) { 14 | this.gl = gl 15 | this.layerCanvas = layerCanvas 16 | this.layers = layers 17 | 18 | this.hourOfDay = new ControlRange(12, 0.001, 23.999) 19 | this.dayOfYear = new ControlRange(182, 1, 365) 20 | this.elevationScale = new ControlRange(10, 1, 30) 21 | 22 | this.sphere = octahedronSphere(6) 23 | 24 | this.sphereBuffer = twgl.createBufferInfoFromArrays(gl, { 25 | indices: { numComponents: 3, data: this.sphere.indices }, 26 | position: { numComponents: 3, data: this.sphere.position }, 27 | texcoord: { numComponents: 2, data: this.sphere.texcoord }, 28 | elevation: { numComponents: 1, data: this.sphere.elevation } 29 | }) 30 | 31 | this.plane = twgl.primitives.createPlaneVertices( 32 | 2, 1, 255, 127 33 | ) 34 | 35 | this.plane.elevation = new Float32Array( 36 | this.sphere.position.length 37 | ) 38 | 39 | this.planeBuffer = twgl.createBufferInfoFromArrays(gl, { 40 | indices: { numComponents: 3, data: this.plane.indices }, 41 | position: { numComponents: 3, data: this.plane.position }, 42 | texcoord: { numComponents: 2, data: this.plane.texcoord }, 43 | elevation: { numComponents: 1, data: this.plane.elevation } 44 | }) 45 | 46 | this.projection = 'sphere' 47 | this.renderMode = 'dayAndNight' 48 | this.fillInElevations() 49 | this.initTextures() 50 | 51 | window.addEventListener('landmask-updated', () => { 52 | this.updateLayerTexture('landmask') 53 | }) 54 | 55 | window.addEventListener('borders-updated', () => { 56 | this.updateLayerTexture('borders') 57 | }) 58 | } 59 | 60 | updateLayerTexture(texture) { 61 | let canvas = this.layerCanvas.canvas.node() 62 | twgl.setTextureFromElement( 63 | this.gl, 64 | this.textures[texture + 'Map'], 65 | canvas 66 | ) 67 | 68 | dispatchEvent('texture-loaded', { 69 | texture: texture + 'Map', 70 | width: canvas.width, 71 | height: canvas.height 72 | }) 73 | } 74 | 75 | initTextures() { 76 | let textures = { 77 | diffuseMap: { 78 | format: this.gl.RGB, 79 | internalFormat: this.gl.RGB, 80 | src: 'data/color-4096.jpg', 81 | color: [0,0,0,1] 82 | }, 83 | topographyMap: { 84 | format: this.gl.LUMINANCE, 85 | internalFormat: this.gl.LUMINANCE, 86 | src: 'data/topo-bathy-4096.jpg', 87 | color: [0,0,0,1] 88 | }, 89 | landmaskMap: { 90 | format: this.gl.LUMINANCE, 91 | internalFormat: this.gl.LUMINANCE, 92 | width: 2, 93 | height: 2 94 | }, 95 | bordersMap: { 96 | format: this.gl.LUMINANCE, 97 | internalFormat: this.gl.LUMINANCE, 98 | width: 2, 99 | height: 2 100 | }, 101 | lightsMap: { 102 | src: 'data/lights-4096.png', 103 | format: this.gl.LUMINANCE, 104 | internalFormat: this.gl.LUMINANCE, 105 | color: [0,0,0,1] 106 | } 107 | } 108 | 109 | for (let key in textures) { 110 | dispatchEvent('texture-loaded', { 111 | texture: key, 112 | width: 1, 113 | height: 1 114 | }) 115 | } 116 | 117 | this.textures = twgl.createTextures( 118 | this.gl, 119 | textures, 120 | (err, textures, sources) => { 121 | for (let key in sources) { 122 | dispatchEvent('texture-loaded', { 123 | texture: key, 124 | width: sources[key].width, 125 | height: sources[key].height 126 | }) 127 | } 128 | } 129 | ) 130 | } 131 | 132 | calculatedMoment() { 133 | return moment('2016-01-01T00:00:00.000Z') 134 | .utcOffset(0) 135 | .dayOfYear(this.dayOfYear.value) 136 | .add(this.hourOfDay.value * 60 * 60, 'seconds') 137 | } 138 | 139 | getSunVector() { 140 | let moment = this.calculatedMoment() 141 | let sun = sunCoordinates(_.toInteger(moment.format('x'))) 142 | 143 | let light = m4.identity() 144 | light = m4.rotateY(light, (Math.PI / 2) - sun.hourAngle) 145 | light = m4.rotateZ(light, -sun.declination) 146 | return light 147 | } 148 | 149 | getElevationScales() { 150 | let base = (10034 * 2) / 6371000 151 | let land = base 152 | let ocean = 0 153 | 154 | if (this.renderMode === 'elevation') { 155 | land = this.elevationScale.value * base 156 | ocean = this.elevationScale.value * base 157 | } 158 | 159 | return { 160 | oceanElevationScale: ocean, 161 | landElevationScale: land 162 | } 163 | } 164 | 165 | // Sample texture to fill in vertex attribute. 166 | // This could just be a texture lookup in the vertex 167 | // shader, but that results in visible gaps between 168 | // triangles on iphone. 169 | fillInElevations() { 170 | let img = new Image() 171 | img.onload = () => { 172 | let canvas = document.createElement('canvas') 173 | canvas.width = img.width 174 | canvas.height = img.height 175 | let ctx = canvas.getContext('2d') 176 | ctx.drawImage(img, 0, 0, img.width, img.height) 177 | let data = ctx.getImageData(0, 0, img.width, img.height).data 178 | 179 | let w = img.width - 1 180 | let h = img.height - 1 181 | for (let i = 0; i < this.sphere.elevation.length; i++) { 182 | let u = this.sphere.texcoord[i * 2] 183 | let v = this.sphere.texcoord[i * 2 + 1] 184 | let x = _.clamp(_.floor((u == 1 ? 0 : u) * w), 0, w) 185 | let y = _.clamp(_.floor((v == 1 ? 0 : v) * h), 0, h) 186 | let sample = data[[x + y * img.width] * 4] 187 | this.sphere.elevation[i] = (sample / 255) - 0.5 188 | } 189 | 190 | twgl.setAttribInfoBufferFromArray( 191 | this.gl, 192 | this.sphereBuffer.attribs.elevation, 193 | this.sphere.elevation 194 | ) 195 | 196 | twgl.setAttribInfoBufferFromArray( 197 | this.gl, 198 | this.planeBuffer.attribs.elevation, 199 | this.plane.elevation 200 | ) 201 | } 202 | 203 | img.src = 'data/topo-bathy-128.jpg' 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /app/js/shaders.js: -------------------------------------------------------------------------------- 1 | import { createProgramInfo as create } from 'twgl.js/dist/4.x/twgl-full' 2 | import sphereVert from '../shaders/sphere.vert.glsl' 3 | import planeVert from '../shaders/plane.vert.glsl' 4 | import dayAndNightFrag from '../shaders/dayAndNight.frag.glsl' 5 | import dayAndNightSimpleFrag from '../shaders/dayAndNightSimple.frag.glsl' 6 | import dayFrag from '../shaders/day.frag.glsl' 7 | import nightFrag from '../shaders/night.frag.glsl' 8 | import elevationFrag from '../shaders/elevation.frag.glsl' 9 | 10 | export default class Shaders { 11 | constructor(gl) { 12 | this.shaders = { 13 | 'sphere': { 14 | 'dayAndNight': create(gl, [sphereVert, dayAndNightFrag]), 15 | 'day': create(gl, [sphereVert, dayFrag]), 16 | 'night': create(gl, [sphereVert, nightFrag]), 17 | 'elevation': create(gl, [sphereVert, elevationFrag]) 18 | }, 19 | 'plane': { 20 | 'dayAndNight': create(gl, [planeVert, dayAndNightSimpleFrag]), 21 | 'day': create(gl, [planeVert, dayFrag]), 22 | 'night': create(gl, [planeVert, nightFrag]), 23 | 'elevation': create(gl, [planeVert, elevationFrag]) 24 | } 25 | } 26 | } 27 | 28 | getProgram(projection, renderMode) { 29 | return this.shaders[projection][renderMode] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/js/utils.js: -------------------------------------------------------------------------------- 1 | import platform from 'platform' 2 | import _ from 'lodash' 3 | 4 | export function toRadians(degrees) { 5 | return degrees / 180 * Math.PI 6 | } 7 | 8 | // Needed for IE11 compatibility 9 | export function dispatchEvent(name, detail) { 10 | let evt = document.createEvent('CustomEvent') 11 | evt.initCustomEvent(name, false, false, detail || {}) 12 | window.dispatchEvent(evt) 13 | } 14 | -------------------------------------------------------------------------------- /app/js/vectorArray.js: -------------------------------------------------------------------------------- 1 | // Utilities for managing an array of vec2 or vec3 2 | // using a one-dimensional TypedArray 3 | 4 | export class Vec2Array { 5 | constructor(data) { 6 | this.data = data 7 | this.length = data.length / 2 8 | } 9 | 10 | get(offset) { 11 | let begin = offset * 2 12 | 13 | return [ 14 | this.data[begin], 15 | this.data[begin + 1] 16 | ] 17 | } 18 | 19 | setRange(offset, range) { 20 | for (let i = 0; i < range.length; i++) { 21 | this.set(offset + i, range[i]) 22 | } 23 | } 24 | 25 | set(offset, entry) { 26 | this.data[offset * 2] = entry[0] 27 | this.data[offset * 2 + 1] = entry[1] 28 | } 29 | } 30 | 31 | 32 | export class Vec3Array { 33 | constructor(data) { 34 | this.data = data 35 | this.length = data.length / 3 36 | } 37 | 38 | get(offset) { 39 | let begin = offset * 3 40 | 41 | return [ 42 | this.data[begin], 43 | this.data[begin + 1], 44 | this.data[begin + 2] 45 | ] 46 | } 47 | 48 | setRange(offset, range) { 49 | for (let i = 0; i < range.length; i++) { 50 | this.set(offset + i, range[i]) 51 | } 52 | } 53 | 54 | set(offset, entry) { 55 | this.data[offset * 3] = entry[0] 56 | this.data[offset * 3 + 1] = entry[1] 57 | this.data[offset * 3 + 2] = entry[2] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/shaders/day.frag.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | @import ./functions/gamma; 5 | @import ./functions/tonemap; 6 | @import ./functions/exposure; 7 | @import ./functions/brdf; 8 | 9 | uniform sampler2D topographyMap; 10 | uniform sampler2D diffuseMap; 11 | uniform sampler2D landmaskMap; 12 | uniform sampler2D bordersMap; 13 | uniform vec2 bordersMapSize; 14 | 15 | varying vec2 vUv; 16 | varying vec3 vNormal; 17 | 18 | void main() { 19 | vec3 constantLight = vNormal; 20 | vec3 V = vNormal; 21 | 22 | float landness = texture2D(landmaskMap, vUv, -0.25).r; 23 | float countryBorder = texture2D(bordersMap, vUv, -0.25).r; 24 | float oceanDepth = (0.5 - texture2D(topographyMap, vUv).r) * 2.0; 25 | 26 | vec3 oceanColor = mix( 27 | vec3(0.1, 0.15, 0.45), 28 | vec3(0.1, 0.15, 0.35), 29 | oceanDepth 30 | ); 31 | 32 | vec3 diffuseColor = mix( 33 | oceanColor, 34 | toLinear(texture2D(diffuseMap, vUv).rgb), 35 | landness 36 | ); 37 | 38 | vec3 N = vNormal; 39 | vec3 L = normalize(constantLight); 40 | vec3 H = normalize(L + V); 41 | 42 | float roughness = mix( 43 | mix(0.75, 0.55, oceanDepth), 44 | (1.0 - diffuseColor.r * 0.5), 45 | landness 46 | ); 47 | 48 | vec3 color = vec3(0.0); 49 | if (dot(vNormal, L) > 0.0) { 50 | vec3 lightColor = vec3(10.0); 51 | 52 | color = lightColor * brdf( 53 | diffuseColor, 54 | 0.0, //metallic 55 | 0.5, //subsurface 56 | 0.0, //specular 57 | roughness, //roughness 58 | L, V, N 59 | ); 60 | } 61 | 62 | vec3 shaded = toGamma(tonemap(color * 1.0)); 63 | vec3 final = shaded + countryBorder; 64 | gl_FragColor = vec4(final, 1.0); 65 | } 66 | -------------------------------------------------------------------------------- /app/shaders/dayAndNight.frag.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | @import ./functions/gamma; 5 | @import ./functions/heightDerivative; 6 | @import ./functions/perturbNormal; 7 | @import ./functions/tonemap; 8 | @import ./functions/exposure; 9 | @import ./functions/terrainBumpScale; 10 | @import ./functions/atmosphere; 11 | @import ./functions/nightAmbient; 12 | @import ./functions/brdf; 13 | 14 | uniform sampler2D topographyMap; 15 | uniform vec2 topographyMapSize; 16 | uniform sampler2D diffuseMap; 17 | uniform sampler2D landmaskMap; 18 | uniform sampler2D bordersMap; 19 | uniform vec2 bordersMapSize; 20 | uniform sampler2D lightsMap; 21 | uniform vec3 lightDirection; 22 | uniform vec3 eye; 23 | 24 | varying vec2 vUv; 25 | varying vec3 vPosition; 26 | varying vec3 vNormal; 27 | 28 | void main() { 29 | vec3 V = normalize(eye - vPosition); 30 | float vNdotL = dot(vNormal, lightDirection); 31 | float vNdotL_clamped = clamp(vNdotL, 0.0, 1.0); 32 | float vNdotV = dot(vNormal, V); 33 | float vNdotV_clamped = clamp(vNdotV, 0.0, 1.0); 34 | 35 | float landness = texture2D(landmaskMap, vUv, -0.25).r; 36 | float countryBorder = texture2D(bordersMap, vUv, -0.25).r; 37 | float oceanDepth = (0.5 - texture2D(topographyMap, vUv).r) * 2.0; 38 | 39 | vec2 dHdxy = heightDerivative( 40 | vUv, 41 | topographyMap, 42 | topographyMapSize 43 | ); 44 | 45 | dHdxy *= terrainBumpScale( 46 | landness, 47 | 0.0, 48 | vNdotL, 49 | vNdotV, 50 | eye, 51 | vPosition 52 | ); 53 | 54 | vec3 oceanColor = mix( 55 | vec3(0.0, 0.0, 0.35), 56 | vec3(0.0, 0.0, 0.3), 57 | oceanDepth 58 | ); 59 | 60 | vec3 diffuseColor = mix( 61 | oceanColor, 62 | toLinear(texture2D(diffuseMap, vUv).rgb), 63 | landness 64 | ); 65 | 66 | vec3 N = perturbNormal(vPosition, vNormal, dHdxy); 67 | vec3 L = normalize(lightDirection); 68 | vec3 H = normalize(L + V); 69 | 70 | float roughness = mix( 71 | mix(0.6, 0.8, oceanDepth), 72 | (1.0 - diffuseColor.r * 0.5), 73 | smoothstep(0.25, 0.75, landness) 74 | ); 75 | 76 | vec3 color = nightAmbient( 77 | vNdotL, 78 | diffuseColor, 79 | texture2D(lightsMap, vUv).x, 80 | vUv 81 | ); 82 | 83 | if (dot(vNormal, L) > 0.0) { 84 | vec3 lightColor = vec3(8.0); 85 | 86 | float incidence = pow(dot(N, L), 1.5); 87 | 88 | color = lightColor * incidence * brdf( 89 | diffuseColor, 90 | 0.0, //metallic 91 | 0.5, //subsurface 92 | 0.3, //specular 93 | roughness, //roughness 94 | L, V, N 95 | ); 96 | 97 | color += atmosphere( 98 | vNdotL_clamped, 99 | vNdotV_clamped, 100 | vec3(0.1, 0.1, 1.0) * 20.0 101 | ); 102 | } 103 | 104 | vec3 shaded = toGamma(tonemap(color * exposure(eye, L, 1.5, 300.0))); 105 | vec3 final = shaded + countryBorder; 106 | 107 | gl_FragColor = vec4(final, 1.0); 108 | } 109 | -------------------------------------------------------------------------------- /app/shaders/dayAndNightSimple.frag.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | @import ./functions/gamma; 5 | @import ./functions/heightDerivative; 6 | @import ./functions/perturbNormal; 7 | @import ./functions/tonemap; 8 | @import ./functions/exposure; 9 | @import ./functions/terrainBumpScale; 10 | @import ./functions/atmosphere; 11 | @import ./functions/nightAmbient; 12 | @import ./functions/brdf; 13 | 14 | uniform sampler2D topographyMap; 15 | uniform vec2 topographyMapSize; 16 | uniform sampler2D diffuseMap; 17 | uniform sampler2D landmaskMap; 18 | uniform sampler2D bordersMap; 19 | uniform vec2 bordersMapSize; 20 | uniform sampler2D lightsMap; 21 | uniform vec3 lightDirection; 22 | uniform vec3 eye; 23 | 24 | varying vec2 vUv; 25 | varying vec3 vPosition; 26 | varying vec3 vSpherePosition; 27 | varying vec3 vNormal; 28 | 29 | void main() { 30 | vec3 V = normalize(eye - vPosition); 31 | float vNdotL = dot(vNormal, lightDirection); 32 | float vNdotL_clamped = clamp(vNdotL, 0.0, 1.0); 33 | float vNdotV = dot(vNormal, V); 34 | float vNdotV_clamped = clamp(vNdotV, 0.0, 1.0); 35 | 36 | float landness = texture2D(landmaskMap, vUv, -0.25).r; 37 | float countryBorder = texture2D(bordersMap, vUv, -0.25).r; 38 | float oceanDepth = (0.5 - texture2D(topographyMap, vUv).r) * 2.0; 39 | 40 | vec2 dHdxy = heightDerivative( 41 | vUv, 42 | topographyMap, 43 | topographyMapSize 44 | ); 45 | 46 | float poleFalloff = 1.0 - pow(abs(vPosition.z * 2.0), 4.0); 47 | dHdxy *= terrainBumpScale( 48 | landness, 49 | 0.0, 50 | vNdotL, 51 | vNdotV, 52 | eye, 53 | vSpherePosition 54 | ) * poleFalloff; 55 | 56 | vec3 oceanColor = mix( 57 | vec3(0.15, 0.15, 0.35), 58 | vec3(0.125, 0.125, 0.3), 59 | oceanDepth 60 | ); 61 | 62 | vec3 diffuseColor = mix( 63 | oceanColor, 64 | toLinear(texture2D(diffuseMap, vUv).rgb), 65 | landness 66 | ); 67 | 68 | vec3 N = perturbNormal(vSpherePosition, vNormal, dHdxy); 69 | vec3 L = normalize(lightDirection); 70 | vec3 H = normalize(L + V); 71 | 72 | float roughness = mix( 73 | mix(0.6, 0.8, oceanDepth), 74 | (1.0 - diffuseColor.r * 0.5), 75 | smoothstep(0.25, 0.75, landness) 76 | ); 77 | 78 | vec3 color = nightAmbient( 79 | vNdotL - 0.025, 80 | diffuseColor, 81 | texture2D(lightsMap, vUv).x, 82 | vUv 83 | ); 84 | 85 | vec3 lightColor = vec3(0.05); 86 | 87 | if (dot(vNormal, L) > 0.0) { 88 | float incidence = dot(N, L); 89 | color = color + lightColor * pow(diffuseColor, vec3(2.2)) * incidence; 90 | } 91 | 92 | vec3 shaded = toGamma(tonemap(color * 100.0)); 93 | vec3 final = shaded + countryBorder; 94 | 95 | vec3 terminatorColor = vec3(0.4, 0.4, 0.0); 96 | final += terminatorColor * min(pow(1.0 - abs(vNdotL), 350.0), 1.0); 97 | gl_FragColor = vec4(final, 1.0); 98 | } 99 | -------------------------------------------------------------------------------- /app/shaders/elevation.frag.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | @import ./functions/gamma; 5 | @import ./functions/heightDerivative; 6 | @import ./functions/perturbNormal; 7 | @import ./functions/tonemap; 8 | @import ./functions/exposure; 9 | @import ./functions/terrainBumpScale; 10 | @import ./functions/atmosphere; 11 | @import ./functions/nightAmbient; 12 | @import ./functions/brdf; 13 | 14 | uniform sampler2D topographyMap; 15 | uniform sampler2D diffuseMap; 16 | uniform sampler2D landmaskMap; 17 | uniform sampler2D bordersMap; 18 | uniform sampler2D lightsMap; 19 | uniform vec3 lightDirection; 20 | uniform vec3 eye; 21 | uniform bool flatProjection; 22 | 23 | varying vec2 vUv; 24 | varying vec3 vPosition; 25 | varying vec3 vNormal; 26 | 27 | void main() { 28 | vec3 constantLight = vNormal; 29 | vec3 V = vNormal; 30 | 31 | float landness = texture2D(landmaskMap, vUv, -0.25).r; 32 | float countryBorder = texture2D(bordersMap, vUv, -0.25).r; 33 | float elevation = texture2D(topographyMap, vUv).r; 34 | 35 | float steps = 16.0; 36 | vec3 diffuseColor = vec3(1.0, 0.75, 0.5) * 37 | floor((elevation / 0.9) * steps) / steps; 38 | 39 | if (landness < 0.5) { 40 | diffuseColor =vec3(0.25, 0.5, 1.0) * 41 | floor((elevation / 0.9) * steps) / steps; 42 | } 43 | 44 | // Outline the transition from land to sea 45 | diffuseColor = mix( 46 | diffuseColor, 47 | vec3(0.0, 0.0, 0.5), 48 | pow(1.0 - abs(landness - 0.5) * 2.0, 1.0) 49 | ); 50 | 51 | vec3 N = vNormal; 52 | vec3 L = normalize(constantLight); 53 | vec3 H = normalize(L + V); 54 | 55 | vec3 color = vec3(0.0); 56 | if (dot(vNormal, L) > 0.0) { 57 | vec3 lightColor = vec3(20.0); 58 | 59 | color = lightColor * brdf( 60 | diffuseColor, 61 | 0.0, //metallic 62 | 0.5, //subsurface 63 | 0.3, //specular 64 | 0.99, //roughness 65 | L, V, N 66 | ); 67 | 68 | if (!flatProjection) { 69 | color += atmosphere( 70 | dot(vNormal, L), 71 | dot(vNormal, normalize(eye - vPosition)), 72 | vec3(5.0) 73 | ); 74 | } 75 | } 76 | 77 | vec3 shaded = toGamma(tonemap(color * 0.5)); 78 | vec3 final = shaded + countryBorder; 79 | gl_FragColor = vec4(final, 1.0); 80 | } 81 | -------------------------------------------------------------------------------- /app/shaders/functions/atmosphere.glsl: -------------------------------------------------------------------------------- 1 | vec3 atmosphere(float NdotL, float NdotV, vec3 color) { 2 | return ( 3 | max(pow(NdotL, 2.0), 0.0) * 4 | pow(1.0 - NdotV, 12.0) 5 | ) * color; 6 | } 7 | -------------------------------------------------------------------------------- /app/shaders/functions/brdf.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | Modified from https://github.com/wdas/brdf/blob/master/src/brdfs/disney.brdf 3 | 4 | Original license notice: 5 | # Copyright Disney Enterprises, Inc. All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License 9 | # and the following modification to it: Section 6 Trademarks. 10 | # deleted and replaced with: 11 | # 12 | # 6. Trademarks. This License does not grant permission to use the 13 | # trade names, trademarks, service marks, or product names of the 14 | # Licensor and its affiliates, except as required for reproducing 15 | # the content of the NOTICE file. 16 | # 17 | # You may obtain a copy of the License at 18 | # http://www.apache.org/licenses/LICENSE-2.0 19 | */ 20 | 21 | float specularTint = 0.0; 22 | float anisotropic = 0.0; 23 | float sheen = 0.0; 24 | float sheenTint = 0.5; 25 | float clearcoat = 0.0; 26 | float clearcoatGloss = 1.0; 27 | 28 | const float PI = 3.14159265358979323846; 29 | 30 | float sqr(float x) { return x*x; } 31 | 32 | float SchlickFresnel(float u) { 33 | float m = clamp(1.0 - u, 0.0, 1.0); 34 | float m2 = m * m; 35 | return m2 * m2 * m; // pow(m, 5) 36 | } 37 | 38 | float GTR1(float NdotH, float a) { 39 | if (a >= 1.0) return 1.0 / PI; 40 | float a2 = a * a; 41 | float t = 1.0 + (a2 - 1.0) * NdotH * NdotH; 42 | return (a2 - 1.0) / (PI * log(a2) * t); 43 | } 44 | 45 | float GTR2(float NdotH, float a) { 46 | float a2 = a * a; 47 | float t = 1.0 + (a2 - 1.0) * NdotH * NdotH; 48 | return a2 / (PI * t * t); 49 | } 50 | 51 | float GTR2_aniso(float NdotH, float HdotX, float HdotY, float ax, float ay) { 52 | return 1.0 / ( 53 | PI * ax * ay * 54 | sqr(sqr(HdotX / ax) + sqr(HdotY / ay) + NdotH * NdotH) 55 | ); 56 | } 57 | 58 | float smithG_GGX(float Ndotv, float alphaG) { 59 | float a = alphaG * alphaG; 60 | float b = Ndotv * Ndotv; 61 | return 1.0 / (Ndotv + sqrt(a + b - a * b)); 62 | } 63 | 64 | vec3 mon2lin(vec3 x) { 65 | return vec3(pow(x[0], 2.2), pow(x[1], 2.2), pow(x[2], 2.2)); 66 | } 67 | 68 | vec3 brdf( 69 | vec3 baseColor, 70 | float metallic, 71 | float subsurface, 72 | float specular, 73 | float roughness, 74 | vec3 L, vec3 V, vec3 N 75 | ) { 76 | float NdotL = dot(N, L); 77 | float NdotV = dot(N, V); 78 | 79 | vec3 H = normalize(L + V); 80 | float NdotH = dot(N, H); 81 | float LdotH = dot(L, H); 82 | 83 | vec3 Cdlin = mon2lin(baseColor); 84 | float Cdlum = 0.3 * Cdlin[0] + 0.6 * Cdlin[1] + 0.1 * Cdlin[2]; // luminance approx. 85 | 86 | vec3 Ctint = Cdlum > 0.0 ? Cdlin / Cdlum : vec3(1.0); // normalize lum. to isolate hue+sat 87 | vec3 Cspec0 = mix(specular * .08 * mix(vec3(1.0), Ctint, specularTint), Cdlin, metallic); 88 | vec3 Csheen = mix(vec3(1.0), Ctint, sheenTint); 89 | 90 | // Diffuse fresnel - go from 1 at normal incidence to .5 at grazing 91 | // and mix in diffuse retro-reflection based on roughness 92 | float FL = SchlickFresnel(NdotL), FV = SchlickFresnel(NdotV); 93 | float Fd90 = 0.5 + 2.0 * LdotH * LdotH * roughness; 94 | float Fd = mix(1.0, Fd90, FL) * mix(1.0, Fd90, FV); 95 | 96 | // Based on Hanrahan-Krueger brdf approximation of isotropic bssrdf 97 | // 1.25 scale is used to (roughly) preserve albedo 98 | // Fss90 used to "flatten" retroreflection based on roughness 99 | float Fss90 = LdotH * LdotH * roughness; 100 | float Fss = mix(1.0, Fss90, FL) * mix(1.0, Fss90, FV); 101 | float ss = 1.25 * (Fss * (1.0 / (NdotL + NdotV) - 0.5) + 0.5); 102 | 103 | // specular 104 | float Ds = GTR2(NdotH, max(0.001, sqr(roughness))); 105 | float FH = SchlickFresnel(LdotH); 106 | vec3 Fs = mix(Cspec0, vec3(1.0), FH); 107 | float roughg = sqr(roughness * 0.5 + 0.5); 108 | float Gs = smithG_GGX(NdotL, roughg) * smithG_GGX(NdotV, roughg); 109 | 110 | // sheen 111 | vec3 Fsheen = FH * sheen * Csheen; 112 | 113 | // clearcoat (ior = 1.5 -> F0 = 0.04) 114 | float Dr = GTR1(NdotH, mix(0.1, 0.001, clearcoatGloss)); 115 | float Fr = mix(0.04, 1.0, FH); 116 | float Gr = smithG_GGX(NdotL, 0.25) * smithG_GGX(NdotV, 0.25); 117 | 118 | return ((1.0/PI) * mix(Fd, ss, subsurface) * Cdlin + Fsheen) 119 | * (1.0 - metallic) + 120 | Gs * Fs * Ds + 121 | 0.25 * clearcoat * Gr * Fr * Dr; 122 | } 123 | -------------------------------------------------------------------------------- /app/shaders/functions/diffuseLambert.glsl: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/glslify/glsl-diffuse-lambert 2 | 3 | float lambertDiffuse( 4 | vec3 lightDirection, 5 | vec3 surfaceNormal) { 6 | return max(0.0, dot(lightDirection, surfaceNormal)); 7 | } 8 | -------------------------------------------------------------------------------- /app/shaders/functions/exposure.glsl: -------------------------------------------------------------------------------- 1 | // Set camera exposure based on angle between sun and eye 2 | 3 | float exposure(vec3 eye, vec3 L, float low, float high) { 4 | return mix( 5 | low, 6 | high, 7 | pow((1.0 - dot(normalize(eye), L)) / 2.0, 10.0) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/shaders/functions/gamma.glsl: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/glslify/glsl-gamma 2 | 3 | const float gamma = 2.2; 4 | 5 | float toLinear(float v) { 6 | return pow(v, gamma); 7 | } 8 | 9 | vec2 toLinear(vec2 v) { 10 | return pow(v, vec2(gamma)); 11 | } 12 | 13 | vec3 toLinear(vec3 v) { 14 | return pow(v, vec3(gamma)); 15 | } 16 | 17 | vec4 toLinear(vec4 v) { 18 | return vec4(toLinear(v.rgb), v.a); 19 | } 20 | 21 | float toGamma(float v) { 22 | return pow(v, 1.0 / gamma); 23 | } 24 | 25 | vec2 toGamma(vec2 v) { 26 | return pow(v, vec2(1.0 / gamma)); 27 | } 28 | 29 | vec3 toGamma(vec3 v) { 30 | return pow(v, vec3(1.0 / gamma)); 31 | } 32 | 33 | vec4 toGamma(vec4 v) { 34 | return vec4(toGamma(v.rgb), v.a); 35 | } 36 | -------------------------------------------------------------------------------- /app/shaders/functions/heightDerivative.glsl: -------------------------------------------------------------------------------- 1 | 2 | @import ./texture2DCubic; 3 | 4 | // Based on https://docs.unrealengine.com/latest/attachments/Engine/Rendering/LightingAndShadows/BumpMappingWithoutTangentSpace/mm_sfgrad_bump.pdf 5 | 6 | vec2 heightDerivative( 7 | vec2 texST, 8 | sampler2D map, 9 | vec2 textureResolution 10 | ) { 11 | vec2 TexDx = dFdx(texST); 12 | vec2 TexDy = dFdy(texST); 13 | vec2 STll = texST; 14 | vec2 STlr = texST + TexDx; 15 | vec2 STul = texST + TexDy; 16 | float Hll = texture2DCubic(map, STll, textureResolution).x; 17 | float Hlr = texture2DCubic(map, STlr, textureResolution).x; 18 | float Hul = texture2DCubic(map, STul, textureResolution).x; 19 | float dBs = Hlr - Hll; 20 | float dBt = Hul - Hll; 21 | return vec2(dBs, dBt); 22 | } 23 | -------------------------------------------------------------------------------- /app/shaders/functions/inverse.glsl: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/glslify/glsl-inverse 2 | 3 | float inverse(float m) { 4 | return 1.0 / m; 5 | } 6 | 7 | mat2 inverse(mat2 m) { 8 | return mat2(m[1][1],-m[0][1], 9 | -m[1][0], m[0][0]) / (m[0][0]*m[1][1] - m[0][1]*m[1][0]); 10 | } 11 | 12 | mat3 inverse(mat3 m) { 13 | float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; 14 | float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2]; 15 | float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2]; 16 | 17 | float b01 = a22 * a11 - a12 * a21; 18 | float b11 = -a22 * a10 + a12 * a20; 19 | float b21 = a21 * a10 - a11 * a20; 20 | 21 | float det = a00 * b01 + a01 * b11 + a02 * b21; 22 | 23 | return mat3(b01, (-a22 * a01 + a02 * a21), (a12 * a01 - a02 * a11), 24 | b11, (a22 * a00 - a02 * a20), (-a12 * a00 + a02 * a10), 25 | b21, (-a21 * a00 + a01 * a20), (a11 * a00 - a01 * a10)) / det; 26 | } 27 | 28 | mat4 inverse(mat4 m) { 29 | float 30 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 31 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 32 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 33 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 34 | 35 | b00 = a00 * a11 - a01 * a10, 36 | b01 = a00 * a12 - a02 * a10, 37 | b02 = a00 * a13 - a03 * a10, 38 | b03 = a01 * a12 - a02 * a11, 39 | b04 = a01 * a13 - a03 * a11, 40 | b05 = a02 * a13 - a03 * a12, 41 | b06 = a20 * a31 - a21 * a30, 42 | b07 = a20 * a32 - a22 * a30, 43 | b08 = a20 * a33 - a23 * a30, 44 | b09 = a21 * a32 - a22 * a31, 45 | b10 = a21 * a33 - a23 * a31, 46 | b11 = a22 * a33 - a23 * a32, 47 | 48 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 49 | 50 | return mat4( 51 | a11 * b11 - a12 * b10 + a13 * b09, 52 | a02 * b10 - a01 * b11 - a03 * b09, 53 | a31 * b05 - a32 * b04 + a33 * b03, 54 | a22 * b04 - a21 * b05 - a23 * b03, 55 | a12 * b08 - a10 * b11 - a13 * b07, 56 | a00 * b11 - a02 * b08 + a03 * b07, 57 | a32 * b02 - a30 * b05 - a33 * b01, 58 | a20 * b05 - a22 * b02 + a23 * b01, 59 | a10 * b10 - a11 * b08 + a13 * b06, 60 | a01 * b08 - a00 * b10 - a03 * b06, 61 | a30 * b04 - a31 * b02 + a33 * b00, 62 | a21 * b02 - a20 * b04 - a23 * b00, 63 | a11 * b07 - a10 * b09 - a12 * b06, 64 | a00 * b09 - a01 * b07 + a02 * b06, 65 | a31 * b01 - a30 * b03 - a32 * b00, 66 | a20 * b03 - a21 * b01 + a22 * b00) / det; 67 | } 68 | -------------------------------------------------------------------------------- /app/shaders/functions/isNan.glsl: -------------------------------------------------------------------------------- 1 | // Hack isNan for debugging since WebGL doesn't define it 2 | 3 | bool isNan(float val) { 4 | return (val <= 0.0 || 0.0 <= val) ? false : true; 5 | } 6 | -------------------------------------------------------------------------------- /app/shaders/functions/nightAmbient.glsl: -------------------------------------------------------------------------------- 1 | vec3 nightAmbient( 2 | float NdotL, 3 | vec3 diffuseColor, 4 | float nightLightAmount, 5 | vec2 vUv 6 | ) { 7 | return 0.01 * ( 8 | nightLightAmount * vec3(1.0, 1.0, 0.8) + 9 | 0.1 * vec3(0.1, 0.1, 1.0) * diffuseColor 10 | ) * clamp((-NdotL + 0.01) * 2.0, 0.0, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /app/shaders/functions/perturbNormal.glsl: -------------------------------------------------------------------------------- 1 | // Based on https://docs.unrealengine.com/latest/attachments/Engine/Rendering/LightingAndShadows/BumpMappingWithoutTangentSpace/mm_sfgrad_bump.pdf 2 | 3 | vec3 perturbNormal(vec3 surf_pos, vec3 surf_norm, vec2 dHdxy) { 4 | // Calling derivatives per-component to address android bug: 5 | // http://stackoverflow.com/questions/20272272/ 6 | vec3 vSigmaX = vec3(dFdx(surf_pos.x), dFdx(surf_pos.y), dFdx(surf_pos.z)); 7 | vec3 vSigmaY = vec3(dFdy(surf_pos.x), dFdy(surf_pos.y), dFdy(surf_pos.z)); 8 | vec3 vN = vec3(normalize(surf_norm)); // normalized 9 | vec3 R1 = cross(vSigmaY, vN); 10 | vec3 R2 = cross(vN, vSigmaX); 11 | 12 | float fDet = dot(vSigmaX, R1); 13 | vec3 vGrad = sign(fDet) * (dHdxy.x * R1 + dHdxy.y * R2); 14 | return normalize(abs(fDet) * surf_norm - vGrad); 15 | } 16 | -------------------------------------------------------------------------------- /app/shaders/functions/terrainBumpScale.glsl: -------------------------------------------------------------------------------- 1 | // Scale bump map effect to produce relatively even relief 2 | // shading across surface. Goal is to avoid too much shading 3 | // at glancing angles and too little shading in the center. 4 | // 5 | // Also, allow for an additional bump scale factor with 6 | // `oceanFactor`. This allows ocean floor rendering to show 7 | // full bumps vs ocean surface rendering being flat. 8 | 9 | float terrainBumpScale( 10 | float landness, 11 | float oceanFactor, 12 | float vNdotL, 13 | float vNdotV, 14 | vec3 vEye, 15 | vec3 vPosition 16 | ) { 17 | float shadowStart = 0.25; 18 | float bumpFalloff = clamp(vNdotL / shadowStart, 0.0, 0.5); 19 | 20 | float bumpScale = mix( 21 | 0.005, 22 | 0.05, 23 | vNdotL * vNdotL * vNdotV 24 | ) * bumpFalloff; 25 | 26 | if (landness < 0.5) { 27 | bumpScale *= -oceanFactor; 28 | } 29 | 30 | return bumpScale; 31 | } 32 | -------------------------------------------------------------------------------- /app/shaders/functions/texture2DCubic.glsl: -------------------------------------------------------------------------------- 1 | // Based on: 2 | // http://stackoverflow.com/questions/13501081/efficient-bicubic-filtering-code-in-glsl 3 | 4 | vec4 cubic(float v) { 5 | vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v; 6 | vec4 s = n * n * n; 7 | float x = s.x; 8 | float y = s.y - 4.0 * s.x; 9 | float z = s.z - 4.0 * s.y + 6.0 * s.x; 10 | float w = 6.0 - x - y - z; 11 | return vec4(x, y, z, w); 12 | } 13 | 14 | vec4 texture2DCubic( 15 | sampler2D tex, 16 | vec2 uv, 17 | vec2 textureResolution 18 | ) { 19 | vec2 texcoord = uv * textureResolution; 20 | texcoord -= vec2(0.5); 21 | float fx = fract(texcoord.x); 22 | float fy = fract(texcoord.y); 23 | texcoord.x -= fx; 24 | texcoord.y -= fy; 25 | 26 | vec4 xcubic = cubic(fx); 27 | vec4 ycubic = cubic(fy); 28 | 29 | vec4 c = vec4( 30 | texcoord.x - 0.5, 31 | texcoord.x + 1.5, 32 | texcoord.y - 0.5, 33 | texcoord.y + 1.5 34 | ); 35 | 36 | vec4 s = vec4( 37 | xcubic.x + xcubic.y, 38 | xcubic.z + xcubic.w, 39 | ycubic.x + ycubic.y, 40 | ycubic.z + ycubic.w 41 | ); 42 | 43 | vec4 offset = c + vec4( 44 | xcubic.y, 45 | xcubic.w, 46 | ycubic.y, 47 | ycubic.w 48 | ) / s; 49 | 50 | vec4 sample0 = texture2D(tex, 51 | vec2(offset.x, offset.z) / textureResolution); 52 | 53 | vec4 sample1 = texture2D(tex, 54 | vec2(offset.y, offset.z) / textureResolution); 55 | 56 | vec4 sample2 = texture2D(tex, 57 | vec2(offset.x, offset.w) / textureResolution); 58 | 59 | vec4 sample3 = texture2D(tex, 60 | vec2(offset.y, offset.w) / textureResolution); 61 | 62 | float sx = s.x / (s.x + s.y); 63 | float sy = s.z / (s.z + s.w); 64 | 65 | return mix( 66 | mix(sample3, sample2, sx), 67 | mix(sample1, sample0, sx), 68 | sy 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /app/shaders/functions/tonemap.glsl: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/vorg/pragmatic-pbr/blob/master/local_modules/glsl-tonemap-uncharted2/index.glsl 2 | float A = 0.15; 3 | float B = 0.50; 4 | float C = 0.10; 5 | float D = 0.20; 6 | float E = 0.02; 7 | float F = 0.30; 8 | float W = 11.2; 9 | 10 | vec3 Uncharted2Tonemap(vec3 x) { 11 | return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; 12 | } 13 | 14 | // Based on Filmic Tonemapping Operators http://filmicgames.com/archives/75 15 | vec3 tonemap(vec3 color) { 16 | float ExposureBias = 2.0; 17 | vec3 curr = Uncharted2Tonemap(ExposureBias * color); 18 | 19 | vec3 whiteScale = 1.0 / Uncharted2Tonemap(vec3(W)); 20 | return curr * whiteScale; 21 | } 22 | -------------------------------------------------------------------------------- /app/shaders/functions/transpose.glsl: -------------------------------------------------------------------------------- 1 | // based on https://github.com/glslify/glsl-transpose 2 | 3 | float transpose(float m) { 4 | return m; 5 | } 6 | 7 | mat2 transpose(mat2 m) { 8 | return mat2(m[0][0], m[1][0], 9 | m[0][1], m[1][1]); 10 | } 11 | 12 | mat3 transpose(mat3 m) { 13 | return mat3(m[0][0], m[1][0], m[2][0], 14 | m[0][1], m[1][1], m[2][1], 15 | m[0][2], m[1][2], m[2][2]); 16 | } 17 | 18 | mat4 transpose(mat4 m) { 19 | return mat4(m[0][0], m[1][0], m[2][0], m[3][0], 20 | m[0][1], m[1][1], m[2][1], m[3][1], 21 | m[0][2], m[1][2], m[2][2], m[3][2], 22 | m[0][3], m[1][3], m[2][3], m[3][3]); 23 | } 24 | -------------------------------------------------------------------------------- /app/shaders/night.frag.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | @import ./functions/gamma; 5 | @import ./functions/tonemap; 6 | @import ./functions/exposure; 7 | @import ./functions/nightAmbient; 8 | @import ./functions/texture2DCubic; 9 | 10 | uniform sampler2D topographyMap; 11 | uniform sampler2D diffuseMap; 12 | uniform sampler2D landmaskMap; 13 | uniform sampler2D bordersMap; 14 | uniform vec2 bordersMapSize; 15 | uniform sampler2D lightsMap; 16 | uniform vec3 eye; 17 | 18 | varying vec2 vUv; 19 | varying vec3 vPosition; 20 | varying vec3 vNormal; 21 | 22 | void main() { 23 | float landness = texture2D(landmaskMap, vUv, -0.25).r; 24 | float countryBorder = texture2DCubic( 25 | bordersMap, 26 | vUv, 27 | bordersMapSize 28 | ).r; 29 | 30 | float oceanDepth = (0.5 - texture2D(topographyMap, vUv).r) * 2.0; 31 | 32 | vec3 oceanColor = mix( 33 | vec3(0.0, 0.0, 0.3), 34 | vec3(0.0, 0.0, 0.35), 35 | oceanDepth 36 | ); 37 | 38 | vec3 diffuseColor = mix( 39 | oceanColor, 40 | toLinear(texture2D(diffuseMap, vUv).rgb), 41 | landness 42 | ); 43 | 44 | vec3 color = nightAmbient( 45 | -1.0, 46 | diffuseColor, 47 | texture2D(lightsMap, vUv).x, 48 | vUv 49 | ); 50 | 51 | vec3 shaded = toGamma(tonemap(color * 200.0)); 52 | vec3 final = shaded + countryBorder; 53 | gl_FragColor = vec4(final, 1.0); 54 | } 55 | -------------------------------------------------------------------------------- /app/shaders/plane.vert.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | @import ./functions/transpose; 4 | @import ./functions/inverse; 5 | 6 | attribute vec3 position; 7 | attribute vec2 texcoord; 8 | attribute float elevation; 9 | 10 | uniform mat4 model; 11 | uniform mat4 view; 12 | uniform mat4 projection; 13 | 14 | uniform float oceanElevationScale; 15 | uniform float landElevationScale; 16 | 17 | varying vec2 vUv; 18 | varying vec3 vPosition; 19 | varying vec3 vSpherePosition; 20 | varying vec3 vNormal; 21 | 22 | const float PI = 3.141592653589793; 23 | 24 | void main(void) { 25 | mat4 modelView = view * model; 26 | vUv = texcoord; 27 | 28 | float scale; 29 | if (elevation > 0.0) { 30 | scale = elevation * landElevationScale; 31 | } 32 | else { 33 | scale = elevation * oceanElevationScale; 34 | } 35 | 36 | vec3 planePosition = vec3(position.x, scale, position.z); 37 | 38 | float x = planePosition.x * PI; 39 | float z = planePosition.z * PI * 0.999; 40 | 41 | vec3 spherePosition = vec3(sin(x) * cos(z), -sin(z), cos(x) * cos(z)); 42 | 43 | gl_Position = projection * modelView * vec4(planePosition, 1.0); 44 | vPosition = vec3(model * vec4(planePosition, 1.0)); 45 | 46 | mat3 normalMatrix = transpose(inverse(mat3(model))); 47 | vSpherePosition = spherePosition; 48 | vNormal = normalize(normalMatrix * spherePosition); 49 | } 50 | -------------------------------------------------------------------------------- /app/shaders/sphere.vert.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | @import ./functions/transpose; 4 | @import ./functions/inverse; 5 | 6 | attribute vec3 position; 7 | attribute vec2 texcoord; 8 | attribute float elevation; 9 | 10 | uniform mat4 model; 11 | uniform mat4 view; 12 | uniform mat4 projection; 13 | 14 | uniform float oceanElevationScale; 15 | uniform float landElevationScale; 16 | 17 | varying vec2 vUv; 18 | varying vec3 vPosition; 19 | varying vec3 vNormal; 20 | 21 | void main(void) { 22 | mat4 modelView = view * model; 23 | vUv = texcoord; 24 | 25 | float scale = 1.0; 26 | if (elevation > 0.0) { 27 | scale += elevation * landElevationScale; 28 | } 29 | else { 30 | scale += elevation * oceanElevationScale; 31 | } 32 | 33 | vec3 spherePosition = scale * position; 34 | 35 | gl_Position = projection * modelView * vec4(spherePosition, 1.0); 36 | vPosition = vec3(model * vec4(spherePosition, 1.0)); 37 | 38 | mat3 normalMatrix = transpose(inverse(mat3(model))); 39 | vNormal = normalize(normalMatrix * spherePosition); 40 | } 41 | -------------------------------------------------------------------------------- /app/styles/_map.scss: -------------------------------------------------------------------------------- 1 | @import "range"; 2 | $thumb-color: darken(#8895b3, 20); 3 | $ui-background: #f2f2f2; 4 | 5 | @mixin antialias() { 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .loading { 11 | background: white; 12 | opacity: 0.75; 13 | position: absolute; 14 | width: 100vw; 15 | height: 100vh; 16 | top: 0; 17 | left: 0; 18 | z-index: 10; 19 | 20 | img { 21 | position: absolute; 22 | left: 50vw; 23 | top: 50vh; 24 | margin-left: -19px; 25 | margin-top: -19px; 26 | } 27 | } 28 | 29 | .map-container { 30 | height: 100vh; 31 | width: 100vw; 32 | display: flex; 33 | flex-direction: row-reverse; 34 | 35 | .map-controls { 36 | flex: 0 0 320px; 37 | font-size: 13px; 38 | overflow-y: auto; 39 | box-shadow: 0 0 3px #d3d3d3; 40 | 41 | img { height: 1.25em; } 42 | 43 | >div { 44 | padding: 8px 24px; 45 | 46 | .top-row { 47 | display: none; 48 | font-weight: bold; 49 | color: #405060; 50 | letter-spacing: 0px; 51 | margin-bottom: -4px; 52 | text-transform: uppercase; 53 | 54 | span { 55 | flex: 1; 56 | color: #6d6d6d; 57 | font-weight: normal; 58 | text-align: right; 59 | } 60 | } 61 | } 62 | 63 | .sub-group { 64 | padding: 4px 0 0 0; 65 | 66 | >div { 67 | padding: 6px 0 8px 0; 68 | } 69 | 70 | .top-row { 71 | display: flex; 72 | color: #222; 73 | text-transform: none; 74 | font-weight: normal; 75 | } 76 | } 77 | 78 | .header-row { 79 | display: flex; 80 | background: #f1f1f1; 81 | padding: 6px 10px 6px 14px; 82 | } 83 | 84 | h1 { 85 | flex: 1 1; 86 | color: desaturate($thumb-color, 10); 87 | font-size: 16px; 88 | letter-spacing: 0; 89 | margin: 0; 90 | padding: 0; 91 | 92 | img { 93 | height: 1.5em; 94 | margin: 0 3px -5px 0; 95 | } 96 | } 97 | 98 | .info { 99 | align-self: flex-end; 100 | position: relative; 101 | top: 1px; 102 | a { 103 | text-decoration: none; 104 | } 105 | } 106 | } 107 | 108 | .map-canvas { 109 | flex: 1 1 75vh; 110 | overflow: hidden; 111 | -ms-user-select: none; 112 | -webkit-user-select: none; 113 | -moz-user-select: none; 114 | user-select: none; 115 | touch-action: none; 116 | 117 | canvas { display: block; } 118 | } 119 | 120 | .range-control { 121 | @include range-styles(12px, $thumb-color); 122 | 123 | input { box-sizing: border-box; } 124 | 125 | input[type=range] { flex: 1 1 100%; } 126 | } 127 | 128 | .vertical-slider-layout { 129 | margin-right: 65px; 130 | position: relative; 131 | 132 | .vertical { 133 | position: absolute; 134 | top: 4px; 135 | right: -65px; 136 | width: 40px; 137 | padding-top: 0; 138 | 139 | input[type=range] { 140 | -webkit-transform: rotate(-90deg); 141 | transform: rotate(-90deg); 142 | position: relative; 143 | top: 24px; 144 | left: -8px; 145 | width: 62px; 146 | } 147 | 148 | .top-row { 149 | justify-content: flex-end; 150 | span { display: none; } 151 | } 152 | } 153 | } 154 | 155 | .checkbox-options { 156 | padding-top: 8px; 157 | margin-right: -8px; 158 | display: flex; 159 | 160 | .checkbox-option { 161 | flex: 1 1 50%; 162 | margin: 0 6px 6px 0; 163 | padding: 6px 8px; 164 | border-radius: 5px; 165 | background-color: $ui-background; 166 | cursor: pointer; 167 | input { display: none } 168 | 169 | &:before { 170 | content: ""; 171 | box-sizing: border-box; 172 | display: inline-block; 173 | height: 12px; 174 | width: 12px; 175 | border: 1px solid $thumb-color; 176 | border-radius: 3px; 177 | vertical-align: -1.5px; 178 | } 179 | 180 | &.active { 181 | &:before { background: $thumb-color; } 182 | } 183 | } 184 | } 185 | 186 | .radio-buttons { 187 | padding-top: 8px; 188 | 189 | .radio-button { 190 | margin: 0 0 6px 0; 191 | background: $ui-background; 192 | border-radius: 5px; 193 | overflow: hidden; 194 | 195 | >label { 196 | display: block; 197 | padding: 6px 8px; 198 | 199 | input { display: none; } 200 | } 201 | 202 | &.active { 203 | background: #8895b3; 204 | 205 | >label { 206 | @include antialias(); 207 | color: white; 208 | } 209 | 210 | >.sub-group { 211 | background: #fff; 212 | border-radius: 0 0 4px 4px; 213 | padding: 6px 16px; 214 | margin: 1px; 215 | } 216 | } 217 | 218 | &:not(.active) label:hover { 219 | background: #f2f2f2; 220 | cursor: pointer; 221 | } 222 | } 223 | } 224 | 225 | .map-controls>.debug-panel>.top-row { 226 | label, span { color: #7a7; } 227 | } 228 | } 229 | 230 | @media (max-width: 640px) { 231 | .map-container { 232 | flex-direction: column-reverse; 233 | height: auto; 234 | 235 | .map-controls { 236 | flex: 0 0 auto; 237 | @include range-styles(30px, $thumb-color); 238 | 239 | input[type=range] { margin: 18px 0 10px 0; } 240 | } 241 | 242 | .map-canvas { height: 100vw; } 243 | 244 | .hidden-mobile { display: none; } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /app/styles/_range.scss: -------------------------------------------------------------------------------- 1 | /* Generated from http://danielstern.ca/range.css/#/ */ 2 | 3 | @mixin range-styles($thumb-size, $thumb-color) { 4 | input[type=range] { 5 | -webkit-appearance: none; 6 | width: 100%; 7 | margin: 4px 0; 8 | } 9 | 10 | input[type=range]:focus { 11 | outline: none; 12 | } 13 | 14 | input[type=range]::-webkit-slider-runnable-track { 15 | width: 100%; 16 | height: 4px; 17 | cursor: pointer; 18 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 19 | background: #eeeeee; 20 | border-radius: 0px; 21 | border: 0px solid #010101; 22 | } 23 | 24 | input[type=range]::-webkit-slider-thumb { 25 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 26 | border: 0px solid #000000; 27 | height: $thumb-size; 28 | width: $thumb-size; 29 | border-radius: $thumb-size / 2; 30 | background: $thumb-color; 31 | cursor: pointer; 32 | -webkit-appearance: none; 33 | margin-top: 2px - $thumb-size / 2; 34 | } 35 | 36 | input[type=range]:focus::-webkit-slider-runnable-track { 37 | background: #ddeeff; 38 | } 39 | 40 | input[type=range]::-moz-range-track { 41 | width: 100%; 42 | height: 4px; 43 | cursor: pointer; 44 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 45 | background: #eeeeee; 46 | border-radius: 0px; 47 | border: 0px solid #010101; 48 | } 49 | input[type=range]::-moz-range-thumb { 50 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 51 | border: 0px solid #000000; 52 | height: $thumb-size; 53 | width: $thumb-size; 54 | border-radius: $thumb-size / 2; 55 | background: $thumb-color; 56 | cursor: pointer; 57 | } 58 | 59 | input[type=range]::-ms-tooltip { 60 | display: none; 61 | } 62 | 63 | input[type=range]::-ms-track { 64 | width: 100%; 65 | height: 4px; 66 | cursor: pointer; 67 | background: transparent; 68 | border-color: transparent; 69 | color: transparent; 70 | overflow: visible; 71 | } 72 | 73 | input[type=range]::-ms-fill-lower { 74 | background: #e1e1e1; 75 | border: 0px solid #010101; 76 | border-radius: 0px; 77 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 78 | } 79 | 80 | input[type=range]::-ms-fill-upper { 81 | background: #eeeeee; 82 | border: 0px solid #010101; 83 | border-radius: 0px; 84 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 85 | } 86 | 87 | input[type=range]::-ms-thumb { 88 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 89 | border: 0px solid #000000; 90 | height: $thumb-size; 91 | width: $thumb-size; 92 | border-radius: $thumb-size / 2; 93 | background: $thumb-color; 94 | margin-top: 5px - $thumb-size / 2; 95 | cursor: pointer; 96 | } 97 | 98 | input[type=range]:focus::-ms-fill-lower { 99 | background: #eeeeee; 100 | } 101 | 102 | input[type=range]:focus::-ms-fill-upper { 103 | background: #fbfbfb; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "map"; 2 | 3 | * { margin: 0; padding: 0 } 4 | 5 | body { 6 | color: #333; 7 | font-family: 'Droid Sans', Helvetica, Arial, sans-serif; 8 | line-height: 1.45; 9 | margin: 0; 10 | max-width: 100%; 11 | } 12 | 13 | p { 14 | font-size: 1rem; 15 | } 16 | 17 | h1, h2, h3 { 18 | color: #111; 19 | } 20 | 21 | h1 { 22 | letter-spacing: -0.1rem; 23 | margin-top: 3rem; 24 | } 25 | 26 | p, h3 { 27 | margin: 0.6rem 0 0 0; 28 | } 29 | 30 | h2 { 31 | letter-spacing: -0.05rem; 32 | margin: 2.5rem 0 0 0; 33 | } 34 | 35 | h3 { 36 | font-size: 1.1rem; 37 | line-height: 1.35; 38 | } 39 | 40 | dl { 41 | font-size: 1rem; 42 | margin: 0.75rem 0 0 0; 43 | } 44 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisualPerspective/globe-viewer/8309d718037aec8fbaba47bce9d426d130db415a/demo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "globe-viewer", 3 | "description": "Render the globe in different ways using WebGL", 4 | "author": "Kevin James (http://www.kwjames.com)", 5 | "version": "0.0.1", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/VisualPerspective/globe-viewer" 10 | }, 11 | "scripts": { 12 | "start": "webpack-dev-server --config webpack.dev.js", 13 | "build": "webpack --config webpack.prod.js", 14 | "make-vectors": "bash script/make-vectors.sh", 15 | "process-images": "bash script/process-images.sh" 16 | }, 17 | "dependencies": { 18 | "@babel/core": "^7.0.0-beta.44", 19 | "@babel/preset-env": "^7.6.3", 20 | "babel-loader": "^8.0.0-beta", 21 | "babel-plugin-glslify": "^2.0.0", 22 | "copy-webpack-plugin": "^5.0.4", 23 | "css-loader": "^3.2.0", 24 | "d3": "^5.1.0", 25 | "d3-dsv": "^1.0.8", 26 | "d3-geo-projection": "^2.4.0", 27 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 28 | "glsl-diffuse-lambert": "^1.0.0", 29 | "glsl-gamma": "^2.0.0", 30 | "glsl-inverse": "^1.0.0", 31 | "glsl-luma": "^1.0.1", 32 | "glsl-specular-cook-torrance": "^2.0.1", 33 | "glsl-transpose": "^1.0.0", 34 | "glslify": "^7.0.0", 35 | "hammerjs": "^2.0.8", 36 | "lodash": "^4.13.1", 37 | "moment": "^2.13.0", 38 | "ndjson-cli": "^0.3.1", 39 | "node-sass": "^4.8.3", 40 | "numeral": "^2.0.6", 41 | "platform": "^1.3.1", 42 | "raw-loader": "^3.1.0", 43 | "sass-loader": "^8.0.0", 44 | "shapefile": "^0.6.6", 45 | "stats.js": "^0.17.0", 46 | "topojson": "^3.0.2", 47 | "twgl.js": "^4.4.0", 48 | "vue": "^2.0.0-beta.6", 49 | "webpack": "^4.6.0", 50 | "webpack-dev-server": "^3.1.3", 51 | "webpack-glsl-loader": "^1.0.1", 52 | "webpack-merge": "^4.1.3", 53 | "world-atlas": "^2.0.2" 54 | }, 55 | "devDependencies": { 56 | "webpack-cli": "^3.3.9" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /script/make-vectors.sh: -------------------------------------------------------------------------------- 1 | # based on https://github.com/cambecc/earth 2 | mkdir -p tmpdata 3 | cd tmpdata 4 | 5 | # land 6 | if [ ! -f ne_50m_land.zip ]; then 7 | curl -L "http://naciscdn.org/naturalearth/50m/physical/ne_50m_land.zip" -o ne_50m_land.zip 8 | fi 9 | unzip -o ne_50m_land.zip 10 | 11 | # lakes 12 | if [ ! -f ne_50m_lakes.zip ]; then 13 | curl -L "http://naciscdn.org/naturalearth/50m/physical/ne_50m_lakes.zip" -o ne_50m_lakes.zip 14 | fi 15 | unzip -o ne_50m_lakes.zip 16 | 17 | # rivers 18 | if [ ! -f ne_50m_rivers_lake_centerlines_scale_rank.zip ]; then 19 | curl -L "http://naciscdn.org/naturalearth/50m/physical/ne_50m_rivers_lake_centerlines_scale_rank.zip" -o ne_50m_rivers_lake_centerlines_scale_rank.zip 20 | fi 21 | unzip -o ne_50m_rivers_lake_centerlines_scale_rank.zip 22 | 23 | 24 | # countries 25 | if [ ! -f ne_50m_admin_0_countries.zip ]; then 26 | curl -L "http://naciscdn.org/naturalearth/50m/cultural/ne_50m_admin_0_countries.zip" -o ne_50m_admin_0_countries.zip 27 | fi 28 | unzip -o ne_50m_admin_0_countries.zip 29 | 30 | mkdir -p ../app/assets/data/ 31 | 32 | geo2topo -q 1e5 -n\ 33 | countries=<( \ 34 | shp2json -n ne_50m_admin_0_countries.shp \ 35 | | ndjson-map 'i = d.properties.iso_n3, d.id = i === "-99" ? undefined : i, delete d.properties, d' \ 36 | | geostitch -n \ 37 | ) \ 38 | land=<( \ 39 | shp2json -n ne_50m_land.shp \ 40 | | geostitch -n \ 41 | ) \ 42 | lakes=<( \ 43 | shp2json -n ne_50m_lakes.shp \ 44 | | geostitch -n \ 45 | ) \ 46 | rivers=<( \ 47 | shp2json -n ne_50m_rivers_lake_centerlines_scale_rank.shp \ 48 | | geostitch -n \ 49 | ) \ 50 | > ../app/assets/data/vectors.json 51 | 52 | -------------------------------------------------------------------------------- /script/process-images.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | mkdir -p app/assets/data 4 | mkdir -p tmpdata 5 | cd tmpdata 6 | 7 | # Lights 8 | if [ ! -f nightearth.jpg ]; then 9 | curl -L "https://www.nasa.gov/specials/blackmarble/2016/globalmaps/BlackMarble_2016_3km_gray.jpg" -o nightearth.jpg 10 | fi 11 | convert nightearth.jpg -resize 4096x4096\! lights-4096.png 12 | cp lights-4096.png ../app/assets/data/ 13 | 14 | 15 | # Color data 16 | 17 | if [ ! -f HYP_HR.zip ]; then 18 | curl -L "http://naciscdn.org/naturalearth/10m/raster/HYP_HR.zip" -o HYP_HR.zip 19 | fi 20 | unzip -o HYP_HR.zip 21 | 22 | convert HYP_HR/HYP_HR.tif -resize 4096x4096\! -fx "u * 1.2 - 0.2" -quality 90 color-4096.jpg 23 | cp color-4096.jpg ../app/assets/data/ 24 | 25 | 26 | # Heightmaps 27 | 28 | if [ ! -f gebco_08_rev_elev_21600x10800.png ]; then 29 | curl -L "https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73934/gebco_08_rev_elev_21600x10800.png" -o gebco_08_rev_elev_21600x10800.png 30 | fi 31 | 32 | if [ ! -f gebco_08_rev_bath_21600x10800.png ]; then 33 | curl -L "https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73963/gebco_08_rev_bath_21600x10800.png" -o gebco_08_rev_bath_21600x10800.png 34 | fi 35 | 36 | # Convert to 16 bit 37 | convert gebco_08_rev_elev_21600x10800.png -resize 4096x4096\! -compress RLE -depth 16 -type Grayscale -define tiff:bits-per-sample topo-16bit.tiff 38 | convert gebco_08_rev_bath_21600x10800.png -resize 4096x4096\! -compress RLE -depth 16 -type Grayscale -define tiff:bits-per-sample bathy-16bit.tiff 39 | 40 | # Change topo and bathy levels to map to one continuous ramp. 41 | # Highest point, everest is 8838 meters above sea level. 42 | # Lowest, Challenger Deep is 11034 meters below sea level. 43 | # Scale such that sea level is 0.5, meaning Everest will be 0.9 44 | # (11034 + 8838) / (11034 * 2) = 0.9 45 | convert topo-16bit.tiff +level 50%,90% topo-16bit.tiff 46 | convert bathy-16bit.tiff +level 0,50% bathy-16bit.tiff 47 | 48 | convert topo-16bit.tiff bathy-16bit.tiff \ 49 | -fx "abs(0.5 - u) > abs(0.5 - v) ? u : v" \ 50 | topo-bathy-16bit.png 51 | 52 | convert topo-bathy-16bit.png -depth 8 -quality 95 topo-bathy-4096.jpg 53 | convert topo-bathy-16bit.png -resize 128x128\! -depth 8 -quality 95 topo-bathy-128.jpg 54 | 55 | cp topo-bathy-4096.jpg ../app/assets/data/ 56 | cp topo-bathy-128.jpg ../app/assets/data/ 57 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: './app/initialize.js', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.js$/, 11 | exclude: /node_modules/, 12 | use: { 13 | loader: 'babel-loader', 14 | options: { 15 | presets: ['@babel/preset-env'] 16 | } 17 | } 18 | }, 19 | { 20 | test: /\.(glsl|frag|vert)$/, 21 | exclude: /node_modules/, 22 | loader: 'webpack-glsl-loader' 23 | }, 24 | { 25 | test: /\.scss$/, 26 | use: ExtractTextPlugin.extract({ 27 | use: [{ 28 | loader: "css-loader" 29 | }, { 30 | loader: "sass-loader" 31 | }] 32 | }) 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new CopyWebpackPlugin([{ from: 'app/assets', to: '.' }]), 38 | new ExtractTextPlugin('app.css') 39 | ], 40 | output: { 41 | filename: 'bundle.js', 42 | path: path.resolve(__dirname, 'dist') 43 | }, 44 | resolve: { 45 | modules: ['node_modules', 'app'], 46 | extensions: ['.js', '.json', '.glsl'] 47 | }, 48 | devServer: { 49 | contentBase: './dist/', 50 | port: 3333 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'inline-source-map', 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | devtool: 'source-map', 7 | }); 8 | --------------------------------------------------------------------------------