rsplot 0.5.0

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>,
    // Linear fog datum (see scene3d.wgsl): x = scale, y = near, z = on/off.
    fog_info: vec4<f32>,
    fog_color: vec4<f32>,
    // Row 2 of the view matrix — camera-space z of the point centre (silx fogs
    // `_Points` on the point's `vCameraPosition`, primitives.py).
    view_row_z: vec4<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,
    // Camera-space z of the point centre (shared by the whole sprite).
    @location(4) cam_z: 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;
    out.cam_z = dot(params.view_row_z, vec4<f32>(pos, 1.0));
    return out;
}

// Linear fog, port of silx scene/function.py Fog._fragDecl (:79-93); colours
// are premultiplied, so the fog colour is scaled by alpha (see scene3d.wgsl).
fn apply_fog(color: vec4<f32>, cam_z: f32) -> vec4<f32> {
    if (params.fog_info.z == 0.0) {
        return color;
    }
    let factor = clamp(params.fog_info.x * (cam_z - params.fog_info.y), 0.0, 1.0);
    let rgb = mix(color.rgb, params.fog_color.rgb * color.a, factor);
    return vec4<f32>(rgb, color.a);
}

// 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 apply_fog(in.color * a, in.cam_z);
}