// **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);
}