2026-08-04 02:09:54 +02:00
|
|
|
import { Color } from "./Color"
|
|
|
|
|
import type { Framebuffer } from "./Framebuffer"
|
|
|
|
|
import { Camera } from "../scene/Camera"
|
|
|
|
|
import { Vec3 } from "../math/Vec3"
|
|
|
|
|
|
2026-08-04 02:39:20 +02:00
|
|
|
/** 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. */
|
2026-08-04 02:09:54 +02:00
|
|
|
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
|
2026-08-04 02:39:20 +02:00
|
|
|
clouds: CloudLayer | null
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-04 02:39:20 +02:00
|
|
|
* wherever it is nearer. `time` (seconds) drives cloud motion.
|
2026-08-04 02:09:54 +02:00
|
|
|
*
|
|
|
|
|
* Per pixel it reconstructs the view ray from the camera basis, shades a
|
2026-08-04 02:39:20 +02:00
|
|
|
* horizon->zenith gradient by the ray's elevation, brightens toward `sun` near
|
|
|
|
|
* `sunDir`, then lays crisp-edged cumulus over the top.
|
2026-08-04 02:09:54 +02:00
|
|
|
*/
|
2026-08-04 02:39:20 +02:00
|
|
|
export function render(fb: Framebuffer, camera: Camera, sky: SkyConfig, time: number): void {
|
2026-08-04 02:09:54 +02:00
|
|
|
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)
|
2026-08-04 02:39:20 +02:00
|
|
|
const clouds = sky.clouds
|
2026-08-04 02:09:54 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-04 02:39:20 +02:00
|
|
|
if (clouds !== null && dy > 0.02) {
|
|
|
|
|
const cover = cumulus(dx, dy, dz, clouds, time)
|
|
|
|
|
if (cover > 0) {
|
|
|
|
|
c = Color.lerp(c, clouds.color, cover)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-04 02:09:54 +02:00
|
|
|
const i = y * width + x
|
|
|
|
|
color[i] = c
|
|
|
|
|
depth[i] = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-04 02:39:20 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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)
|
|
|
|
|
}
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|