GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Plate crest is light-facing: the far edges of a raised plate shade dark
The crest — the ambient-catching rim term on a raised plate's perimeter
roll — was a flat +PLATE_CREST on every side, regardless of where the
light is. On the edges facing away from the light that out-measured the
roll's own diffuse fall-off at every point of the profile (about -0.20
at the silhouette against +0.25 of crest with the DE's 135° light), so
a raised plate had a bright rim toward the light and NO dark rim away
from it: the glint had no counterpart.
The crest is now weighted by how squarely the rim faces the light's
azimuth (crest_weight, clamped at zero). Light-facing edges keep their
crest; down-light edges keep only their diffuse shading and fall to
about 0.80 of the face at the silhouette. A light from straight
overhead has no near or far side and keeps the crest everywhere. Both
the plate branch and the fill-less window roll (MODE_ROLL) take the
weight; the droplet's fresnel crest is its own term and is untouched.
relief_shade mirrors the weight so cce-relief's prediction still matches
the shader, with a test that the near edge shades brighter than the
face and the far edge darker.
Verified in a scale-2 shadow on opaque raised plates: at the silhouette
the bottom/right edge of a 196-luma face went from 202 to 184, the top
from 239 to 235. On the designer's frosted panes over a dark viewport
the same shading is only 1-5 luma — a multiply on a ~20-luma face —
which is the face's brightness, not the roll.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/scene/relief_shade.rs | 35 ++++++++++++++++++++++++++++++++++-
src/vk/shader2d.wgsl | 22 ++++++++++++++++++++--
2 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/src/scene/relief_shade.rs b/src/scene/relief_shade.rs
index a6865d0..bc6198d 100644
--- a/src/scene/relief_shade.rs
+++ b/src/scene/relief_shade.rs
@@ -160,6 +160,21 @@ pub fn analytic_roll_slope(f: f32) -> f32 {
}
}
+/// How squarely a rim faces the light's azimuth, 0..1 — the weight on the
+/// plate crest. Mirrors `crest_weight`. The crest used to be a flat
+/// `PLATE_CREST` on every side, which out-measured the far edges' diffuse
+/// fall-off everywhere along the roll, so a raised plate had a lit rim and no
+/// shadowed one; weighted, the down-light edges keep only their diffuse
+/// shading and read as the glint's dark counterpart. A light from straight
+/// overhead has no near or far side and keeps the crest everywhere.
+pub fn crest_weight(facing: [f32; 2], light: [f32; 3]) -> f32 {
+ let m = (light[0] * light[0] + light[1] * light[1]).sqrt();
+ if m < 1e-4 {
+ return 1.0;
+ }
+ ((facing[0] * light[0] + facing[1] * light[1]) / m).max(0.0)
+}
+
/// The plate's own surface colour across its perimeter roll — mode 1, which is
/// NOT the free-carve branch and does not composite like one.
///
@@ -191,7 +206,7 @@ pub fn plate_surface(
let ndl = (n[0] * light[0] + n[1] * light[1] + n[2] * light[2]).max(0.0);
let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * ndl;
// The crest: the ambient-catching convex rim that makes glass read as glass.
- let extra = PLATE_CREST * f * f * f;
+ 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;
Some([
@@ -244,6 +259,24 @@ mod tests {
assert_eq!(wgsl_const("ROLL_CUT"), ROLL_CUT);
}
+ /// The crest is light-facing: at the silhouette the edge toward the light
+ /// shades brighter than the face and the edge away from it darker. Before
+ /// the weight, the flat crest left the far edge at or above the face.
+ #[test]
+ fn far_edge_shades_darker_than_the_face() {
+ let light = light_vector();
+ let mat = Finish::from_style();
+ let base = [0.5f32; 3];
+ let lxy = [light[0], light[1]];
+ let m = (lxy[0] * lxy[0] + lxy[1] * lxy[1]).sqrt();
+ let near = [lxy[0] / m, lxy[1] / m];
+ let far = [-near[0], -near[1]];
+ let lit = plate_surface(base, 1.0, near, &analytic_roll_slope, light, &mat).unwrap();
+ let dark = plate_surface(base, 1.0, far, &analytic_roll_slope, light, &mat).unwrap();
+ assert!(lit[0] > base[0] + 0.02, "near edge {} vs face {}", lit[0], base[0]);
+ assert!(dark[0] < base[0] - 0.02, "far edge {} vs face {}", dark[0], base[0]);
+ }
+
/// The plate's face must come through as exactly the app's colour, or a
/// plate silently recolours whatever it is filled with.
#[test]
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index c8e4e4a..1d81009 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -277,6 +277,24 @@ fn roll_spec_wrap(sv: vec2f) -> f32 {
return rrect_clip.p_mat.y * max(pow(prof, shininess) - pow(hv.z, shininess), 0.0);
}
+// How squarely a rim faces the light's azimuth, 0..1 — the weight on the
+// plate crest. The crest was a flat +PLATE_CREST on every side, and on the
+// far (down-light) edges that out-measured the roll's own diffuse fall-off at
+// every point of the profile: a raised plate had a bright rim toward the
+// light and NO dark rim away from it. Weighted this way the near edges keep
+// their crest and the far edges keep only their diffuse shading, so the
+// silhouette reads lit on one side and shadowed on the other, like the
+// glint's counterpart. Light from straight overhead has no near or far side
+// and keeps the crest everywhere.
+fn crest_weight(facing: vec2f) -> f32 {
+ let lxy = rrect_clip.p_light.xy;
+ let m = length(lxy);
+ if (m < 1e-4) {
+ return 1.0;
+ }
+ return max(dot(facing, lxy) / m, 0.0);
+}
+
// Slope of the raised roll's height profile at f (0 at the face join, 1 at the
// silhouette). Circular (shape 2): a quarter-round h = sqrt(1 - f²) — tangent-
// continuous with the face but with a curvature JUMP at the join (1/t → 0), the
@@ -612,7 +630,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
if (vcol.a < 0.0) {
base = resolve_blur(frag, vcol, sv_rim * (refr * t), refr * f * f, k_plate, stride);
}
- var extra = PLATE_CREST * f * f * f;
+ var extra = PLATE_CREST * f * f * f * crest_weight(gd.xy);
let f_off = u32(rrect_clip.p_host.x);
let f_cnt = u32(rrect_clip.p_host.y);
for (var i = 0u; i < f_cnt; i = i + 1u) {
@@ -686,7 +704,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
let u = clamp(d / t, 0.0, 1.0);
let f = 1.0 - u;
let sv = gd.xy * roll_slope(f);
- let extra = PLATE_CREST * f * f * f;
+ let extra = PLATE_CREST * f * f * f * crest_weight(gd.xy);
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);