// L0 SDF quad shader — instanced screen-aligned quads, one fragment-shader signed
// distance function per shape (circle / ring / marker). The coverage math is
// byte-for-byte the same as `render/cpu/sdf.rs` so a GPU frame matches a CPU frame.
//
// Shape tags (match `prim::shape`):
const SHAPE_CIRCLE: u32 = 0u;
const SHAPE_RING: u32 = 1u;
const SHAPE_SQUARE: u32 = 2u;
const SHAPE_TRIANGLE: u32 = 3u;
const SHAPE_DIAMOND: u32 = 4u;
struct Uniforms {
// viewport size in pixels (for the pixel→clip transform).
viewport: vec2<f32>,
_pad: vec2<f32>,
};
@group(0) @binding(0) var<uniform> U: Uniforms;
// One QuadInstance (matches `prim::QuadInstance`, 48 bytes).
struct Instance {
@location(0) center: vec2<f32>,
@location(1) radius: f32,
@location(2) inner: f32,
@location(3) color: vec4<f32>,
@location(4) aa: f32,
@location(5) shape: u32,
};
struct VsOut {
@builtin(position) clip: vec4<f32>,
@location(0) local: vec2<f32>, // offset from the instance centre, pixels
@location(1) color: vec4<f32>,
@location(2) @interpolate(flat) radius: f32,
@location(3) @interpolate(flat) inner: f32,
@location(4) @interpolate(flat) aa: f32,
@location(5) @interpolate(flat) shape: u32,
};
// A unit quad expanded to ±(radius + aa) around the instance centre.
@vertex
fn vs_main(@builtin(vertex_index) vi: u32, inst: Instance) -> VsOut {
// Two triangles: (0,0)(1,0)(0,1) / (1,0)(1,1)(0,1).
var corners = 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),
);
let he = inst.radius + inst.aa;
let corner = corners[vi];
let local = corner * he;
let px = inst.center + local;
// pixel → clip (y down in pixels → y up in clip).
let ndc = vec2<f32>(px.x / U.viewport.x * 2.0 - 1.0, 1.0 - px.y / U.viewport.y * 2.0);
var out: VsOut;
out.clip = vec4<f32>(ndc, 0.0, 1.0);
out.local = local;
out.color = inst.color;
out.radius = inst.radius;
out.inner = inst.inner;
out.aa = inst.aa;
out.shape = inst.shape;
return out;
}
fn coverage_from_sd(d: f32, aa: f32) -> f32 {
if (aa <= 0.0) {
return select(0.0, 1.0, d <= 0.0);
}
return 1.0 - smoothstep(-aa, aa, d);
}
fn triangle_sd(p: vec2<f32>, half: f32) -> f32 {
// Intersection of three half-planes (apex up). Identical to the CPU form.
let bottom = p.y - half;
let inv = 1.0 / sqrt(5.0);
let right = (p.x * 2.0 + (p.y + half) * -1.0) * inv;
let left = (p.x * -2.0 + (p.y + half) * -1.0) * inv;
return max(bottom, max(right, left));
}
fn box_sd(p: vec2<f32>, half: f32, corner: f32) -> f32 {
let c = clamp(corner, 0.0, half);
let d = abs(p) - vec2<f32>(half - c, half - c);
let outside = length(max(d, vec2<f32>(0.0, 0.0)));
let inside = min(max(d.x, d.y), 0.0);
return outside + inside - c;
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let dist = length(in.local);
var cov = 0.0;
if (in.shape == SHAPE_CIRCLE) {
cov = coverage_from_sd(dist - in.radius, in.aa);
} else if (in.shape == SHAPE_RING) {
let sd = max(dist - in.radius, in.inner - dist);
cov = coverage_from_sd(sd, in.aa);
} else if (in.shape == SHAPE_SQUARE) {
cov = coverage_from_sd(box_sd(in.local, in.radius, in.inner), in.aa);
} else if (in.shape == SHAPE_TRIANGLE) {
cov = coverage_from_sd(triangle_sd(in.local, in.radius), in.aa);
} else if (in.shape == SHAPE_DIAMOND) {
let sd = (abs(in.local.x) + abs(in.local.y)) - in.radius;
cov = coverage_from_sd(sd, in.aa);
}
if (cov <= 0.0) {
discard;
}
// Premultiplied output for the standard alpha-blend state.
let a = in.color.a * cov;
return vec4<f32>(in.color.rgb * a, a);
}