struct U { vp: mat4x4, back: vec4, // xyz = toward the eye, w = 1 for linear output screen: vec4, // viewport width, height (px), unused, silhouettes on (1) clip: vec4, // section plane: normal, d (zero normal: no section) cap: vec4, // colour of the inside of cut bodies ana: vec4, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter pull: vec4, // draft pull direction acc_u: vec4, // accessibility map axes (xyz, offset) acc_v: vec4, eye: vec4, // 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, 8>, ana_f: vec4, // x = 1: stripe the negative draft and blocked faces depth: vec4, // 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 { return select(c.z, c.w - c.z, reversed()); } @group(1) @binding(2) var acc_t: texture_2d; fn clipped(p: vec3) -> bool { return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w; } @group(0) @binding(0) var u: U; fn to_linear(c: vec3) -> vec3 { return select(c, pow((c + vec3(0.055)) / vec3(1.055), vec3(2.4)), c > vec3(0.04045)); } fn out_color(c: vec4) -> vec4 { if (u.back.w > 0.5) { return vec4(to_linear(c.rgb), c.a); } return c; } struct TOut { @builtin(position) pos: vec4, @location(0) n: vec3, @location(1) c: vec4, @location(2) wp: vec3, }; @vertex fn vs_tri(@location(0) p: vec3, @location(1) n: vec3, @location(2) c: vec4) -> TOut { var o: TOut; o.pos = u.vp * vec4(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 { let c = solid(i, front); return c; } struct CutOut { @location(0) c: vec4, @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, 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(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 { // 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(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(mix(c.rgb, out_color(vec4(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 { 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(0.0, 0.0, 1.0)); if (length(side) < 1e-3) { side = vec3(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(vec3(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(floor(vec2(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(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(0.32, 0.34, 0.38), vec3(0.80, 0.86, 0.95), smoothstep(-0.05, 0.6, r.z)); c = c + vec3(0.6) * exp(-abs(r.z) * 40.0); let side = atan2(r.y, r.x); c = c + vec3(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(0.55, 0.56, 0.60), vec3(0.20, 0.20, 0.22), clamp(-r.z * 1.6, 0.0, 1.0)); c = c + vec3(0.35) * smoothstep(0.95, 0.99, cos(side * 3.0)); } return out_color(vec4(min(c, vec3(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(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(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) -> vec3 { 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 { 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 { return shade(i); } fn shade(i: TOut) -> vec4 { // 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(-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(min(i.c.rgb * k + vec3(spec), vec3(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, @location(0) c: vec4, @location(1) wp: vec3, }; @vertex fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3, @location(1) b: vec3, @location(2) c: vec4, @location(3) w: f32) -> LOut { var pa = a; var pb = b; var ca = u.vp * vec4(a, 1.0); var cb = u.vp * vec4(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(1.0, 0.0); } else { dir = dir / len; } let nrm = vec2(-dir.y, dir.x); // Two triangles: corners (t, side). var ts = array(0.0, 1.0, 1.0, 0.0, 1.0, 0.0); var ss = array(-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(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 { if (clipped(i.wp)) { discard; } return out_color(i.c); } struct IOut { @builtin(position) pos: vec4, @location(0) uv: vec2, @location(1) a: f32, @location(2) wp: vec3, }; @group(1) @binding(0) var img_t: texture_2d; @group(1) @binding(1) var img_s: sampler; @vertex fn vs_img(@location(0) p: vec3, @location(1) uv: vec2, @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(p, 1.0); o.uv = uv; o.a = a; o.wp = p; return o; } @fragment fn fs_img(i: IOut) -> @location(0) vec4 { let c = textureSample(img_t, img_s, i.uv); if (clipped(i.wp)) { discard; } return out_color(vec4(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 { return out_color(i.c); }