rustial-renderer-wgpu 0.0.1

Pure WGPU renderer for the rustial 2.5D map engine
Documentation
// Sky / atmosphere shader.
//
// Rayleigh + Mie single-scattering with sun disk, rendered as a
// fullscreen triangle at the far plane (depth = 1.0).
//
// Based on the Mapbox GL JS skybox_capture scattering model
// (Bruneton & Neyret 2008) adapted for WGSL.

// ---------------------------------------------------------------------------
// Uniforms
// ---------------------------------------------------------------------------

struct SkyUniforms {
    /// Inverse view-projection matrix (clip → world).
    inv_view_proj: mat4x4<f32>,
    /// Sun direction (unit, toward sun).  w = sun_intensity.
    sun_dir:       vec4<f32>,
    /// Rayleigh tint (linear RGB).  w = sky_enabled (1 or 0).
    rayleigh_tint: vec4<f32>,
    /// Mie tint (linear RGB).  w = opacity.
    mie_tint:      vec4<f32>,
};

@group(0) @binding(0) var<uniform> u: SkyUniforms;

// ---------------------------------------------------------------------------
// Physical constants (Earth-like atmosphere)
// ---------------------------------------------------------------------------

const PI: f32             = 3.14159265;
const PLANET_RADIUS: f32  = 6360000.0;  // m
const ATMOS_RADIUS: f32   = 6420000.0;  // m
const H_R: f32            = 8000.0;     // Rayleigh scale height (m)
const H_M: f32            = 1200.0;     // Mie scale height (m)
const BETA_R: vec3<f32>   = vec3(5.5e-6, 13.0e-6, 22.4e-6); // Rayleigh β at sea level
const BETA_M: vec3<f32>   = vec3(21.0e-6, 21.0e-6, 21.0e-6); // Mie β
const MIE_G: f32          = 0.76;       // Mie asymmetry parameter

const PRIMARY_STEPS: i32  = 12;
const SHADOW_STEPS: i32   = 4;

// ---------------------------------------------------------------------------
// Vertex (fullscreen triangle)
// ---------------------------------------------------------------------------

struct VsOut {
    @builtin(position) clip_position: vec4<f32>,
    @location(0)       ndc:           vec2<f32>,
};

@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
    // Three-vertex fullscreen triangle covering [-1,1]².
    let x = f32(i32(vi) / 2) * 4.0 - 1.0;
    let y = f32(i32(vi) % 2) * 4.0 - 1.0;
    var out: VsOut;
    out.clip_position = vec4(x, y, 1.0, 1.0); // depth = 1.0 (far plane)
    out.ndc = vec2(x, y);
    return out;
}

// ---------------------------------------------------------------------------
// Ray-sphere intersection
// ---------------------------------------------------------------------------

/// Returns the distance along `dir` from `origin` to the first
/// intersection with a sphere of the given radius centered at the
/// world origin.  Returns -1 if no intersection.
fn ray_sphere(origin: vec3<f32>, dir: vec3<f32>, radius: f32) -> f32 {
    let a = dot(dir, dir);
    let b = 2.0 * dot(dir, origin);
    let c = dot(origin, origin) - radius * radius;
    let disc = b * b - 4.0 * a * c;
    if disc < 0.0 { return -1.0; }
    return (-b + sqrt(disc)) / (2.0 * a);
}

// ---------------------------------------------------------------------------
// Scattering
// ---------------------------------------------------------------------------

fn atmosphere(ray_dir: vec3<f32>, sun_dir: vec3<f32>, sun_intensity: f32,
              rayleigh_tint: vec3<f32>, mie_tint: vec3<f32>) -> vec3<f32> {
    // Camera on planet surface.
    let origin = vec3(0.0, PLANET_RADIUS + 1.0, 0.0);

    let t_max = ray_sphere(origin, ray_dir, ATMOS_RADIUS);
    if t_max < 0.0 { return vec3(0.0); }

    let ds = t_max / f32(PRIMARY_STEPS);
    var total_r = vec3(0.0, 0.0, 0.0);
    var total_m = vec3(0.0, 0.0, 0.0);
    var opt_r: f32 = 0.0;
    var opt_m: f32 = 0.0;

    for (var i: i32 = 0; i < PRIMARY_STEPS; i++) {
        let t = (f32(i) + 0.5) * ds;
        let pos = origin + ray_dir * t;
        let h = length(pos) - PLANET_RADIUS;

        let rho_r = exp(-h / H_R) * ds;
        let rho_m = exp(-h / H_M) * ds;
        opt_r += rho_r;
        opt_m += rho_m;

        // Secondary ray toward sun for shadow optical depth.
        let t_sun = ray_sphere(pos, sun_dir, ATMOS_RADIUS);
        let ds_sun = t_sun / f32(SHADOW_STEPS);
        var opt_r_sun: f32 = 0.0;
        var opt_m_sun: f32 = 0.0;

        for (var j: i32 = 0; j < SHADOW_STEPS; j++) {
            let t_s = (f32(j) + 0.5) * ds_sun;
            let pos_s = pos + sun_dir * t_s;
            let h_s = length(pos_s) - PLANET_RADIUS;
            opt_r_sun += exp(-h_s / H_R) * ds_sun;
            opt_m_sun += exp(-h_s / H_M) * ds_sun;
        }

        let tau = BETA_R * (opt_r + opt_r_sun) + BETA_M * 1.1 * (opt_m + opt_m_sun);
        let atten = exp(-tau);

        total_r += rho_r * atten;
        total_m += rho_m * atten;
    }

    // Phase functions.
    let cos_theta = dot(ray_dir, sun_dir);
    let cos2 = cos_theta * cos_theta;

    // Rayleigh phase: 3/(16π) · (1 + cos²θ)
    let phase_r = 3.0 / (16.0 * PI) * (1.0 + cos2);

    // Mie phase: Henyey-Greenstein
    let g2 = MIE_G * MIE_G;
    let phase_m = 3.0 / (8.0 * PI) * ((1.0 - g2) * (1.0 + cos2))
                  / ((2.0 + g2) * pow(1.0 + g2 - 2.0 * MIE_G * cos_theta, 1.5));

    let scatter = sun_intensity * (
        phase_r * BETA_R * total_r * rayleigh_tint +
        phase_m * BETA_M * total_m * mie_tint
    );
    return scatter;
}

// ---------------------------------------------------------------------------
// Tone mapping (Uncharted 2 filmic)
// ---------------------------------------------------------------------------

fn uncharted2_partial(x: vec3<f32>) -> vec3<f32> {
    let A = 0.15; let B = 0.50; let C = 0.10;
    let D = 0.20; let E = 0.02; let F = 0.30;
    return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

fn tonemap(color: vec3<f32>) -> vec3<f32> {
    let exposure_bias = 2.0;
    let curr = uncharted2_partial(exposure_bias * color);
    let white_scale = vec3(1.0) / uncharted2_partial(vec3(11.2));
    return curr * white_scale;
}

// ---------------------------------------------------------------------------
// Fragment
// ---------------------------------------------------------------------------

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
    let enabled = u.rayleigh_tint.w;
    if enabled < 0.5 {
        discard;
    }

    // Reconstruct world-space ray direction from NDC.
    let clip = vec4(in.ndc.x, in.ndc.y, 1.0, 1.0);
    let world_h = u.inv_view_proj * clip;
    let world = world_h.xyz / world_h.w;
    // The camera is at the world origin for camera-relative rendering.
    let ray = normalize(world);

    let sun_dir = normalize(u.sun_dir.xyz);
    let sun_intensity = u.sun_dir.w;
    let rayleigh_tint = u.rayleigh_tint.xyz;
    let mie_tint = u.mie_tint.xyz;

    // Force ray y ≥ 0 (above horizon) with a small bias to avoid
    // artefacts when looking exactly at the horizon.
    var ray_clamped = ray;
    ray_clamped.y = max(ray_clamped.y, 0.001);
    ray_clamped = normalize(ray_clamped);

    var color = atmosphere(ray_clamped, sun_dir, sun_intensity, rayleigh_tint, mie_tint);

    // Sun disk.
    let sun_cos = dot(ray, sun_dir);
    let sun_disk = smoothstep(0.9998 - 1e-5, 0.9998 + 1e-5, sun_cos);
    color += vec3(sun_disk) * sun_intensity * 0.04;

    // Tone map + gamma.
    color = tonemap(color);
    color = pow(color, vec3(1.0 / 2.2));

    let opacity = u.mie_tint.w;
    return vec4(color, opacity);
}