siplot 0.4.1

silx-style scientific plotting for egui, rendered with wgpu
Documentation
// 3D point-sprite (scatter) shader — the wgpu analogue of silx
// `scene/primitives.py` `_Points`.
//
// wgpu's `PointList` topology only ever rasterises 1×1-pixel points (there is no
// `gl_PointSize`), so a sized, screen-facing point symbol is drawn as a
// billboarded quad: each point is one instance, expanded to two triangles whose
// corners are offset from the projected centre by `size` pixels (converted to an
// NDC offset, multiplied by clip `w` so it survives the perspective divide). The
// fragment stage reproduces silx's per-marker `alphaSymbol(gl_PointCoord, size)`
// coverage functions and discards fully-transparent texels.

struct PointParams {
    // camera.matrix() × model, column-major, depth-corrected (same as scene3d.wgsl).
    mvp: mat4x4<f32>,
    // Offscreen target size in physical pixels (for the pixel→NDC offset).
    viewport: vec2<f32>,
    _pad: vec2<f32>,
};

@group(0) @binding(0) var<uniform> params: PointParams;

struct VsOut {
    @builtin(position) clip: vec4<f32>,
    // 0..1 across the sprite, the analogue of GL's gl_PointCoord.
    @location(0) coord: vec2<f32>,
    @location(1) color: vec4<f32>,
    @location(2) @interpolate(flat) marker: u32,
    @location(3) size: f32,
};

@vertex
fn vs_main(
    @builtin(vertex_index) vi: u32,
    @location(0) pos: vec3<f32>,
    @location(1) color: vec4<f32>,
    @location(2) size: f32,
    @location(3) marker: u32,
) -> VsOut {
    // Two triangles spanning the unit quad, corners in [-0.5, 0.5].
    var corners = array<vec2<f32>, 6>(
        vec2<f32>(-0.5, -0.5),
        vec2<f32>(0.5, -0.5),
        vec2<f32>(0.5, 0.5),
        vec2<f32>(-0.5, -0.5),
        vec2<f32>(0.5, 0.5),
        vec2<f32>(-0.5, 0.5),
    );
    let corner = corners[vi];

    let center = params.mvp * vec4<f32>(pos, 1.0);
    // Pixel offset → NDC offset (NDC spans 2.0 over `viewport` pixels), scaled by
    // clip `w` so the size stays constant in pixels after the perspective divide.
    let ndc = vec2<f32>(
        2.0 * corner.x * size / params.viewport.x,
        2.0 * corner.y * size / params.viewport.y,
    );

    var out: VsOut;
    out.clip = vec4<f32>(center.xy + ndc * center.w, center.z, center.w);
    out.coord = corner + vec2<f32>(0.5, 0.5);
    out.color = color;
    out.marker = marker;
    out.size = size;
    return out;
}

// Per-marker coverage in [0, 1], ported one-for-one from silx
// `_Points._MARKER_FUNCTIONS` (the `size` factor gives a ~1px antialiased edge).
fn alpha_symbol(coord: vec2<f32>, size: f32, marker: u32) -> f32 {
    let centered = abs(coord - vec2<f32>(0.5, 0.5));
    switch marker {
        // 0: circle ('o')
        case 0u: {
            let r = distance(coord, vec2<f32>(0.5, 0.5));
            return clamp(size * (0.5 - r), 0.0, 1.0);
        }
        // 1: diamond ('d')
        case 1u: {
            let f = centered.x + centered.y;
            return clamp(size * (0.5 - f), 0.0, 1.0);
        }
        // 2: square ('s')
        case 2u: {
            return 1.0;
        }
        // 3: plus ('+')
        case 3u: {
            let d = abs(size * (coord - vec2<f32>(0.5, 0.5)));
            if (min(d.x, d.y) < 0.5) { return 1.0; }
            return 0.0;
        }
        // 4: x-cross ('x')
        case 4u: {
            let p = floor(size * coord) + vec2<f32>(0.5, 0.5);
            let dx = abs(vec2<f32>(p.x - p.y, p.x + p.y - size));
            if (min(dx.x, dx.y) <= 0.5) { return 1.0; }
            return 0.0;
        }
        // 5: asterisk ('*') — combining +, x and a soft circle edge
        case 5u: {
            let dplus = abs(size * (coord - vec2<f32>(0.5, 0.5)));
            let p = floor(size * coord) + vec2<f32>(0.5, 0.5);
            let dx = abs(vec2<f32>(p.x - p.y, p.x + p.y - size));
            if (min(dplus.x, dplus.y) < 0.5) {
                return 1.0;
            } else if (min(dx.x, dx.y) <= 0.5) {
                let r = distance(coord, vec2<f32>(0.5, 0.5));
                return clamp(size * (0.5 - r), 0.0, 1.0);
            }
            return 0.0;
        }
        // 6: horizontal line ('_')
        case 6u: {
            let dy = abs(size * (coord.y - 0.5));
            if (dy < 0.5) { return 1.0; }
            return 0.0;
        }
        // 7: vertical line ('|')
        case 7u: {
            let dx2 = abs(size * (coord.x - 0.5));
            if (dx2 < 0.5) { return 1.0; }
            return 0.0;
        }
        default: {
            return 1.0;
        }
    }
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
    let a = alpha_symbol(in.coord, in.size, in.marker);
    if (a == 0.0) {
        discard;
    }
    // `color` is linear, premultiplied alpha; scaling every channel by the
    // coverage keeps it premultiplied for the One/OneMinusSrcAlpha blend.
    return in.color * a;
}