viewport-lib 0.1.0

3D viewport rendering library
Documentation
// fxaa.wgsl — FXAA (Fast Approximate Anti-Aliasing) fullscreen post-process pass.
//
// Renders a fullscreen triangle, reads from the tone-mapped LDR texture, and
// applies luminance-based edge detection and sub-pixel blending to reduce aliasing.
//
// Based on the NVIDIA FXAA 3.11 algorithm, simplified for clarity.
// Input: LDR Rgba16Float or Rgba8Unorm tone-mapped texture.
// Output: Anti-aliased LDR color.

@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;

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

// Generate a fullscreen triangle from three vertices (no vertex buffer needed).
@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);
}

// Perceived luminance of an LDR RGB color.
fn luminance(rgb: vec3<f32>) -> f32 {
    return dot(rgb, vec3<f32>(0.299, 0.587, 0.114));
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let dims = vec2<f32>(textureDimensions(input_texture));
    let texel = 1.0 / dims;

    // Sample center and 4 neighbors.
    let c  = textureSample(input_texture, input_sampler, in.uv).rgb;
    let n  = textureSample(input_texture, input_sampler, in.uv + vec2<f32>( 0.0,  texel.y)).rgb;
    let s  = textureSample(input_texture, input_sampler, in.uv + vec2<f32>( 0.0, -texel.y)).rgb;
    let e  = textureSample(input_texture, input_sampler, in.uv + vec2<f32>( texel.x,  0.0)).rgb;
    let w  = textureSample(input_texture, input_sampler, in.uv + vec2<f32>(-texel.x,  0.0)).rgb;

    let lc = luminance(c);
    let ln = luminance(n);
    let ls = luminance(s);
    let le = luminance(e);
    let lw = luminance(w);

    // Local contrast.
    let l_min = min(lc, min(min(ln, ls), min(le, lw)));
    let l_max = max(lc, max(max(ln, ls), max(le, lw)));
    let l_range = l_max - l_min;

    // FXAA threshold: skip low-contrast regions (thin line threshold = 0.0312).
    let FXAA_EDGE_THRESHOLD      = 0.125;
    let FXAA_EDGE_THRESHOLD_MIN  = 0.0312;
    if l_range < max(FXAA_EDGE_THRESHOLD_MIN, l_max * FXAA_EDGE_THRESHOLD) {
        return vec4<f32>(c, 1.0);
    }

    // Diagonal neighbors for 3x3 sub-pixel blend.
    let nw = textureSample(input_texture, input_sampler, in.uv + vec2<f32>(-texel.x,  texel.y)).rgb;
    let ne = textureSample(input_texture, input_sampler, in.uv + vec2<f32>( texel.x,  texel.y)).rgb;
    let sw = textureSample(input_texture, input_sampler, in.uv + vec2<f32>(-texel.x, -texel.y)).rgb;
    let se = textureSample(input_texture, input_sampler, in.uv + vec2<f32>( texel.x, -texel.y)).rgb;

    let l_nw = luminance(nw);
    let l_ne = luminance(ne);
    let l_sw = luminance(sw);
    let l_se = luminance(se);

    // Sub-pixel blend factor: 3x3 box minus center, weighted by distance.
    let l_subpix_aa = (2.0 * (ln + ls + le + lw) + (l_nw + l_ne + l_sw + l_se)) / 12.0;
    let l_subpix_delta = abs(l_subpix_aa - lc) / l_range;
    let blend = smoothstep(0.0, 1.0, l_subpix_delta) * 0.75;

    // Edge direction: horizontal vs. vertical.
    let horiz = abs(ln + ls - 2.0 * lc) * 2.0 + abs(l_ne + l_se - 2.0 * le) + abs(l_nw + l_sw - 2.0 * lw);
    let vert  = abs(le + lw - 2.0 * lc) * 2.0 + abs(l_ne + l_nw - 2.0 * ln) + abs(l_se + l_sw - 2.0 * ls);
    let is_horiz = horiz >= vert;

    // Step one texel in the direction perpendicular to the edge.
    var step_uv: vec2<f32>;
    if is_horiz {
        // Horizontal edge: blend vertically.
        let l_pos = ln;
        let l_neg = ls;
        step_uv = vec2<f32>(0.0, select(-texel.y, texel.y, l_pos >= l_neg));
    } else {
        // Vertical edge: blend horizontally.
        let l_pos = le;
        let l_neg = lw;
        step_uv = vec2<f32>(select(-texel.x, texel.x, l_pos >= l_neg), 0.0);
    }

    let sample_uv = in.uv + step_uv * blend;
    let blended = textureSample(input_texture, input_sampler, sample_uv).rgb;
    return vec4<f32>(mix(c, blended, blend), 1.0);
}