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

commit1c40e60459a59a3bc94d5601f63681c0cc34d8b5
parentf2fc3c8e03
authorLucas Galante <[email protected]>
date2026-09-21 10:18
Plate roll: a shade line on the far edges, the glint's dark counterpart

With the crest light-facing (f2fc3c8) the down-light edges of a raised
plate shade by their diffuse fall-off alone, and that cannot answer the
glint: under the ambient floor and the truncated roll the far edge
bottoms out near 0.8 of the face, and only in its last, anti-aliased
pixel, while the glint adds ~0.17 of white over a band. On the
designer's frosted panes — a ~20-luma face over a dark viewport — that
was 1 to 5 luma: a plate read lit on one side and merely unlit on the
other, never shadowed.

roll_shade_line is roll_spec evaluated under the light's azimuth
mirrored: the same decoupled lobe, same inset, width and peak, on the
edges facing away from the light, subtracted in colour units exactly
where the glint is added, scaled by PLATE_SHADE_LINE. roll_spec and the
new function share roll_lobe. Applied on the neutral plate return and
on the fill-less window roll; the accent-tinted (focused) plate keeps
its wrap ring untouched, since a dark line under the far half of the
ring would only dim it.

PLATE_SHADE_LINE is 0.5, not the exact mirror: the subtraction is in
linear light, and at 1.0 a dark face (sRGB 119) hit black at the
silhouette. At 0.5 the same edge sits at 77 and a 196 face at 169,
against 111 and 184 with the crest change alone. Verified in a scale-2
shadow on opaque plates and on the designer, where the panes and nodes
now carry a visible dark rim on the bottom and right over the dark
viewport.

relief_shade mirrors the function and the constant, which the
shader-source constants test now pins.

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

 src/scene/relief_shade.rs | 14 +++++++++++++-
 src/vk/shader2d.wgsl      | 29 ++++++++++++++++++++++++++---
 2 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/src/scene/relief_shade.rs b/src/scene/relief_shade.rs
index bc6198d..4fb4411 100644
--- a/src/scene/relief_shade.rs
+++ b/src/scene/relief_shade.rs
@@ -33,6 +33,9 @@ pub const RECESS_DEPTH: f32 = 0.6;
 /// Amplitude of the bright crest hugging a raised plate's silhouette.
 /// Mirrors `PLATE_CREST`.
 pub const PLATE_CREST: f32 = 0.25;
+/// The far-edge shade line's strength relative to the glint. Mirrors
+/// `PLATE_SHADE_LINE`.
+pub const PLATE_SHADE_LINE: f32 = 0.5;
 /// The roll's descent is truncated at this fraction of the quadrant, so the
 /// profile ends on a bounded slope instead of plunging vertical at the
 /// silhouette. Mirrors `ROLL_CUT`.
@@ -95,6 +98,14 @@ pub fn roll_spec(sv: [f32; 2], light: [f32; 3], mat: &Finish) -> f32 {
     mat.spec * (prof.powf(mat.shininess) - hv[2].powf(mat.shininess)).max(0.0) * az * az
 }
 
+/// The glint's dark counterpart: [`roll_spec`] under the light's azimuth
+/// mirrored, so the same lobe lands on the edges facing away from the light,
+/// scaled by [`PLATE_SHADE_LINE`]. Mirrors `roll_shade_line`. Subtracted in
+/// colour units where the glint is added.
+pub fn roll_shade_line(sv: [f32; 2], light: [f32; 3], mat: &Finish) -> f32 {
+    roll_spec(sv, [-light[0], -light[1], light[2]], mat) * PLATE_SHADE_LINE
+}
+
 /// The signed shading value one carve contributes at `u` across its wall —
 /// the shader's `v`, before the tint branch. Positive is a white screen over
 /// what is beneath, negative a black multiply; magnitude is the alpha.
@@ -208,7 +219,7 @@ pub fn plate_surface(
     // The crest: the ambient-catching convex rim that makes glass read as glass.
     let extra = PLATE_CREST * f * f * f * crest_weight(facing, light);
     let shade = 1.0 + (diff / flat_shade(light) - 1.0 + extra) * mat.strength;
-    let spec = roll_spec(sv, light, mat) * mat.strength;
+    let spec = (roll_spec(sv, light, mat) - roll_shade_line(sv, light, mat)) * mat.strength;
     Some([
         (base[0] * shade + spec).clamp(0.0, 1.0),
         (base[1] * shade + spec).clamp(0.0, 1.0),
@@ -257,6 +268,7 @@ mod tests {
         assert_eq!(wgsl_const("RECESS_DEPTH"), RECESS_DEPTH);
         assert_eq!(wgsl_const("PLATE_CREST"), PLATE_CREST);
         assert_eq!(wgsl_const("ROLL_CUT"), ROLL_CUT);
+        assert_eq!(wgsl_const("PLATE_SHADE_LINE"), PLATE_SHADE_LINE);
     }
 
     /// The crest is light-facing: at the silhouette the edge toward the light
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 1d81009..e11ffb4 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -168,6 +168,9 @@ const PLATE_AMBIENT: f32 = 0.55;
 // Amplitude of the bright crest line hugging a raised plate's silhouette — the
 // ambient-catching convex rim that makes glass read as glass.
 const PLATE_CREST: f32 = 0.25;
+// The far-edge shade line's strength relative to the glint (roll_shade_line):
+// 1 is the exact mirror; 0.5 keeps dark faces from bottoming out at black.
+const PLATE_SHADE_LINE: f32 = 0.5;
 // Recess depth as a fraction of the roll width (a recess is visually shallower
 // than a raised plate's full quarter-round).
 const RECESS_DEPTH: f32 = 0.6;
@@ -243,11 +246,29 @@ fn rr_sdf_grad(p: vec2f, prect: vec4f, pradii: vec4f) -> vec3f {
 // `sv` is the surface's slope vector — the horizontal part of the unnormalized
 // normal (-∇height, 1): its magnitude is the tilt, its direction the facing.
 fn roll_spec(sv: vec2f) -> f32 {
+    return roll_lobe(sv, rrect_clip.p_light.xyz);
+}
+
+// The glint's dark counterpart: the SAME decoupled lobe — same inset, width
+// and peak — on the edges facing AWAY from the light, which is roll_spec
+// evaluated under the light's azimuth mirrored. Subtracted in colour units
+// exactly as the glint is added, scaled by PLATE_SHADE_LINE. It exists
+// because the diffuse fall-off alone cannot answer the glint: with the
+// ambient floor and the truncated roll the far edge bottoms out near 0.8 of
+// the face, and only in its last pixel, while the glint adds ~0.17 of white
+// over a band — so a raised plate read lit on one side and merely unlit on
+// the other, never shadowed.
+fn roll_shade_line(sv: vec2f) -> f32 {
+    let l = rrect_clip.p_light.xyz;
+    return roll_lobe(sv, vec3f(-l.xy, l.z)) * PLATE_SHADE_LINE;
+}
+
+fn roll_lobe(sv: vec2f, light: vec3f) -> f32 {
     let m = length(sv);
     if (m < 1e-5) {
         return 0.0;
     }
-    let hv = normalize(rrect_clip.p_light.xyz + vec3f(0.0, 0.0, 1.0));
+    let hv = normalize(light + vec3f(0.0, 0.0, 1.0));
     let shininess = rrect_clip.p_mat.z;
     let facing = sv / m;
     let cos_t = inverseSqrt(1.0 + m * m);
@@ -685,7 +706,8 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
             );
         }
         let spec = roll_spec(sv);
-        return vec4f(base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), abs(base.a) * aa);
+        let dark = roll_shade_line(sv);
+        return vec4f(base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength) - vec3f(dark * strength), abs(base.a) * aa);
     }
 
     if (mode == MODE_ROLL) {
@@ -708,7 +730,8 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         let n = normalize(vec3f(sv, 1.0));
         let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
         let spec = roll_spec(sv);
-        let v = (diff / flat_shade - 1.0 + extra + spec) * strength * aa;
+        let dark = roll_shade_line(sv);
+        let v = (diff / flat_shade - 1.0 + extra + spec - dark) * strength * aa;
         if (v >= 0.0) {
             return vec4f(1.0, 1.0, 1.0, min(v, 1.0));
         }