Files

338 lines
13 KiB
WebGPU Shading Language

struct U {
vp: mat4x4<f32>,
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
screen: vec4<f32>, // viewport width, height (px), unused, silhouettes on (1)
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
cap: vec4<f32>, // colour of the inside of cut bodies
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
pull: vec4<f32>, // draft pull direction
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
acc_v: vec4<f32>,
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
// analysis colours (`theme::AnalysisColors`): draft positive, between, negative; reachable,
// blocked; curvature flat, middle, tight
ana_c: array<vec4<f32>, 8>,
ana_f: vec4<f32>, // x = 1: stripe the negative draft and blocked faces
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
};
fn reversed() -> bool {
return u.depth.x > 0.5;
}
/// Clip-space depth measured the usual way (0 at the near plane, w at the far one), whichever
/// way the depth buffer runs.
fn near_z(c: vec4<f32>) -> f32 {
return select(c.z, c.w - c.z, reversed());
}
@group(1) @binding(2) var acc_t: texture_2d<f32>;
fn clipped(p: vec3<f32>) -> bool {
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
}
@group(0) @binding(0) var<uniform> u: U;
fn to_linear(c: vec3<f32>) -> vec3<f32> {
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
}
fn out_color(c: vec4<f32>) -> vec4<f32> {
if (u.back.w > 0.5) {
return vec4<f32>(to_linear(c.rgb), c.a);
}
return c;
}
struct TOut {
@builtin(position) pos: vec4<f32>,
@location(0) n: vec3<f32>,
@location(1) c: vec4<f32>,
@location(2) wp: vec3<f32>,
};
@vertex
fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
var o: TOut;
o.pos = u.vp * vec4<f32>(p, 1.0);
o.n = n;
o.c = c;
o.wp = p;
return o;
}
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
@fragment
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let c = solid(i, front);
return c;
}
struct CutOut {
@location(0) c: vec4<f32>,
@builtin(frag_depth) depth: f32,
};
/// The section cap (the inside seen through the cut, drawn on the far walls' back faces where
/// they are the visible surface) again, at the depth of the section plane rather than of the
/// far wall, so the cut face hides what lies inside the body instead of showing it like a
/// pane of glass.
@fragment
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
var o: CutOut;
// Derivatives (in `pushed`) first, before `solid` may discard.
o.depth = pushed(cap_depth(i.wp, i.pos.z));
o.c = solid(i, front);
return o;
}
/// A depth the shader writes itself, pushed back like the faces the GPU pushes (`FACE_BIAS`,
/// which does not apply then): its own slope over `depth.z` pixels and `depth.y` depth steps.
fn pushed(z: f32) -> f32 {
let slope = max(abs(dpdx(z)), abs(dpdy(z)));
// One step: 2⁻²⁴ in a 24-bit buffer; a float buffer's steps follow the value (2⁻²³ of it).
let step = select(1.0 / 16777216.0, abs(z) * 1.1920929e-7, reversed());
let push = slope * u.depth.z + step * u.depth.y;
return clamp(z - select(-push, push, reversed()), 0.0, 1.0);
}
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
/// never behind the wall itself (`z`).
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
var dir = -u.back.xyz;
if (u.eye.w > 0.5) { dir = wp - u.eye.xyz; }
let dn = dot(u.clip.xyz, dir);
if (abs(dn) < 1e-9) { return z; }
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
let c = u.vp * vec4<f32>(p, 1.0);
if (c.w <= 0.0) { return z; }
if (reversed()) { return clamp(c.z / c.w, z, 1.0); }
return clamp(c.z / c.w, 0.0, z);
}
fn solid(i: TOut, front: bool) -> vec4<f32> {
// Derivatives first, in uniform control flow (browsers' WGSL compilers reject them after
// a branch that returns; the whole shader module then fails and nothing is drawn).
let k = curvature(i);
let ndv = dot(normalize(i.n), normalize(u.back.xyz));
let fw = fwidth(ndv);
if (clipped(i.wp)) { discard; }
if (!front && length(u.clip.xyz) > 0.5) {
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
let h = fract((i.pos.x + i.pos.y) / 9.0);
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
}
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
return analysis(i, k);
}
let c = shade(i);
// Silhouettes (styles with edges): where a curved surface turns away from the eye, a line
// about 1.5 px wide, on the visible surface only (depth-correct).
if (u.screen.w > 0.5 && length(i.n) > 0.5 && fw > 1e-6) {
let a = 1.0 - smoothstep(fw * 0.8, fw * 2.0, abs(ndv));
return vec4<f32>(mix(c.rgb, out_color(vec4<f32>(0.07, 0.08, 0.10, 1.0)).rgb, a), c.a);
}
return c;
}
/// How fast the normal turns across the pixel (per mm).
fn curvature(i: TOut) -> f32 {
let nn = normalize(i.n);
let kx = length(dpdx(nn)) / max(length(dpdx(i.wp)), 1e-6);
let ky = length(dpdy(nn)) / max(length(dpdy(i.wp)), 1e-6);
return max(kx, ky);
}
/// Surface analysis colours: zebra stripes, draft angle bands, curvature map (`k`: the
/// surface's curvature here, worked out by the caller).
fn analysis(i: TOut, k: f32) -> vec4<f32> {
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
let lit = 0.55 + 0.45 * max(dot(n, v), 0.0);
if (u.ana.x < 1.5) {
// Reflections of upright light bars standing around the viewer: the stripe follows
// the reflected ray's sideways direction (horizontal on screen).
let r = reflect(-v, n);
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
let c = select(0.08, 0.95, s > 0.0);
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
}
if (u.ana.x > 4.5) {
// Reachable from the access direction: facing it and nothing above.
let d = normalize(u.pull.xyz);
let size = i32(u.ana.w);
let px = vec2<i32>(floor(vec2<f32>(dot(i.wp, u.acc_u.xyz) + u.acc_u.w, dot(i.wp, u.acc_v.xyz) + u.acc_v.w) * u.ana.z));
var top = -1e30;
if (px.x >= 0 && px.y >= 0 && px.x < size && px.y < size) {
top = textureLoad(acc_t, px, 0).x;
}
let cn = dot(normalize(i.n), d);
let facing = cn > 0.02;
// Slack of a pixel and a half, more on slopes (the map holds the pixel's highest point).
let slack = u.ana.y * (1.0 + sqrt(max(1.0 - cn * cn, 0.0)) / max(cn, 0.05));
let clear = dot(i.wp, d) >= top - slack;
var c = u.ana_c[4].rgb;
if (facing && clear) { c = u.ana_c[3].rgb; } else { c = stripe(i, c); }
return out_color(vec4<f32>(c * lit, 1.0));
}
if (u.ana.x > 3.5) {
// A studio around the model: sky above, a dark floor below, a bright horizon band and
// two soft light panels, seen in a mirror.
let r = reflect(-v, n);
var c = mix(vec3<f32>(0.32, 0.34, 0.38), vec3<f32>(0.80, 0.86, 0.95), smoothstep(-0.05, 0.6, r.z));
c = c + vec3<f32>(0.6) * exp(-abs(r.z) * 40.0);
let side = atan2(r.y, r.x);
c = c + vec3<f32>(0.9) * smoothstep(0.92, 0.98, cos(side * 2.0)) * smoothstep(0.1, 0.3, r.z) * smoothstep(0.8, 0.6, r.z);
if (r.z < -0.02) {
// The floor: lighter toward the horizon, with light-panel streaks along it.
c = mix(vec3<f32>(0.55, 0.56, 0.60), vec3<f32>(0.20, 0.20, 0.22), clamp(-r.z * 1.6, 0.0, 1.0));
c = c + vec3<f32>(0.35) * smoothstep(0.95, 0.99, cos(side * 3.0));
}
return out_color(vec4<f32>(min(c, vec3<f32>(1.0)), 1.0));
}
if (u.ana.x < 2.5) {
let a = degrees(asin(clamp(dot(normalize(i.n), normalize(u.pull.xyz)), -1.0, 1.0)));
var c = u.ana_c[1].rgb;
if (a >= u.ana.y) { c = u.ana_c[0].rgb; }
if (a <= -u.ana.y) { c = stripe(i, u.ana_c[2].rgb); }
return out_color(vec4<f32>(c * lit, 1.0));
}
// Curvature from how fast the normal turns across the pixel.
let t = clamp(k * u.ana.y, 0.0, 1.0);
let c = select(mix(u.ana_c[6].rgb, u.ana_c[7].rgb, t * 2.0 - 1.0), mix(u.ana_c[5].rgb, u.ana_c[6].rgb, t * 2.0), t < 0.5);
return out_color(vec4<f32>(c * lit, 1.0));
}
/// A "bad" analysis face (negative draft, blocked): with the stripes on (colour vision modes), the
/// section cap's 45 degree hatch over it, so colour is not the only cue.
fn stripe(i: TOut, c: vec3<f32>) -> vec3<f32> {
if (u.ana_f.x < 0.5) { return c; }
let h = fract((i.pos.x + i.pos.y) / 9.0);
return select(c, c * 0.6, h < 0.22);
}
@fragment
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return shade(i);
}
/// Reference planes: shaded like `fs_tri`, but never cut by the section plane.
@fragment
fn fs_tri_all(i: TOut) -> @location(0) vec4<f32> {
return shade(i);
}
fn shade(i: TOut) -> vec4<f32> {
// A zero normal marks flat colour (selection fills, translucent planes).
if (length(i.n) < 0.5) {
return out_color(i.c);
}
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
let diff = max(dot(n, key), 0.0);
let fill = max(dot(n, v), 0.0);
let h = normalize(key + v);
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
let k = 0.42 + 0.38 * diff + 0.25 * fill;
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
}
// Lines, decals and ghosts are drawn at their own depth: opaque faces are pushed back by their
// slope instead (`FACE_BIAS`, #1434), so what lies on a face draws over it at any angle.
struct LOut {
@builtin(position) pos: vec4<f32>,
@location(0) c: vec4<f32>,
@location(1) wp: vec3<f32>,
};
@vertex
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
var pa = a;
var pb = b;
var ca = u.vp * vec4<f32>(a, 1.0);
var cb = u.vp * vec4<f32>(b, 1.0);
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
// the segment at the plane instead; clip space is linear in the world point.
let za = near_z(ca);
let zb = near_z(cb);
if (za < 0.0 && zb > 0.0) {
let k = za / (za - zb);
ca = mix(ca, cb, k);
pa = mix(a, b, k);
} else if (zb < 0.0 && za > 0.0) {
let k = zb / (zb - za);
cb = mix(cb, ca, k);
pb = mix(b, a, k);
}
let half = u.screen.xy * 0.5;
let sa = ca.xy / max(ca.w, 1e-6) * half;
let sb = cb.xy / max(cb.w, 1e-6) * half;
var dir = sb - sa;
let len = length(dir);
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
let nrm = vec2<f32>(-dir.y, dir.x);
// Two triangles: corners (t, side).
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
var ss = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let t = ts[vi];
let s = ss[vi];
let base = select(ca, cb, t > 0.5);
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
var o: LOut;
o.pos = vec4<f32>(base.xy + off / half * base.w, base.z, base.w);
o.c = c;
o.wp = select(pa, pb, t > 0.5);
return o;
}
@fragment
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return out_color(i.c);
}
struct IOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) a: f32,
@location(2) wp: vec3<f32>,
};
@group(1) @binding(0) var img_t: texture_2d<f32>;
@group(1) @binding(1) var img_s: sampler;
@vertex
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
var o: IOut;
// On a face it shows over it: the face is pushed back (`FACE_BIAS`).
o.pos = u.vp * vec4<f32>(p, 1.0);
o.uv = uv;
o.a = a;
o.wp = p;
return o;
}
@fragment
fn fs_img(i: IOut) -> @location(0) vec4<f32> {
let c = textureSample(img_t, img_s, i.uv);
if (clipped(i.wp)) { discard; }
return out_color(vec4<f32>(c.rgb, c.a * i.a));
}
/// Lines the section never cuts (the grid, highlights drawn on top).
@fragment
fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
return out_color(i.c);
}