git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/vk/scene3d.wgsl (4.8K)

  1 struct Uniforms {
  2     mvp: mat4x4<f32>,
  3     window_size: vec2<f32>,
  4     window_radius: f32,
  5     // Corner-shape exponent shared with shader2d: circular arc at 2,
  6     // superellipse squircle above.
  7     corner_shape: f32,
  8     // rgb + mix: fragment color mixed toward .rgb by .a. Zero = vertex
  9     // colors untouched; a wireframe pass overlaid on its own filled mesh
 10     // sets it so the lines separate from the identical fill beneath.
 11     wire_tint: vec4<f32>,
 12     // Whole-draw alpha multiplier (straight-alpha blend): 1 = opaque.
 13     opacity: f32,
 14     // 1 on wireframe draws: skip the derivative-normal shading — the
 15     // screen-space derivatives of a line fragment are along-axis only, so
 16     // the "normal" is noise and speckles the wires.
 17     is_wire: f32,
 18     // 1 on a draw whose vertex colours are already lit (the host baked
 19     // smooth shading from vertex normals against the same world light):
 20     // skip the flat shading below so it is not applied twice.
 21     prelit: f32,
 22 }
 23 
 24 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
 25 
 26 // Signed distance to the window's rounded silhouette — shader2d's
 27 // window_corner_distance, kept in lockstep so the 3D scene fill cuts along the
 28 // exact curve the 2D pass (and the plates' tessellated corners) use: positive
 29 // outside the corner arcs and past the window bounds, large-negative on the
 30 // straight edges (those keep their hard cut).
 31 fn window_corner_distance(pos: vec2<f32>) -> f32 {
 32     let w = uniforms.window_size.x;
 33     let h = uniforms.window_size.y;
 34     let r = uniforms.window_radius;
 35 
 36     if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
 37         return 1e5;
 38     }
 39     if (r <= 0.0) {
 40         return -1e5;
 41     }
 42     let q = abs(pos - vec2f(w * 0.5, h * 0.5)) - vec2f(w * 0.5 - r, h * 0.5 - r);
 43     if (q.x > 0.0 && q.y > 0.0) {
 44         let shape = uniforms.corner_shape;
 45         if (shape > 2.001) {
 46             let lp = max(pow(pow(q.x, shape) + pow(q.y, shape), 1.0 / shape), 1e-4);
 47             let g = vec2f(pow(q.x / lp, shape - 1.0), pow(q.y / lp, shape - 1.0));
 48             return (lp - r) / max(length(g), 1e-4);
 49         }
 50         return length(q) - r;
 51     }
 52     return -1e5;
 53 }
 54 
 55 struct VertexOutput {
 56     @builtin(position) position: vec4f,
 57     @location(0) color: vec3f,
 58     // World-space position, for the flat-shading normal; `lit` is 0 on the
 59     // screen-space background quad (the z=9.99 sentinel), 1 on scene geometry.
 60     @location(1) world: vec3f,
 61     @location(2) lit: f32,
 62 };
 63 
 64 @vertex
 65 fn vs_main(
 66     @location(0) position: vec3f,
 67     @location(1) color: vec3f,
 68 ) -> VertexOutput {
 69     var out: VertexOutput;
 70     if (abs(position.z - 9.99) < 0.01) {
 71         out.position = vec4f(position.xy, 0.9999, 1.0);
 72         out.lit = 0.0;
 73     } else {
 74         out.position = uniforms.mvp * vec4f(position, 1.0);
 75         out.lit = 1.0;
 76     }
 77     out.color = color;
 78     out.world = position;
 79     return out;
 80 }
 81 
 82 @fragment
 83 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
 84     // ~1px feather along the squircle window corner (the pass clears to
 85     // transparent and blends with straight alpha, so partial coverage fades
 86     // the scene out exactly at the silhouette).
 87     let cov = 1.0 - smoothstep(-0.5, 0.5, window_corner_distance(in.position.xy));
 88     if (cov <= 0.0) {
 89         discard;
 90     }
 91     var rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
 92     // Flat shading off a fixed WORLD light: the facet normal comes from the
 93     // screen-space derivatives of the world position, so every facet keeps a
 94     // brightness pinned to its world orientation. That anchoring is what makes
 95     // an orbit read as the camera moving around stationary geometry — an unlit
 96     // scene's only cues are the vertex colors, and any rotationally
 97     // self-similar surface (a UV sphere's lattice, especially under a
 98     // wireframe overlay whose fill occludes the back wires) reads as glued to
 99     // the camera without it. Two-sided so unculled back faces stay sane.
100     if (in.lit > 0.5 && uniforms.is_wire < 0.5 && uniforms.prelit < 0.5) {
101         let n = normalize(cross(dpdx(in.world), dpdy(in.world)));
102         // A strongly AZIMUTHAL light, wrap-shaded. A near-vertical light (or a
103         // two-sided |dot|) yields a latitude-dominated / 180-degree-symmetric
104         // brightness pattern — invariant under a yaw orbit, which reads as the
105         // scene turning with the camera. The horizontal component pins the lit
106         // side to a world azimuth the orbit visibly sweeps across; the wrap
107         // term keeps a soft floor without |dot|'s ambiguity (the fill pass
108         // culls to front faces, so the derivative normal's sign is stable).
109         let l = normalize(vec3f(-0.55, 0.45, 0.7));
110         let d = clamp(dot(n, l) * 0.5 + 0.5, 0.0, 1.0);
111         rgb = rgb * (0.55 + 0.45 * d);
112     }
113     return vec4f(rgb, cov * uniforms.opacity);
114 }