git.lucas.co / cce-ui
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 }