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