three-globe
Version:
Globe data visualization as a ThreeJS reusable 3D object
168 lines (147 loc) • 5.47 kB
HTML
<head>
<script type="importmap">{ "imports": {
"three": "https://esm.sh/three",
"three/": "https://esm.sh/three/"
}}</script>
<!-- <script type="module"> import * as THREE from 'three'; window.THREE = THREE;</script>-->
<!-- <script src="../../dist/three-globe.js" defer></script>-->
<style>
body { margin: 0; }
#time {
position: absolute;
bottom: 8px;
left: 8px;
color: lightblue;
font-family: monospace;
}
</style>
</head>
<body>
<div id="globeViz"></div>
<div id="time"></div>
<script type="module">
import ThreeGlobe from 'https://esm.sh/three-globe?external=three';
import * as THREE from 'https://esm.sh/three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js?external=three';
import * as solar from 'https://esm.sh/solar-calculator';
const VELOCITY = 1; // minutes per frame
// Custom shader: Blends night and day images to simulate day/night cycle
const dayNightShader = {
vertexShader: `
varying vec3 vNormal;
varying vec2 vUv;
void main() {
vNormal = normalize(normalMatrix * normal);
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
#define PI 3.141592653589793
uniform sampler2D dayTexture;
uniform sampler2D nightTexture;
uniform vec2 sunPosition;
uniform vec2 globeRotation;
varying vec3 vNormal;
varying vec2 vUv;
float toRad(in float a) {
return a * PI / 180.0;
}
vec3 Polar2Cartesian(in vec2 c) { // [lng, lat]
float theta = toRad(90.0 - c.x);
float phi = toRad(90.0 - c.y);
return vec3( // x,y,z
sin(phi) * cos(theta),
cos(phi),
sin(phi) * sin(theta)
);
}
void main() {
float invLon = toRad(globeRotation.x);
float invLat = -toRad(globeRotation.y);
mat3 rotX = mat3(
1, 0, 0,
0, cos(invLat), -sin(invLat),
0, sin(invLat), cos(invLat)
);
mat3 rotY = mat3(
cos(invLon), 0, sin(invLon),
0, 1, 0,
-sin(invLon), 0, cos(invLon)
);
vec3 rotatedSunDirection = rotX * rotY * Polar2Cartesian(sunPosition);
float intensity = dot(normalize(vNormal), normalize(rotatedSunDirection));
vec4 dayColor = texture2D(dayTexture, vUv);
vec4 nightColor = texture2D(nightTexture, vUv);
float blendFactor = smoothstep(-0.1, 0.1, intensity);
gl_FragColor = mix(nightColor, dayColor, blendFactor);
}
`
};
const sunPosAt = dt => {
const day = new Date(+dt).setUTCHours(0, 0, 0, 0);
const t = solar.century(dt);
const longitude = (day - dt) / 864e5 * 360 - 180;
return [longitude - solar.equationOfTime(t) / 4, solar.declination(t)];
};
let dt = +new Date();
const timeEl = document.getElementById('time');
const Globe = new ThreeGlobe();
let globeMaterial;
Promise.all([
new THREE.TextureLoader().loadAsync('//cdn.jsdelivr.net/npm/three-globe/example/img/earth-day.jpg'),
new THREE.TextureLoader().loadAsync('//cdn.jsdelivr.net/npm/three-globe/example/img/earth-night.jpg')
]).then(([dayTexture, nightTexture]) => {
const material = globeMaterial = new THREE.ShaderMaterial({
uniforms: {
dayTexture: { value: dayTexture },
nightTexture: { value: nightTexture },
sunPosition: { value: new THREE.Vector2() },
globeRotation: { value: new THREE.Vector2() }
},
vertexShader: dayNightShader.vertexShader,
fragmentShader: dayNightShader.fragmentShader
});
Globe.globeMaterial(material);
requestAnimationFrame(() =>
(function animate() {
// animate time of day
dt += VELOCITY * 60 * 1000;
timeEl.textContent = new Date(dt).toLocaleString();
material.uniforms.sunPosition.value.set(...sunPosAt(dt));
requestAnimationFrame(animate);
})()
);
});
// Setup renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('globeViz').appendChild(renderer.domElement);
// Setup scene
const scene = new THREE.Scene();
scene.add(Globe);
scene.add(new THREE.AmbientLight(0xcccccc, Math.PI));
scene.add(new THREE.DirectionalLight(0xffffff, 0.6 * Math.PI));
// Setup camera
const camera = new THREE.PerspectiveCamera();
camera.aspect = window.innerWidth/ window.innerHeight;
camera.updateProjectionMatrix();
camera.position.z = 500;
// Add camera controls
const obControls = new OrbitControls(camera, renderer.domElement);
obControls.minDistance = 101;
obControls.addEventListener('change', () => {
// Update globe rotation on shader
const { lng, lat } = Globe.toGeoCoords(camera.position);
globeMaterial?.uniforms.globeRotation.value.set(lng, lat);
});
// Kick-off renderer
(function animate() { // IIFE
// Frame cycle
obControls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
})();
</script>
</body>