// 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;
}
}