graphics 0.6.7

A 3D rendering engine for rust programs, with GUI integration
Documentation
// SSAO blur + composite.
//
// `shader_ssao.wgsl` rotates its sample kernel by one of 16 angles chosen from a 4×4
// screen tile, so the raw AO buffer is noisy by construction — that noise is the
// price of trading a fixed kernel's banding for something a blur can integrate away.
// This pass runs a 4×4 box blur, which is exactly one tile period, so every output
// pixel averages all 16 rotations, then composites the result onto the scene with a
// multiplicative blend.
//
// The blur is depth-aware: taps whose linear depth differs from the centre by more
// than the SSAO radius are dropped, so occlusion never bleeds across a silhouette or
// out of geometry into the sky.

struct SsaoUniforms {
    proj_view:     mat4x4<f32>,
    proj_view_inv: mat4x4<f32>,
    cam_pos:       vec4<f32>,
    near:          f32,
    far:           f32,
    radius:        f32,
    bias:          f32,
    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;
@group(0) @binding(2) var ao_tex: texture_2d<f32>;

struct VOut {
    @builtin(position) pos: vec4<f32>,
    @location(0) ndc: vec2<f32>,
}

@vertex
fn vs_ssao_blur(@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);
}

// See the matching comment in shader_ssao.wgsl: the AO and depth textures are
// window-sized but only the viewport sub-rect was rendered, so taps have to stay
// inside it. 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;
    return vec4<f32>(min(a, b), max(a, b));
}

fn clamp_px(px: vec2<i32>, dims: vec2<i32>, vp: vec4<f32>) -> vec2<i32> {
    let lo = max(vec2<i32>(vp.xy), vec2<i32>(0));
    let hi = min(vec2<i32>(vp.zw) - 1, dims - 1);
    return clamp(px, lo, hi);
}

fn linearize(d: f32) -> f32 {
    return su.near * su.far / (su.far - d * (su.far - su.near));
}

// 4×4 box: offsets -2..1 on each axis cover one full noise tile.
// LO = -1 and HI = 0 produces a less smooth/more pixelated effect.
const TAP_LO: i32 = -2;
const TAP_HI: i32 =  1;

@fragment
fn fs_ssao_blur(input: VOut) -> @location(0) vec4<f32> {
    let ndc_per_px = vec2<f32>(dpdx(input.ndc.x), dpdy(input.ndc.y));
    let dims = vec2<i32>(textureDimensions(ao_tex));
    let vp   = viewport_bounds(input.pos.xy, input.ndc, ndc_per_px);
    let px   = vec2<i32>(input.pos.xy);

    let linear0 = linearize(textureLoad(depth_tex, clamp_px(px, dims, vp), 0));

    // Two surfaces further apart than the sample radius can't occlude each other, so
    // that is also the right cutoff for deciding whether their AO should be mixed. The
    // relative term keeps oblique or distant geometry — where one pixel of screen space
    // covers a lot of depth — from rejecting its own neighbours and staying noisy.
    let depth_cutoff = max(su.radius, 0.05 * linear0);

    var sum    = 0.0;
    var weight = 0.0;

    for (var y = TAP_LO; y <= TAP_HI; y++) {
        for (var x = TAP_LO; x <= TAP_HI; x++) {
            let p = clamp_px(px + vec2<i32>(x, y), dims, vp);
            let d = linearize(textureLoad(depth_tex, p, 0));
            // 1 when the tap sits on the same surface as the centre, 0 otherwise.
            let w = select(0.0, 1.0, abs(d - linear0) <= depth_cutoff);
            sum    += textureLoad(ao_tex, p, 0).r * w;
            weight += w;
        }
    }

    // The centre tap always passes its own test, so `weight` is never 0.
    let ao = sum / weight;
    return vec4<f32>(ao, ao, ao, 1.0);
}