graphics 0.6.7

A 3D rendering engine for rust programs, with GUI integration
Documentation
// Screen-space ambient occlusion (SSAO) — occlusion estimation pass.
//
// Reads the 1-sample depth buffer written by the geometry prepass, reconstructs
// world-space positions and normals per-pixel, then samples a fixed 16-point
// hemisphere kernel to estimate how much of the hemisphere is occluded by nearby
// geometry.
//
// The kernel is rotated by one of 16 angles picked from a 4×4 screen-space tile.
// That trades banding for high-frequency noise, which `shader_ssao_blur.wgsl` then
// removes exactly: a 4×4 box blur spans one whole tile, so every blurred pixel
// averages all 16 rotations (16 rotations × 16 samples = 256 effective samples).
// The blur is what makes the result smooth rather than speckled, so this pass
// renders to an offscreen AO buffer and must not be composited onto the scene
// directly.
//
// Output: R8 texture where 1.0 = unoccluded, 0.0 = fully occluded.

struct SsaoUniforms {
    // Combined projection * view matrix, for projecting hemisphere samples back to screen.
    proj_view:     mat4x4<f32>,
    // Inverse of proj_view, for reconstructing world-space position from depth + NDC.
    proj_view_inv: mat4x4<f32>,
    // World-space camera position (to orient normals toward the viewer).
    cam_pos:       vec4<f32>,
    near:          f32,
    far:           f32,
    // World-space hemisphere sample radius.
    radius:        f32,
    // Self-occlusion bias, as a fraction of the linear view distance to the pixel.
    // Relative rather than absolute because depth precision degrades with distance.
    bias:          f32,
    // Output strength multiplier: higher → darker crevices.
    strength:      f32,
    _pad0:         f32,
    _pad1:         f32,
    _pad2:         f32,
}

@group(0) @binding(0) var depth_tex: texture_depth_2d;
@group(0) @binding(1) var<uniform> su: SsaoUniforms;

struct VOut {
    @builtin(position) pos: vec4<f32>, // Full-screen triangle: no vertex buffer needed.
    @location(0) ndc: vec2<f32>,       // True rasterizer-injected NDC space
}

@vertex
fn vs_ssao(@builtin(vertex_index) vi: u32) -> VOut {
    var positions = array<vec2<f32>, 3>(
        vec2<f32>(-1., -1.),
        vec2<f32>( 3., -1.),
        vec2<f32>(-1.,  3.),
    );
    let p = positions[vi];
    return VOut(vec4<f32>(p, 0., 1.), p);
}

// The 3D viewport is a sub-rect of the window when the GUI takes a side or top
// panel, and the depth texture is window-sized — so pixels outside the viewport
// hold the prepass clear value, not geometry. Recover the viewport rect from the
// rasterizer: the full-screen triangle spans NDC [-1, 1], so the NDC-per-pixel
// scale locates both edges. Returns (min_x, min_y, max_x, max_y) in window pixels,
// max exclusive.
fn viewport_bounds(pos: vec2<f32>, ndc: vec2<f32>, ndc_per_px: vec2<f32>) -> vec4<f32> {
    let a = pos + (vec2<f32>(-1.) - ndc) / ndc_per_px;
    let b = pos + (vec2<f32>( 1.) - ndc) / ndc_per_px;
    // ndc_per_px.y is negative (NDC +y is up, window +y is down), so sort the pair.
    return vec4<f32>(min(a, b), max(a, b));
}

// Clamp to the viewport rect (and to the texture, defensively), so edge taps never
// read the untouched region under the GUI panel.
fn load_depth(px: vec2<i32>, dims: vec2<i32>, vp: vec4<f32>) -> f32 {
    let lo = max(vec2<i32>(vp.xy), vec2<i32>(0));
    let hi = min(vec2<i32>(vp.zw) - 1, dims - 1);
    return textureLoad(depth_tex, clamp(px, lo, hi), 0);
}

// Non-linear perspective depth (0..1) → linear view-space distance.
fn linearize(d: f32) -> f32 {
    return su.near * su.far / (su.far - d * (su.far - su.near));
}

// Reconstruct world-space position directly from proper NDC space.
fn world_from_depth(ndc_xy: vec2<f32>, depth: f32) -> vec3<f32> {
    let ndc = vec4<f32>(ndc_xy, depth, 1.0);
    let world_h = su.proj_view_inv * ndc;
    return world_h.xyz / world_h.w;
}

// Rotate 2-D vector by angle (radians).
fn rot2(v: vec2<f32>, a: f32) -> vec2<f32> {
    let s = sin(a);
    let c = cos(a);
    return vec2<f32>(c * v.x - s * v.y, s * v.x + c * v.y);
}

@fragment
fn fs_ssao(input: VOut) -> @location(0) vec4<f32> {
    // Differentiate NDC with respect to window pixels. Constant over the triangle,
    // but derivatives are only defined in uniform control flow, so take them before
    // the sky early-out below.
    let ndc_per_px = vec2<f32>(dpdx(input.ndc.x), dpdy(input.ndc.y));

    let dims = vec2<i32>(textureDimensions(depth_tex));
    let vp   = viewport_bounds(input.pos.xy, input.ndc, ndc_per_px);
    let px   = vec2<i32>(input.pos.xy);

    let depth0 = load_depth(px, dims, vp);

    // Sky / background: nothing to occlude, return unoccluded white.
    if depth0 >= 1.0 { return vec4<f32>(1., 1., 1., 1.); }

    let pos0 = world_from_depth(input.ndc, depth0);

    // ── Normal reconstruction from neighbouring depth samples ────────────────
    // Pick the neighbour that is closest in depth on each axis (avoids artefacts
    // at depth discontinuities such as silhouettes).
    let dx = vec2<i32>(1, 0);
    let dy = vec2<i32>(0, 1);

    let depth_r = load_depth(px + dx, dims, vp);
    let depth_l = load_depth(px - dx, dims, vp);
    let depth_u = load_depth(px + dy, dims, vp);
    let depth_d = load_depth(px - dy, dims, vp);

    let use_right = abs(depth_r - depth0) < abs(depth_l - depth0);
    let use_down  = abs(depth_u - depth0) < abs(depth_d - depth0);

    let h_off = vec2<f32>(ndc_per_px.x, 0.);
    let v_off = vec2<f32>(0., ndc_per_px.y);

    let h_ndc   = select(input.ndc - h_off, input.ndc + h_off, use_right);
    let h_depth = select(depth_l, depth_r, use_right);
    let h_sign  = select(-1.0, 1.0, use_right);

    let v_ndc   = select(input.ndc - v_off, input.ndc + v_off, use_down);
    let v_depth = select(depth_d, depth_u, use_down);
    let v_sign  = select(-1.0, 1.0, use_down);

    let tang  = (world_from_depth(h_ndc, h_depth) - pos0) * h_sign;
    let btng  = (world_from_depth(v_ndc, v_depth) - pos0) * v_sign;
    var normal = normalize(cross(tang, btng));

    // Ensure normal faces the camera.
    if dot(normal, pos0 - su.cam_pos.xyz) > 0.0 { normal = -normal; }

    // ── Orthonormal TBN frame for rotating the hemisphere ────────────────────
    var tangent: vec3<f32>;
    if abs(normal.y) < 0.9 {
        tangent = normalize(cross(normal, vec3<f32>(0., 1., 0.)));
    } else {
        tangent = normalize(cross(normal, vec3<f32>(1., 0., 0.)));
    }
    let bitangent = cross(normal, tangent);

    // Kernel rotation angle, keyed to a 4×4 screen tile so the blur pass can average
    // all 16 of them back out. The golden-ratio stride keeps neighbouring pixels far
    // apart in angle both horizontally (index +1) and vertically (index +4), which is
    // what stops the tile itself from showing up as a pattern.
    let tile      = px & vec2<i32>(3, 3);
    let tile_idx  = f32(tile.y * 4 + tile.x);
    let rot_angle = fract(tile_idx * 0.6180339887) * 6.283185;

    let linear0 = linearize(depth0);

    // ── Fixed 16-point hemisphere kernel (z > 0 → away from surface) ─────────
    // Samples are intentionally non-uniform in distance (closer ones carry more
    // weight for small crevices; farther ones catch larger occluders).
    var kernel = array<vec3<f32>, 16>(
        vec3<f32>( 0.53813,  0.18508,  0.19261),
        vec3<f32>( 0.13488, -0.87838,  0.40077),
        vec3<f32>( 0.35758, -0.38415,  0.28973),
        vec3<f32>(-0.22072,  0.12715,  0.11035),
        vec3<f32>(-0.26987,  0.53448,  0.32635),
        vec3<f32>(-0.07967,  0.04402,  0.26865),
        vec3<f32>(-0.09560, -0.30776,  0.24669),
        vec3<f32>( 0.16201,  0.11422,  0.29748),
        vec3<f32>(-0.38296,  0.56558,  0.46939),
        vec3<f32>(-0.10660, -0.64196,  0.44051),
        vec3<f32>( 0.01006,  0.09939,  0.18907),
        vec3<f32>( 0.09448,  0.60592,  0.58505),
        vec3<f32>( 0.55916,  0.67533,  0.08070),
        vec3<f32>(-0.18608,  0.16559,  0.07534),
        vec3<f32>( 0.14520, -0.39572,  0.08600),
        vec3<f32>(-0.32639,  0.26070,  0.09231),
    );

    // Accelerating sample distance: i/16 lerped into i²/16² keeps short-range
    // samples dense (better for tight crevices) without losing coverage.
    var scale_sq = array<f32, 16>(
        0.0039, 0.0156, 0.0352, 0.0625,
        0.0977, 0.1406, 0.1914, 0.2500,
        0.3164, 0.3906, 0.4727, 0.5625,
        0.6602, 0.7656, 0.8789, 1.0000,
    );

    // Self-occlusion bias in linear view-space distance. A constant bias in raw
    // depth-buffer units is scale-dependent — the same delta spans a hair's width up
    // close and metres far away — which is what produced speckled acne on curved
    // surfaces. Scaling with distance tracks how depth precision actually degrades.
    let bias_linear = su.bias * linear0;

    var occlusion = 0.0;
    let num_samples = 16;

    for (var i = 0; i < num_samples; i++) {
        var s = kernel[i];

        // Rotate the in-plane (xy) components by the per-pixel random angle.
        let rxy = rot2(s.xy, rot_angle);
        s = vec3<f32>(rxy, s.z);

        // Transform from TBN to world space.
        let world_dir = tangent * s.x + bitangent * s.y + normal * s.z;

        // Accelerated radial scale: samples cluster near the surface.
        let scale = mix(0.1, 1.0, scale_sq[i]);
        let sample_world = pos0 + world_dir * su.radius * scale;

        // Project sample point into clip space.
        let clip = su.proj_view * vec4<f32>(sample_world, 1.0);
        if clip.w <= 0.0 { continue; }
        let ndc_xy        = clip.xy / clip.w;
        let sample_depth  = clip.z  / clip.w;   // depth in [0,1] for LH proj

        // Discard out-of-viewport and behind-near-plane samples.
        if any(ndc_xy < vec2<f32>(-1.0)) || any(ndc_xy > vec2<f32>(1.0)) { continue; }
        if sample_depth < 0.0 || sample_depth > 1.0 { continue; }

        // Find exactly which physical window pixel this projected coordinate maps to
        let delta_ndc = ndc_xy - input.ndc;
        let sample_px = vec2<i32>(input.pos.xy + delta_ndc / ndc_per_px);

        // Fetch geometry depth at the projected location.
        let geom_depth = load_depth(sample_px, dims, vp);

        // Range check: suppress contributions from surfaces far away in depth
        // (avoids halos at depth discontinuities).
        let linear_geom   = linearize(geom_depth);
        let linear_sample = linearize(sample_depth);
        let range_check = smoothstep(0.0, 1.0, su.radius / abs(linear0 - linear_geom));

        // Occluded when geometry is closer to the camera than the sample point.
        if linear_geom < linear_sample - bias_linear {
            occlusion += range_check;
        }
    }

    occlusion /= f32(num_samples);

    // ao = 1 (no occlusion) → multiply scene by 1 (no change).
    // ao = 0 (full occlusion) → multiply scene by 0 (black).
    let ao = 1.0 - clamp(occlusion * su.strength, 0.0, 1.0);
    return vec4<f32>(ao, ao, ao, 1.0);
}