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 }