graph-explorer-render 0.3.0

WebGL2/wgpu renderer for graph-explorer — nodes, edges, halos and labels.
Documentation
struct Camera {
  center: vec2<f32>,
  zoom: f32,
  fade: f32,
  viewport: vec2<f32>,
  _pad1: vec2<f32>,
};
@group(0) @binding(0) var<uniform> cam: Camera;

// Half-width of the antialiasing ramp in pixels, and the amount the quad grows
// on each side to make room for it. See the same constant in edge.wgsl.
const AA_PAD: f32 = 1.0;

// 1/sqrt(2), for converting the diamond's L1 distance to a perpendicular one.
const INV_SQRT2: f32 = 0.70710678;

struct VsOut {
  @builtin(position) clip: vec4<f32>,
  // Position within the shape, normalized so |local| == 1 lands exactly on the
  // rim. The quad extends past that (see `pad`), so `local` runs slightly
  // beyond 1 at the corners — that overshoot is the AA band.
  @location(0) local: vec2<f32>,
  @location(1) color: vec4<f32>,
  @location(2) opacity: f32,
  @location(3) @interpolate(flat) shape: u32,
  // Drawn radius in pixels. Converts the normalized distance above back into
  // pixels so the AA ramp stays one pixel wide at every radius and zoom —
  // which the old radius-relative smoothstep(0.9, 1.0, ...) did not: it was a
  // sub-pixel ramp on a small node and a several-pixel smear on a large one.
  @location(4) @interpolate(flat) radius_px: f32,
};

// Unit quad corners (two triangles) provided as vertex_index 0..6
var<private> QUAD: array<vec2<f32>, 6> = 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),
);

@vertex
fn vs_main(
  @builtin(vertex_index) vi: u32,
  @location(0) pos: vec2<f32>,
  @location(1) radius: f32,
  @location(2) opacity: f32,
  @location(3) shape: u32,
  @location(4) color: vec4<f32>,
) -> VsOut {
  let corner = QUAD[vi];
  // A zero radius would make the normalization below divide by zero; such a
  // node covers nothing anyway, so any finite `local` is fine.
  let r = max(radius, 1e-6);
  let pad = AA_PAD / cam.zoom;          // pixels -> world units
  let r_out = r + pad;
  let world = pos + corner * r_out;
  let rel = (world - cam.center) * cam.zoom;
  let clip = vec2<f32>(rel.x / (cam.viewport.x * 0.5), rel.y / (cam.viewport.y * 0.5));
  var out: VsOut;
  out.clip = vec4<f32>(clip, 0.0, 1.0);
  out.local = corner * (r_out / r);
  out.color = color;
  out.opacity = opacity;
  out.shape = shape;
  out.radius_px = r * cam.zoom;
  return out;
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
  let p = in.local;
  // Signed distance from the rim in pixels: negative inside, positive outside.
  var dist_px: f32;
  if (in.shape == 1u) {            // square
    dist_px = (max(abs(p.x), abs(p.y)) - 1.0) * in.radius_px;
  } else if (in.shape == 2u) {     // diamond
    // |x|+|y| is an L1 distance, which overshoots the true perpendicular
    // distance from a 45-degree edge by sqrt(2). Without the correction the
    // diamond's AA band would be 1.41px wide while the circle's is 1px, and
    // the two would visibly disagree side by side.
    dist_px = (abs(p.x) + abs(p.y) - 1.0) * in.radius_px * INV_SQRT2;
  } else {                         // circle
    dist_px = (length(p) - 1.0) * in.radius_px;
  }
  // Coverage of a one-pixel sample straddling the rim.
  let coverage = clamp(0.5 - dist_px, 0.0, 1.0);
  if (coverage <= 0.0) { discard; }
  return vec4<f32>(in.color.rgb, in.color.a * in.opacity * coverage * cam.fade);
}