GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/draw/scene3d.wgsl (6K)
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 // 1 on a `SceneDraw::screen_space` draw (a pane's background quad): the
23 // vertex's xy are already NDC, so it skips the mvp and sits at the far
24 // plane, unlit. A per-draw flag, never read off the vertex data: the
25 // z = 9.99 sentinel it replaced caught real geometry on that plane.
26 screen_space: f32,
27 // xyz: the direction TOWARD the light, in world space
28 // (`VkRenderer::set_scene_light`). Unit length.
29 light: vec4<f32>,
30 }
31
32 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
33
34 // Signed distance to the window's rounded silhouette — shader2d's
35 // window_corner_distance, kept in lockstep so the 3D scene fill cuts along the
36 // exact curve the 2D pass (and the plates' tessellated corners) use: positive
37 // outside the corner arcs and past the window bounds, large-negative on the
38 // straight edges (those keep their hard cut).
39 fn window_corner_distance(pos: vec2<f32>) -> f32 {
40 let w = uniforms.window_size.x;
41 let h = uniforms.window_size.y;
42 let r = uniforms.window_radius;
43
44 if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
45 return 1e5;
46 }
47 if (r <= 0.0) {
48 return -1e5;
49 }
50 let q = abs(pos - vec2f(w * 0.5, h * 0.5)) - vec2f(w * 0.5 - r, h * 0.5 - r);
51 if (q.x > 0.0 && q.y > 0.0) {
52 let shape = uniforms.corner_shape;
53 if (shape > 2.001) {
54 let lp = max(pow(pow(q.x, shape) + pow(q.y, shape), 1.0 / shape), 1e-4);
55 let g = vec2f(pow(q.x / lp, shape - 1.0), pow(q.y / lp, shape - 1.0));
56 return (lp - r) / max(length(g), 1e-4);
57 }
58 return length(q) - r;
59 }
60 return -1e5;
61 }
62
63 struct VertexOutput {
64 @builtin(position) position: vec4f,
65 @location(0) color: vec3f,
66 // World-space position, for the flat-shading normal; `lit` is 0 on a
67 // screen-space draw (the background quad), 1 on scene geometry.
68 @location(1) world: vec3f,
69 @location(2) lit: f32,
70 };
71
72 // The vertex, and the instance it is drawn for (`SceneDraw::instances`):
73 // the instance's offset is added to the vertex and its colour multiplies the
74 // vertex's. A draw without instances is drawn for one at the origin in white,
75 // which changes nothing.
76 @vertex
77 fn vs_main(
78 @location(0) position: vec3f,
79 @location(1) color: vec3f,
80 @location(2) instance_offset: vec3f,
81 @location(3) instance_color: vec3f,
82 ) -> VertexOutput {
83 var out: VertexOutput;
84 let placed = position + instance_offset;
85 if (uniforms.screen_space > 0.5) {
86 out.position = vec4f(position.xy, 0.9999, 1.0);
87 out.lit = 0.0;
88 } else {
89 out.position = uniforms.mvp * vec4f(placed, 1.0);
90 out.lit = 1.0;
91 }
92 out.color = color * instance_color;
93 out.world = placed;
94 return out;
95 }
96
97 @fragment
98 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
99 // The world position's screen-space derivatives, for the flat shading
100 // below — taken here, in uniform control flow: WebGPU rejects a
101 // derivative under a branch on a varying (`in.lit`), and the values are
102 // the same wherever in the invocation they are read.
103 let world_dx = dpdx(in.world);
104 let world_dy = dpdy(in.world);
105 // ~1px feather along the squircle window corner (the pass clears to
106 // transparent and blends with straight alpha, so partial coverage fades
107 // the scene out exactly at the silhouette).
108 let cov = 1.0 - smoothstep(-0.5, 0.5, window_corner_distance(in.position.xy));
109 if (cov <= 0.0) {
110 discard;
111 }
112 var rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
113 // Flat shading off a fixed WORLD light: the facet normal comes from the
114 // screen-space derivatives of the world position, so every facet keeps a
115 // brightness pinned to its world orientation. That anchoring is what makes
116 // an orbit read as the camera moving around stationary geometry — an unlit
117 // scene's only cues are the vertex colors, and any rotationally
118 // self-similar surface (a UV sphere's lattice, especially under a
119 // wireframe overlay whose fill occludes the back wires) reads as glued to
120 // the camera without it. Two-sided so unculled back faces stay sane.
121 if (in.lit > 0.5 && uniforms.is_wire < 0.5 && uniforms.prelit < 0.5) {
122 let n = normalize(cross(world_dx, world_dy));
123 // A strongly AZIMUTHAL light, wrap-shaded. A near-vertical light (or a
124 // two-sided |dot|) yields a latitude-dominated / 180-degree-symmetric
125 // brightness pattern — invariant under a yaw orbit, which reads as the
126 // scene turning with the camera. The horizontal component pins the lit
127 // side to a world azimuth the orbit visibly sweeps across; the wrap
128 // term keeps a soft floor without |dot|'s ambiguity (the fill pass
129 // culls to front faces, so the derivative normal's sign is stable).
130 // `n` is screen-right x framebuffer-DOWN, which for a visible
131 // face points INTO the surface, so the outward normal is `-n`.
132 let d = clamp(dot(-n, uniforms.light.xyz) * 0.5 + 0.5, 0.0, 1.0);
133 rgb = rgb * (0.55 + 0.45 * d);
134 }
135 return vec4f(rgb, cov * uniforms.opacity);
136 }