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

commit8c2f2d334caebcb5c1217fd0247303c333192ad7
parentf3c970be24
authorLucas Galante <[email protected]>
date2026-09-24 21:56
feat(vk): SceneDraw::prelit — a fill whose colours already carry their lighting

The scene pass's fragment shader flat-shades every fill from derivative
normals. A host that wants smooth shading bakes it into vertex colours —
the light is world-fixed, so per-vertex lighting from interpolated normals
is exact and the vertex format needs no normal — and marks the draw prelit
so the shader does not shade it a second time. The flag takes one of the
uniform block's two pad floats; the block stays 112 bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

 src/vk/scene.rs     | 14 ++++++++++++--
 src/vk/scene3d.wgsl |  6 +++++-
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index 24ff899..f10d525 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -57,6 +57,13 @@ pub struct SceneDraw {
     /// contour-following wires) while the fill's slope is exactly the
     /// quantity needed.
     pub wire_base_width: f32,
+    /// FILL draws only: the vertex colours already carry their lighting, so
+    /// the fragment shader skips its derivative-normal flat shading and
+    /// draws them as they are. A host that wants SMOOTH shading bakes it —
+    /// the light is fixed in world space (see `scene3d.wgsl`), so lighting
+    /// per vertex from interpolated normals is exact for a static light,
+    /// and the vertex format needs no normal. False for an ordinary draw.
+    pub prelit: bool,
 }
 
 /// shader_3d.wgsl's uniform block.
@@ -73,7 +80,9 @@ struct SceneUniforms {
     /// normal flat shading, whose screen-space derivatives are degenerate on
     /// line fragments (along-axis only) and light the wires with noise.
     is_wire: f32,
-    _pad: [f32; 2],
+    /// 1.0 on `SceneDraw::prelit` draws: the flat shading is skipped too.
+    prelit: f32,
+    _pad: [f32; 1],
 }
 
 const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
@@ -736,7 +745,8 @@ impl SceneStage {
                 wire_tint: draw.wire_tint,
                 opacity: draw.opacity,
                 is_wire: if draw.wireframe { 1.0 } else { 0.0 },
-                _pad: [0.0; 2],
+                prelit: if draw.prelit { 1.0 } else { 0.0 },
+                _pad: [0.0; 1],
             };
             let offset = (self.uniform_stride as usize) * i;
             mapped[offset..offset + UNIFORM_SIZE as usize]
diff --git a/src/vk/scene3d.wgsl b/src/vk/scene3d.wgsl
index 438295a..14a54ce 100644
--- a/src/vk/scene3d.wgsl
+++ b/src/vk/scene3d.wgsl
@@ -15,6 +15,10 @@ struct Uniforms {
     // screen-space derivatives of a line fragment are along-axis only, so
     // the "normal" is noise and speckles the wires.
     is_wire: f32,
+    // 1 on a draw whose vertex colours are already lit (the host baked
+    // smooth shading from vertex normals against the same world light):
+    // skip the flat shading below so it is not applied twice.
+    prelit: f32,
 }
 
 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -93,7 +97,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
     // 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 && uniforms.is_wire < 0.5) {
+    if (in.lit > 0.5 && uniforms.is_wire < 0.5 && uniforms.prelit < 0.5) {
         let n = normalize(cross(dpdx(in.world), dpdy(in.world)));
         // A strongly AZIMUTHAL light, wrap-shaded. A near-vertical light (or a
         // two-sided |dot|) yields a latitude-dominated / 180-degree-symmetric