338 lines
13 KiB
WebGPU Shading Language
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);
|
|
}
|