rsplot 0.5.0

silx-style scientific plotting for egui, rendered with wgpu
Documentation
// Textured-quad shader — the wgpu analogue of silx `scene/primitives.py`
// `ImageData`/`ImageRgba`, which render a 2D image as one textured quad placed in
// the 3D scene (by default in the z=0 plane, pixel (col,row) → world (x,y)).
//
// The quad's world corners + UVs come in as vertex data (six vertices, two
// triangles); positions are projected by the usual clip MVP (group 0, shared
// with the line/triangle pipelines — `Params` must stay layout-identical to
// `scene3d.wgsl`). The image is a premultiplied-linear RGBA8 texture (group 1)
// so its sampled colour matches the geometry path's linear, premultiplied
// convention and round-trips through the blit. Nearest vs linear filtering is
// chosen by the sampler bound here (silx `InterpolationMixIn`).
//
// silx applies the viewport fog to `_Image` like every other primitive
// (primitives.py `_Image._shaders` composes `sceneDecl`/`scenePostCall`), so the
// same linear-fog term runs here.

struct Params {
    mvp: mat4x4<f32>,
    // Linear fog datum (see scene3d.wgsl — shared buffer, identical layout).
    fog_info: vec4<f32>,
    fog_color: vec4<f32>,
    view_row_z: vec4<f32>,
};

@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(0) var tex: texture_2d<f32>;
@group(1) @binding(1) var samp: sampler;

struct VsOut {
    @builtin(position) clip: vec4<f32>,
    @location(0) uv: vec2<f32>,
    @location(1) cam_z: f32,
};

@vertex
fn vs_main(@location(0) pos: vec3<f32>, @location(1) uv: vec2<f32>) -> VsOut {
    var out: VsOut;
    out.clip = params.mvp * vec4<f32>(pos, 1.0);
    out.uv = uv;
    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);
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
    // The texture already holds premultiplied-linear RGBA; sample straight
    // through (premultiplied-alpha blend composites it correctly).
    let color = textureSample(tex, samp, in.uv);
    // silx `_Image` discards fully transparent texels
    // (`plot3d/scene/primitives.py:2120-2123`, `if (color.a == 0.) discard;`),
    // covering both ImageData (a below-min colormap hole → alpha 0) and
    // ImageRgba. `discard` writes neither colour nor DEPTH; without it a
    // transparent texel — the CPU stand-in for the GLSL `discard`, e.g. a cut
    // plane's below-min hole — still passes the depth test and occludes the 3D
    // geometry behind the hole. The texture is premultiplied-linear, so alpha 0
    // is fully transparent. Sample is taken before the branch so its implicit
    // derivatives stay in uniform control flow.
    if (color.a == 0.0) {
        discard;
    }
    return apply_fog(color, in.cam_z);
}