aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/atmosphere.js
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/atmosphere.js
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/atmosphere.js')
-rw-r--r--viewer/js/atmosphere.js65
1 files changed, 65 insertions, 0 deletions
diff --git a/viewer/js/atmosphere.js b/viewer/js/atmosphere.js
new file mode 100644
index 0000000..98e5947
--- /dev/null
+++ b/viewer/js/atmosphere.js
@@ -0,0 +1,65 @@
+// atmosphere.js — sky and distance haze near the ground: colours from camera height and sun (pure) + the sky pass
+import * as THREE from 'three';
+
+export const SKY_FULL_KM = 20; // full sky below this eye altitude …
+export const SKY_NONE_KM = 100; // … fading to space above this
+export const HAZE_KM = 80; // e-folding distance of the haze
+export const HAZE_MAX = 0.85;
+const DAY = { horizon: [0.74, 0.83, 0.93], zenith: [0.2, 0.4, 0.74] };
+const NIGHT = { horizon: [0.05, 0.06, 0.1], zenith: [0.01, 0.015, 0.04] };
+const smooth = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
+const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);
+
+export function atmosphere(altKm, sunUp = null) { // sunUp: cos of the sun's zenith angle at the eye; null = Sky off (day)
+ const day = sunUp === null ? 1 : smooth(-0.15, 0.1, sunUp);
+ return { amount: 1 - smooth(SKY_FULL_KM, SKY_NONE_KM, altKm),
+ horizon: mix(NIGHT.horizon, DAY.horizon, day), zenith: mix(NIGHT.zenith, DAY.zenith, day) };
+}
+
+const VERT = /* glsl */ `
+varying vec2 vNdc;
+void main() { vNdc = position.xy; gl_Position = vec4(position.xy, 1.0, 1.0); }`;
+
+const FRAG = /* glsl */ `
+uniform mat4 invProj;
+uniform mat4 camWorld;
+uniform vec3 up;
+uniform vec3 horizon;
+uniform vec3 zenith;
+uniform vec3 space;
+uniform float amount;
+varying vec2 vNdc;
+void main() {
+ vec4 v = invProj * vec4(vNdc, 1.0, 1.0);
+ vec3 dir = normalize(mat3(camWorld) * (v.xyz / v.w));
+ vec3 sky = mix(horizon, zenith, smoothstep(0.0, 0.6, dot(dir, up)));
+ gl_FragColor = vec4(mix(space, sky, amount), 1.0);
+}`;
+
+export class SkyDome { // one screen-filling triangle drawn first: sky where no ground covers it
+ constructor(scene, spaceHex) {
+ const g = new THREE.BufferGeometry();
+ g.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3));
+ const s = [(spaceHex >> 16) & 255, (spaceHex >> 8) & 255, spaceHex & 255].map(v => v / 255);
+ this.uniforms = { invProj: { value: new THREE.Matrix4() }, camWorld: { value: new THREE.Matrix4() },
+ up: { value: new THREE.Vector3(0, 1, 0) }, horizon: { value: new THREE.Vector3() }, zenith: { value: new THREE.Vector3() },
+ space: { value: new THREE.Vector3(...s) }, amount: { value: 0 } };
+ this.mesh = new THREE.Mesh(g, new THREE.ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: this.uniforms,
+ depthTest: false, depthWrite: false }));
+ this.mesh.frustumCulled = false;
+ this.mesh.renderOrder = -1000;
+ this.mesh.visible = false;
+ scene.add(this.mesh);
+ }
+
+ update(camera, up, air) {
+ const u = this.uniforms;
+ u.invProj.value.copy(camera.projectionMatrixInverse);
+ u.camWorld.value.copy(camera.matrixWorld);
+ u.up.value.copy(up);
+ u.horizon.value.set(...air.horizon);
+ u.zenith.value.set(...air.zenith);
+ u.amount.value = air.amount;
+ this.mesh.visible = air.amount > 0;
+ }
+}