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

commite955afb19815e35ee8005a075e6ef3b2ded32a88
parent9dd78dd31d
authorLucas Galante <[email protected]>
date2026-07-30 13:46
feat: flat-shaded world lighting in the raster 3D pass

The scene was unlit vertex-color passthrough, so the only orbit cue was
the colors themselves — and any rotationally self-similar surface (a UV
sphere's lattice, worst under a wireframe overlay whose fill occludes the
back wires' parallax) perceptually glued itself to the camera. Facets now
shade off a fixed WORLD light, normal from screen-space derivatives of the
world position (valid on wire passes too — line rasterization keeps the
triangle's plane gradients), two-sided, 0.62 ambient floor. Every facet's
brightness is pinned to its world orientation, which is what makes an
orbit read as the camera moving around stationary geometry. The
screen-space background quad (z=9.99 sentinel) stays unlit.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/vk/scene3d.wgsl | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/vk/scene3d.wgsl b/src/vk/scene3d.wgsl
index d06179b..035451a 100644
--- a/src/vk/scene3d.wgsl
+++ b/src/vk/scene3d.wgsl
@@ -45,6 +45,10 @@ fn window_corner_distance(pos: vec2<f32>) -> f32 {
 struct VertexOutput {
     @builtin(position) position: vec4f,
     @location(0) color: vec3f,
+    // World-space position, for the flat-shading normal; `lit` is 0 on the
+    // screen-space background quad (the z=9.99 sentinel), 1 on scene geometry.
+    @location(1) world: vec3f,
+    @location(2) lit: f32,
 };
 
 @vertex
@@ -55,10 +59,13 @@ fn vs_main(
     var out: VertexOutput;
     if (abs(position.z - 9.99) < 0.01) {
         out.position = vec4f(position.xy, 0.9999, 1.0);
+        out.lit = 0.0;
     } else {
         out.position = uniforms.mvp * vec4f(position, 1.0);
+        out.lit = 1.0;
     }
     out.color = color;
+    out.world = position;
     return out;
 }
 
@@ -71,6 +78,20 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
     if (cov <= 0.0) {
         discard;
     }
-    let rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
+    var rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
+    // Flat shading off a fixed WORLD light: the facet normal comes from the
+    // screen-space derivatives of the world position, so every facet keeps a
+    // brightness pinned to its world orientation. That anchoring is what makes
+    // an orbit read as the camera moving around stationary geometry — an unlit
+    // scene's only cues are the vertex colors, and any rotationally
+    // self-similar surface (a UV sphere's lattice, especially under a
+    // wireframe overlay whose fill occludes the back wires) reads as glued to
+    // the camera without it. Two-sided so unculled back faces stay sane.
+    if (in.lit > 0.5) {
+        let n = normalize(cross(dpdx(in.world), dpdy(in.world)));
+        let l = normalize(vec3f(-0.45, 0.8, 0.35));
+        let d = abs(dot(n, l));
+        rgb = rgb * (0.62 + 0.38 * d);
+    }
     return vec4f(rgb, cov);
 }