viewport-lib 0.2.1

3D viewport rendering library
Documentation
// ssao.wgsl — screen-space ambient occlusion using hemisphere sampling.
// Reconstructs view-space position from depth, samples a rotated hemisphere kernel,
// and estimates per-pixel ambient occlusion.

struct SsaoUniform {
    inv_proj: mat4x4<f32>,  // 64 bytes — NDC+depth -> view-space position (unproject)
    proj:     mat4x4<f32>,  // 64 bytes — view-space -> clip (re-project samples)
    radius:   f32,          //  4 bytes — hemisphere sample radius in view units
    bias:     f32,          //  4 bytes — depth comparison bias (avoids self-occlusion)
    _pad:     vec2<f32>,    //  8 bytes — alignment
}

@group(0) @binding(0) var depth_tex:  texture_depth_2d;
@group(0) @binding(1) var depth_samp: sampler;
@group(0) @binding(2) var noise_tex:  texture_2d<f32>;
@group(0) @binding(3) var noise_samp: sampler;
@group(0) @binding(4) var<storage, read> kernel: array<vec4<f32>>;  // 64 hemisphere samples
@group(0) @binding(5) var<uniform> params: SsaoUniform;

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

@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
    let positions = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0),
    );
    let p = positions[vi];
    let uv = vec2<f32>((p.x + 1.0) * 0.5, (1.0 - p.y) * 0.5);
    return VertexOutput(vec4<f32>(p, 0.0, 1.0), uv);
}

// Reconstruct view-space position from a UV and a depth value.
fn view_pos_from_depth(uv: vec2<f32>, depth: f32) -> vec3<f32> {
    let ndc = vec4<f32>(uv.x * 2.0 - 1.0, -(uv.y * 2.0 - 1.0), depth, 1.0);
    let vp = params.inv_proj * ndc;
    return vp.xyz / vp.w;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let dim = vec2<f32>(textureDimensions(depth_tex));
    let pixel = vec2<i32>(i32(in.pos.x), i32(in.pos.y));

    // Load raw depth and bail on background pixels.
    let depth = textureLoad(depth_tex, pixel, 0);
    if depth >= 0.9999 {
        return vec4<f32>(1.0);
    }

    // Reconstruct the current pixel's view-space position.
    let pos_v = view_pos_from_depth(in.uv, depth);

    // Reconstruct view-space normal from position derivatives.
    let pos_dx = dpdx(pos_v);
    let pos_dy = dpdy(pos_v);
    // Swap order: cross(pos_dx, pos_dy) points into the surface in wgpu screen-space
    // (screen Y increases downward, so dpdy points in −view-Y). Swapping gives +Z (toward camera).
    let normal = normalize(cross(pos_dy, pos_dx));

    // Random rotation tangent from a tiled 4×4 noise texture.
    let noise_uv = in.uv * (dim / 4.0);
    let rnd_xy   = textureSample(noise_tex, noise_samp, noise_uv).xy * 2.0 - 1.0;
    let rnd      = vec3<f32>(rnd_xy, 0.0);
    let tangent  = normalize(rnd - normal * dot(rnd, normal));
    let bitan    = cross(normal, tangent);
    let tbn      = mat3x3<f32>(tangent, bitan, normal);

    // Accumulate occlusion from 64 hemisphere samples.
    var occlusion: f32 = 0.0;
    for (var i: i32 = 0; i < 64; i = i + 1) {
        // Rotate sample into view space.
        let sample_v = pos_v + (tbn * kernel[i].xyz) * params.radius;

        // Project sample back to screen UV.
        let sample_clip = params.proj * vec4<f32>(sample_v, 1.0);
        let sample_ndc  = sample_clip.xyz / sample_clip.w;
        let sample_uv   = vec2<f32>(
            sample_ndc.x *  0.5 + 0.5,
            sample_ndc.y * -0.5 + 0.5,
        );

        // Discard out-of-screen samples.
        if any(sample_uv < vec2<f32>(0.0)) || any(sample_uv > vec2<f32>(1.0)) {
            continue;
        }

        // Load scene depth at the projected sample position.
        let sp = vec2<i32>(
            i32(clamp(sample_uv.x * dim.x, 0.0, dim.x - 1.0)),
            i32(clamp(sample_uv.y * dim.y, 0.0, dim.y - 1.0)),
        );
        let scene_depth = textureLoad(depth_tex, sp, 0);
        let scene_v     = view_pos_from_depth(sample_uv, scene_depth);

        // Ranged occlusion check.
        let range_check = smoothstep(0.0, 1.0, params.radius / abs(pos_v.z - scene_v.z));
        if scene_v.z >= sample_v.z + params.bias {
            occlusion = occlusion + range_check;
        }
    }

    let ao = 1.0 - (occlusion / 64.0);
    return vec4<f32>(ao, ao, ao, 1.0);
}