facett-core 0.1.18

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
// **facett-graph3d GPU lane shaders** (feature `wgpu`) — instanced emissive nodes +
// edges drawn with ADDITIVE blend (src=ONE, dst=ONE) into the shared HDR
// (`Rgba16Float`) offscreen, then a bright-pass → Gaussian-blur → composite bloom
// post chain.
//
// The additive blend is the whole point: an edge crossing draws two emissive
// fragments into the same texel, so their linear radiance SUMS (it can exceed 1.0).
// The HDR offscreen keeps that >1.0 energy, the bright pass isolates it, the blur
// spreads it, and the composite adds the glow back — so dense crossings bloom hot.
//
// Geometry is generated in the vertex shader from a per-instance buffer (a unit quad
// expanded to a screen-space disc / thick line), so there is no index/vertex buffer:
// `draw(0..6, 0..instances)`.

// ── Node + edge pass: viewport size only (px → clip) ──────────────────────────
struct SceneUniforms {
    // viewport.xy = pane size in physical px ; zw = pad
    viewport: vec4<f32>,
};
@group(0) @binding(0) var<uniform> SU: SceneUniforms;

// Per-instance node: a screen-space emissive disc.
struct NodeInst {
    @location(0) center: vec2<f32>, // px, top-left origin (y down)
    @location(1) radius: f32,       // px
    @location(2) color: vec4<f32>,  // emissive linear radiance (rgb may be > 1)
};

struct NodeVsOut {
    @builtin(position) clip: vec4<f32>,
    @location(0) local: vec2<f32>,  // [-1,1] across the quad
    @location(1) color: vec4<f32>,
};

// The 6 corners of a [-1,1] quad (two triangles).
fn quad_corner(vi: u32) -> vec2<f32> {
    var c = array<vec2<f32>, 6>(
        vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, -1.0), vec2<f32>( 1.0,  1.0),
        vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0,  1.0), vec2<f32>(-1.0,  1.0),
    );
    return c[vi];
}

// px (top-left, y down) → clip space (y up).
fn px_to_clip(px: vec2<f32>) -> vec2<f32> {
    let n = px / SU.viewport.xy;          // 0..1
    return vec2<f32>(n.x * 2.0 - 1.0, 1.0 - n.y * 2.0);
}

@vertex
fn node_vs(in: NodeInst, @builtin(vertex_index) vi: u32) -> NodeVsOut {
    let corner = quad_corner(vi);
    let px = in.center + corner * in.radius;
    var out: NodeVsOut;
    out.clip = vec4<f32>(px_to_clip(px), 0.0, 1.0);
    out.local = corner;
    out.color = in.color;
    return out;
}

@fragment
fn node_fs(in: NodeVsOut) -> @location(0) vec4<f32> {
    let d = length(in.local);
    if (d > 1.0) { discard; }
    // A bright core + soft falloff (Gaussian-ish), so a node is a glowing orb.
    let fall = exp(-3.0 * d * d);
    let rgb = in.color.rgb * fall;
    // Additive blend (ONE,ONE): emit linear radiance; alpha carries coverage so the
    // composite background stays correct where nothing drew.
    return vec4<f32>(rgb, fall * in.color.a);
}

// Per-instance edge: a thick screen-space emissive line segment a→b.
struct EdgeInst {
    @location(0) a: vec2<f32>,      // px
    @location(1) b: vec2<f32>,      // px
    @location(2) width: f32,        // px (half-width)
    @location(3) color: vec4<f32>,  // emissive linear radiance
};

struct EdgeVsOut {
    @builtin(position) clip: vec4<f32>,
    @location(0) across: f32,       // [-1,1] across the line width
    @location(1) color: vec4<f32>,
};

@vertex
fn edge_vs(in: EdgeInst, @builtin(vertex_index) vi: u32) -> EdgeVsOut {
    let corner = quad_corner(vi);          // corner.x = along (-1..1), corner.y = across
    let dir = normalize(in.b - in.a + vec2<f32>(1e-6, 0.0));
    let perp = vec2<f32>(-dir.y, dir.x);
    // Map along: -1 → a, +1 → b.
    let mid = (in.a + in.b) * 0.5;
    let half_len = length(in.b - in.a) * 0.5;
    let px = mid + dir * (corner.x * half_len) + perp * (corner.y * in.width);
    var out: EdgeVsOut;
    out.clip = vec4<f32>(px_to_clip(px), 0.0, 1.0);
    out.across = corner.y;
    out.color = in.color;
    return out;
}

@fragment
fn edge_fs(in: EdgeVsOut) -> @location(0) vec4<f32> {
    // Soft across-width falloff so lines are anti-aliased glowing filaments.
    let fall = smoothstep(1.0, 0.0, abs(in.across));
    return vec4<f32>(in.color.rgb * fall, fall * in.color.a);
}

// ── Post stack (bright → blur → composite) ────────────────────────────────────
struct PostUniforms {
    // threshold (x), knee (y), bloom intensity (z), exposure (w)
    params: vec4<f32>,
};
@group(0) @binding(0) var<uniform> PU: PostUniforms;
@group(0) @binding(1) var src_tex: texture_2d<f32>;
@group(0) @binding(2) var src_smp: sampler;

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

@vertex
fn post_vs(@builtin(vertex_index) vi: u32) -> PostVsOut {
    var p = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0),
    );
    var out: PostVsOut;
    let xy = p[vi];
    out.clip = vec4<f32>(xy, 0.0, 1.0);
    out.uv = vec2<f32>(xy.x * 0.5 + 0.5, 1.0 - (xy.y * 0.5 + 0.5));
    return out;
}

// Soft-knee bright pass: keep only radiance above `threshold` (knee = smooth fade).
fn soft_threshold(c: vec3<f32>, threshold: f32, knee: f32) -> vec3<f32> {
    let br = max(c.r, max(c.g, c.b));
    let soft = clamp(br - threshold + knee, 0.0, 2.0 * knee);
    let soft2 = soft * soft / (4.0 * knee + 1e-5);
    let contrib = max(soft2, br - threshold) / max(br, 1e-5);
    return c * max(contrib, 0.0);
}

@fragment
fn bright_fs(in: PostVsOut) -> @location(0) vec4<f32> {
    let c = textureSample(src_tex, src_smp, in.uv).rgb;
    return vec4<f32>(soft_threshold(c, PU.params.x, PU.params.y), 1.0);
}

// Composite: HDR scene + blurred bloom, exposure, Reinhard tonemap (the project's
// established transfer), sRGB-ish lift → LDR. binding 3 = blurred bloom texture.
@group(0) @binding(3) var bloom_tex: texture_2d<f32>;

@fragment
fn composite_fs(in: PostVsOut) -> @location(0) vec4<f32> {
    let scene = textureSample(src_tex, src_smp, in.uv).rgb;
    let bloom = textureSample(bloom_tex, src_smp, in.uv).rgb;
    var c = scene + bloom * PU.params.z;   // additive bloom (intensity)
    c = c * PU.params.w;                    // exposure
    c = c / (c + vec3<f32>(0.85));          // Reinhard-style tonemap
    c = pow(clamp(c, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 1.15));
    return vec4<f32>(c, 1.0);
}