viewport-lib 0.14.0

3D viewport rendering library
Documentation
// outline_edge.wgsl : screen-space edge detection on the selection mask.
//
// Samples the mask texture at multiple radii around each fragment to detect
// the boundary of the selection mask. Draws the outline on both sides of
// the boundary to eliminate dark gaps between the outline and the object.
//
// Group 0: mask texture + sampler + OutlineEdgeUniform.

struct OutlineEdgeUniform {
    colour: vec4<f32>,
    radius: f32,
    viewport_w: f32,
    viewport_h: f32,
    _pad: f32,
};

@group(0) @binding(0) var mask_tex: texture_2d<f32>;
@group(0) @binding(1) var mask_samp: sampler;
@group(0) @binding(2) var<uniform> params: OutlineEdgeUniform;

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

// Fullscreen triangle (no vertex buffer).
@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);
}

// 16-tap ring sample at a given radius, returns (max, min) of mask values.
fn sample_ring(uv: vec2<f32>, r: f32, px: vec2<f32>) -> vec2<f32> {
    var mx: f32 = 0.0;
    var mn: f32 = 1.0;
    // 16 evenly spaced points around a circle.
    let s0  = textureSample(mask_tex, mask_samp, uv + vec2( 1.000,  0.000) * r * px).r;
    let s1  = textureSample(mask_tex, mask_samp, uv + vec2( 0.924,  0.383) * r * px).r;
    let s2  = textureSample(mask_tex, mask_samp, uv + vec2( 0.707,  0.707) * r * px).r;
    let s3  = textureSample(mask_tex, mask_samp, uv + vec2( 0.383,  0.924) * r * px).r;
    let s4  = textureSample(mask_tex, mask_samp, uv + vec2( 0.000,  1.000) * r * px).r;
    let s5  = textureSample(mask_tex, mask_samp, uv + vec2(-0.383,  0.924) * r * px).r;
    let s6  = textureSample(mask_tex, mask_samp, uv + vec2(-0.707,  0.707) * r * px).r;
    let s7  = textureSample(mask_tex, mask_samp, uv + vec2(-0.924,  0.383) * r * px).r;
    let s8  = textureSample(mask_tex, mask_samp, uv + vec2(-1.000,  0.000) * r * px).r;
    let s9  = textureSample(mask_tex, mask_samp, uv + vec2(-0.924, -0.383) * r * px).r;
    let s10 = textureSample(mask_tex, mask_samp, uv + vec2(-0.707, -0.707) * r * px).r;
    let s11 = textureSample(mask_tex, mask_samp, uv + vec2(-0.383, -0.924) * r * px).r;
    let s12 = textureSample(mask_tex, mask_samp, uv + vec2( 0.000, -1.000) * r * px).r;
    let s13 = textureSample(mask_tex, mask_samp, uv + vec2( 0.383, -0.924) * r * px).r;
    let s14 = textureSample(mask_tex, mask_samp, uv + vec2( 0.707, -0.707) * r * px).r;
    let s15 = textureSample(mask_tex, mask_samp, uv + vec2( 0.924, -0.383) * r * px).r;
    mx = max(mx, max(max(max(s0, s1), max(s2, s3)), max(max(s4, s5), max(s6, s7))));
    mx = max(mx, max(max(max(s8, s9), max(s10, s11)), max(max(s12, s13), max(s14, s15))));
    mn = min(mn, min(min(min(s0, s1), min(s2, s3)), min(min(s4, s5), min(s6, s7))));
    mn = min(mn, min(min(min(s8, s9), min(s10, s11)), min(min(s12, s13), min(s14, s15))));
    return vec2(mx, mn);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let px = vec2<f32>(1.0 / params.viewport_w, 1.0 / params.viewport_h);
    let center = textureSample(mask_tex, mask_samp, in.uv).r;
    let r = params.radius;

    // Sample at two radii for smoother coverage.
    let ring_outer = sample_ring(in.uv, r, px);
    let ring_inner = sample_ring(in.uv, r * 0.5, px);

    let max_mask = max(ring_outer.x, ring_inner.x);
    let min_mask = min(ring_outer.y, ring_inner.y);

    // Outside edge: this pixel is outside the mask but a neighbor is inside.
    let outside_edge = max_mask * (1.0 - center);
    // Inside edge: this pixel is inside the mask but a neighbor is outside.
    // This covers the boundary pixels to eliminate the dark gap.
    let inside_edge = center * (1.0 - min_mask);
    // Combined edge strength.
    let edge = max(outside_edge, inside_edge);

    if edge < 0.001 {
        discard;
    }

    return vec4<f32>(params.colour.rgb, params.colour.a * edge);
}