import { Color } from "./Color" import type { Framebuffer } from "./Framebuffer" import { Camera } from "../scene/Camera" import { Vec3 } from "../math/Vec3" /** One procedural cloud layer. For now a single cumulus type; add more kinds * later by giving this a `kind` field and branching in the cloud shader. */ export type CloudLayer = { color: Color /** Roughly the fraction of sky covered, 0..1. */ coverage: number /** Puff size: larger = smaller, busier clouds. */ scale: number /** Scroll speed (wind), in noise units per second. */ speed: number /** Edge softness: small = crisp cumulus rims, large = hazy. */ edge: number } /** Procedural sky: a vertical gradient, a sun disc, and optional moving clouds. */ export type SkyConfig = { zenith: Color horizon: Color sun: Color /** World-space direction toward the sun (need not be normalized). */ sunDir: Vec3 /** Angular radius of the sun's core, in radians. */ sunSize: number clouds: CloudLayer | null } const UP: Vec3 = { x: 0, y: 1, z: 0 } export namespace Sky { /** * Fill the whole framebuffer with the sky and reset depth to 0. Run first each * frame in place of Framebuffer.clear; opaque geometry then overwrites the sky * wherever it is nearer. `time` (seconds) drives cloud motion. * * Per pixel it reconstructs the view ray from the camera basis, shades a * horizon->zenith gradient by the ray's elevation, brightens toward `sun` near * `sunDir`, then lays crisp-edged cumulus over the top. */ export function render(fb: Framebuffer, camera: Camera, sky: SkyConfig, time: number): void { const { width, height, color, depth } = fb const forward = Camera.forward(camera) const right = Vec3.normalize(Vec3.cross(forward, UP)) const up = Vec3.cross(right, forward) const tanY = Math.tan(camera.fov / 2) const tanX = tanY * (width / height) const sun = Vec3.normalize(sky.sunDir) const cosSun = Math.cos(sky.sunSize) const clouds = sky.clouds for (let y = 0; y < height; y++) { const ndcY = 1 - ((y + 0.5) / height) * 2 for (let x = 0; x < width; x++) { const ndcX = ((x + 0.5) / width) * 2 - 1 // View ray = forward + right*ndcX*tanX + up*ndcY*tanY, then normalized. let dx = forward.x + right.x * ndcX * tanX + up.x * ndcY * tanY let dy = forward.y + right.y * ndcX * tanX + up.y * ndcY * tanY let dz = forward.z + right.z * ndcX * tanX + up.z * ndcY * tanY const inv = 1 / Math.hypot(dx, dy, dz) dx *= inv dy *= inv dz *= inv // dy is the ray elevation: 0 at the horizon, 1 straight up. const t = Math.max(0, Math.min(1, dy)) let c = Color.lerp(sky.horizon, sky.zenith, t) const facing = dx * sun.x + dy * sun.y + dz * sun.z if (facing > cosSun) { const glow = Math.min(1, ((facing - cosSun) / (1 - cosSun)) * 1.5) c = Color.lerp(c, sky.sun, glow) } if (clouds !== null && dy > 0.02) { const cover = cumulus(dx, dy, dz, clouds, time) if (cover > 0) { c = Color.lerp(c, clouds.color, cover) } } const i = y * width + x color[i] = c depth[i] = 0 } } } /** * Coverage in 0..1 of a cumulus layer along a view ray. The ray is projected * onto a flat cloud plane "at infinity" (xz / y), scrolled by wind, sampled * with fractal noise, then hard-thresholded so the clouds have distinct puffy * edges rather than a foggy falloff. Fades out near the horizon, where the * projection blows up into noise. */ function cumulus(dx: number, dy: number, dz: number, layer: CloudLayer, time: number): number { const u = (dx / dy) * layer.scale + time * layer.speed const v = (dz / dy) * layer.scale const density = fbm(u, v) const threshold = 0.72 - layer.coverage * 0.4 const cover = smoothstep(threshold - layer.edge, threshold + layer.edge, density) return cover * smoothstep(0.02, 0.22, dy) } /** Fractal (value-noise) sum, ~0..1, giving lumpy cumulus shapes. */ function fbm(x: number, y: number): number { let sum = 0 let amplitude = 0.5 let frequency = 1 for (let octave = 0; octave < 4; octave++) { sum += amplitude * valueNoise(x * frequency, y * frequency) frequency *= 2 amplitude *= 0.5 } return sum } function valueNoise(x: number, y: number): number { const xi = Math.floor(x) const yi = Math.floor(y) const xf = x - xi const yf = y - yi const u = xf * xf * (3 - 2 * xf) const v = yf * yf * (3 - 2 * yf) const a = hash(xi, yi) const b = hash(xi + 1, yi) const c = hash(xi, yi + 1) const d = hash(xi + 1, yi + 1) return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v } /** Deterministic 0..1 hash of an integer lattice point. */ function hash(x: number, y: number): number { let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263)) | 0 h = Math.imul(h ^ (h >>> 13), 1274126177) return ((h ^ (h >>> 16)) >>> 0) / 4294967295 } function smoothstep(a: number, b: number, x: number): number { const t = Math.max(0, Math.min(1, (x - a) / (b - a || 1e-4))) return t * t * (3 - 2 * t) } }