Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map3d/src/sky.wgsl

// **Skybox / atmosphere** (T3.2) — the background is no longer the flat fog
// clear-colour. A single fullscreen-triangle fragment shader paints an analytic
// gradient sky (warm sun-tinted horizon → cool zenith) plus a bright HDR **sun
// disc + glow**, rendered FIRST into the shared `Rgba16Float` HDR offscreen so the
// existing bloom + ACES tonemap catch the sun. The mesh + water then draw on top.
//
// Per pixel we reconstruct the true world-space camera ray from `inverse(view·
// projection)`: NDC → near & far world points → ray direction. The sky is then a
// pure function of that direction + the injected `SunSky` model, so it is fully
// deterministic (FC-7: no clock read here; sun motion rides the injected `SunSky`).

struct SkyUniforms {
    inv_view_proj: mat4x4<f32>, // inverse(camera view·projection): NDC → world
    eye:        vec4<f32>,      // world-space eye (xyz)
    sun_dir:    vec4<f32>,      // surface→sun dir (xyz) + sun-disc intensity (w)
    sun_color:  vec4<f32>,      // sun colour (rgb) + sun-glow intensity (w)
    sky_color:  vec4<f32>,      // zenith tint (rgb) + horizon blend bias (w)
    horizon:    vec4<f32>,      // horizon/haze colour (rgb) + ground darkening (w)
    lut:        vec4<f32>,      // x: LUT-on flag; y: viewer altitude (0..1); z,w reserved
};

@group(0) @binding(0) var<uniform> S: SkyUniforms;
// **Physical-sky transmittance LUT** (FLIGHT-SIM #7). The flight crate's GPU
// scattering-LUT compute pass precomputes atmospheric transmittance-to-sun as an
// Rgba32Float texture (μ across width, altitude up height); when `S.lut.x > 0.5`
// the sun disc/glow is multiplied by the sampled transmittance so the sun reddens
// physically at low elevation. When off, a 1×1 all-ones default is bound and the
// analytic tint below is used unchanged (the CPU-fallback / embeddable path). The
// texture is Rgba32Float (not filterable) → a NON-filtering (nearest) sampler.
@group(0) @binding(1) var lut_tex: texture_2d<f32>;
@group(0) @binding(2) var lut_samp: sampler;

struct SkyOut {
    @builtin(position) clip: vec4<f32>,
    @location(0) ndc: vec2<f32>,   // [-1,1] device coords (== clip.xy at w=1)
};

@vertex
fn sky_vs(@builtin(vertex_index) vi: u32) -> SkyOut {
    // Fullscreen triangle (covers [-1,1]²); its clip xy ARE the NDC the rasterizer
    // walks, so feeding inv_view_proj reconstructs rays matching the mesh pass — no
    // manual y-flip needed (the matrix carries the convention).
    var p = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0),
    );
    var out: SkyOut;
    let xy = p[vi];
    out.clip = vec4<f32>(xy, 0.0, 1.0);
    out.ndc = xy;
    return out;
}

// Reconstruct the world-space view ray direction for an NDC pixel: unproject the
// near (z=0) and far (z=1, wgpu clip-z) points and take their difference.
fn ray_dir(ndc: vec2<f32>) -> vec3<f32> {
    let near = S.inv_view_proj * vec4<f32>(ndc, 0.0, 1.0);
    let far = S.inv_view_proj * vec4<f32>(ndc, 1.0, 1.0);
    let wn = near.xyz / near.w;
    let wf = far.xyz / far.w;
    return normalize(wf - wn);
}

// The analytic sky radiance along a world direction `dir` — gradient + sun disc +
// glow. Shared shape with the water reflection fallback (`water.wgsl::sky_radiance`)
// so the lake mirrors the same sky model.
fn sky_radiance(dir: vec3<f32>) -> vec3<f32> {
    let l = normalize(S.sun_dir.xyz);
    // ── Vertical gradient: horizon (warm haze) → zenith (cool sky). Bias toward the
    // horizon band so the warm haze reads as a thick atmosphere, not a hard line.
    let up = clamp(dir.y, -1.0, 1.0);
    let bias = clamp(S.sky_color.w, 0.05, 4.0);
    let t = pow(clamp(up, 0.0, 1.0), bias);
    var col = mix(S.horizon.rgb, S.sky_color.rgb, t);
    // Below the horizon: darken toward a ground-haze so a low camera still reads as
    // "looking down at haze", not a mirrored sky.
    let below = clamp(-up * 3.0, 0.0, 1.0);
    col = mix(col, S.horizon.rgb * S.horizon.w, below);

    // ── Sun disc + glow. cos(angle) between the ray and the sun; a tight smoothstep
    // core is the disc, a wide pow() falloff is the atmospheric glow. The disc is
    // HDR (≫1.0) so the shared bloom pass blooms it and the ACES tonemap rolls it to
    // near-white — a real game-engine sun, not a clipped white dot.
    let cosang = dot(dir, l);
    // Disc core: ~0.6° radius (cos near 1.0). Soft edge for a clean anti-aliased rim.
    let disc = smoothstep(0.99986, 0.99994, cosang);
    // Broad glow halo around the sun (sky scattering toward the sun).
    let glow = pow(clamp(cosang, 0.0, 1.0), 350.0);
    // A faint, very wide forward-scatter brightening near the sun side of the sky.
    let haze_glow = pow(clamp(cosang * 0.5 + 0.5, 0.0, 1.0), 4.0) * 0.18;

    let sun_disc_i = max(S.sun_dir.w, 0.0);
    let sun_glow_i = max(S.sun_color.w, 0.0);
    // ── Sun tint: analytic by default; when the physical-sky LUT is bound, multiply
    // by the atmospheric transmittance TOWARD THE SUN (a function of the sun's
    // elevation μ = l·up and the viewer altitude), so the sun reddens as it sets —
    // the on-screen result of the GPU scattering-LUT pass (FLIGHT-SIM #7).
    var sun_tint = S.sun_color.rgb;
    if (S.lut.x > 0.5) {
        let mu_sun = clamp(l.y, -1.0, 1.0);            // sun elevation cosine (l·up)
        let u = clamp(mu_sun * 0.5 + 0.5, 0.0, 1.0);   // μ ∈ [-1,1] → LUT width
        let v = clamp(S.lut.y, 0.0, 1.0);              // viewer altitude → LUT height
        let trans = textureSampleLevel(lut_tex, lut_samp, vec2<f32>(u, v), 0.0).rgb;
        sun_tint = sun_tint * trans;
    }
    col = col + sun_tint * (disc * sun_disc_i + glow * sun_glow_i + haze_glow);
    return col;
}

@fragment
fn sky_fs(in: SkyOut) -> @location(0) vec4<f32> {
    let dir = ray_dir(in.ndc);
    // HDR linear radiance out (matches the mesh/water passes): the post stack
    // (bright-pass → bloom → ACES) exposes + tonemaps the Rgba16Float offscreen.
    return vec4<f32>(sky_radiance(dir), 1.0);
}