git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

scenefx/render/fx_renderer/shaders/bevel.frag (6.9K)

  1 // Edge bevel: a lit chamfer around the inside of a rounded rect.
  2 //
  3 // The rim is shaded as if the window edge were rolled off toward the surface
  4 // plane, so the side facing the light picks up a highlight and the far side
  5 // falls into shade, with the transition sweeping smoothly around the corner
  6 // arcs. Everything is derived from the signed distance to the rounded-rect
  7 // boundary, which makes the corners fall out for free — the gradient follows
  8 // the curve instead of being mitred at 45 degrees.
  9 
 10 #ifdef GL_FRAGMENT_PRECISION_HIGH
 11 precision highp float;
 12 #else
 13 precision mediump float;
 14 #endif
 15 
 16 varying vec4 v_color;
 17 varying vec2 v_texcoord;
 18 
 19 uniform vec2 position;
 20 uniform vec2 size;
 21 uniform float corner_radius;
 22 // Rim width in px: how far in from the edge the chamfer reaches.
 23 uniform float thickness;
 24 // Unit vector pointing toward the light (screen space, y down).
 25 uniform vec2 light_dir;
 26 // Highlight and shade strengths, 0..1.
 27 uniform float light_intensity;
 28 uniform float shade_intensity;
 29 // Shoulder: how much of the rim is the rounded roll-off vs the flat face.
 30 // 0 = a hard flat chamfer, 1 = fully rounded shoulder.
 31 uniform float shoulder;
 32 // Focus treatment: > 0 wraps the highlight around all four sides in
 33 // focus_color — the DE's focused-plate glint on the window edge. The azimuth
 34 // mask against the light drops, so every edge shades as the lit one; the
 35 // shade term goes with it (an all-around light casts no rim shadow).
 36 uniform float focus;
 37 uniform vec3 focus_color;
 38 
 39 // Falloff exponent for the focus glint. 1.0 is a linear ramp across the whole
 40 // rim, which reads as a wash; higher concentrates the light at the silhouette.
 41 // Costs peak brightness as it rises — the outermost RENDERED sample sits just
 42 // inside t=0 and is feathered by edge_aa, so it is already below 1.0 and the
 43 // power pushes it down (measured peak, green channel: 242 at 1.0, 222 at 3.0,
 44 // 198 at 6.0, 168 at 12.0). Past ~12 the curve stops narrowing and only dims:
 45 // 12 and 24 differ by 2/255. The DE default is 3.0 — a defined edge without
 46 // washing the accent out to white — set by `bevel focus_sharpness=` in
 47 // config.kdl. Only the focus branch reads it.
 48 uniform float focus_sharpness;
 49 
 50 // Defined in corner_alpha.frag, which is concatenated after this source (the
 51 // same way box_shadow.frag gets it). Using the shared routine rather than a
 52 // private circular SDF is the whole point: at corner_shape > 2 the DE's
 53 // corners are superellipses and the compositor hands us the span-WIDENED
 54 // radius that shape needs. Read as a circle, that radius drew an arc twice
 55 // the size of the real corner and the rim peeled ~15px off the edge at the
 56 // diagonal.
 57 float corner_dist(vec2 size, vec2 position,
 58         float radius_tl, float radius_tr, float radius_bl, float radius_br);
 59 
 60 // The rounded rect is symmetric in both axes here (one radius on all four
 61 // corners), so corner_dist's internal y flip cancels and offsetting the
 62 // `position` uniform by d is just a translation of the sample point by -d.
 63 float bevel_dist(vec2 offset) {
 64     return corner_dist(size, position + offset,
 65         corner_radius, corner_radius, corner_radius, corner_radius);
 66 }
 67 
 68 void main() {
 69     float dist = bevel_dist(vec2(0.0));
 70 
 71     // Outside the rect, or deeper in than the rim: nothing to draw. Discard
 72     // before the gradient so the four extra SDF evaluations only run on the
 73     // thin band that actually shades.
 74     float rim = max(thickness, 1.0);
 75     if (dist > 0.0 || dist < -rim) {
 76         discard;
 77     }
 78 
 79     // t: 0 at the outer edge, 1 where the rim meets the flat surface.
 80     float t = clamp(-dist / rim, 0.0, 1.0);
 81 
 82     // The surface normal of the chamfer. Its in-plane part points OUT of the
 83     // rect (the gradient of the SDF), and its steepness falls off across the
 84     // rim — steep at the edge, flat where it meets the face. Sampling at
 85     // -offset means the differences below are already in the same space the
 86     // light direction is authored in.
 87     vec2 grad = normalize(vec2(
 88         bevel_dist(vec2(-1.0, 0.0)) - bevel_dist(vec2(1.0, 0.0)),
 89         bevel_dist(vec2(0.0, -1.0)) - bevel_dist(vec2(0.0, 1.0))
 90     ) + vec2(1e-6));
 91 
 92     // Slope profile across the rim. Mixing linear and smoothstep gives the
 93     // "shoulder" control: a hard chamfer keeps a constant slope, a rounded
 94     // one eases off at both ends.
 95     float slope = mix(1.0 - t, 1.0 - smoothstep(0.0, 1.0, t), shoulder);
 96 
 97     // Lambert against the light, using only the in-plane direction (the
 98     // chamfer's tilt is what varies; the face itself is flat-on).
 99     float facing = dot(grad, normalize(light_dir + vec2(1e-6)));
100 
101     // Feather the very outer pixel so the rim doesn't alias against the
102     // window's own rounded edge.
103     float edge_aa = clamp(-dist, 0.0, 1.0);
104 
105     // Overall scale on the effect: the tint's alpha times the outer feather.
106     float k = v_color.a * edge_aa;
107 
108     if (focus > 0.5) {
109         // Focused window: the familiar lit-edge highlight, wrapped — every
110         // edge shades as the one facing the light, in the accent color, no
111         // shade side. Same slope profile, so band width and shoulder match
112         // the unfocused rim exactly.
113         // Concentrate the band toward the outer edge. The linear ramp
114         // spreads the glint over the whole rim, which reads as a wash; a
115         // power keeps the peak at the silhouette and falls away fast, which
116         // is what makes cce-ui's plate line read as a glint rather than a
117         // glow (shader2d.wgsl raises its profile to `shininess`).
118         float h = light_intensity * pow(slope, max(focus_sharpness, 0.01));
119         // ADDITIVE: alpha 0 with premultiplied colour gives dst + rgb under
120         // the pass's GL_ONE/GL_ONE_MINUS_SRC_ALPHA blend, so the glint ADDS
121         // light to the window instead of cross-fading toward the accent.
122         // Two reasons that matters. A cross-fade ceilings the whole effect at
123         // focus_color and DARKENS the rim wherever it crosses client pixels
124         // brighter than the accent, which is why the old rim dipped below the
125         // window's own value. And the alpha term is gone, so the intensity is
126         // linear in h rather than squared — h peaked at light_intensity (0.6),
127         // so squaring cost the highlight nearly two thirds of its strength.
128         // This is what cce-ui's plate does: shader2d.wgsl adds its specular on
129         // top of the lit fill rather than blending toward it.
130         gl_FragColor = vec4(focus_color * h * k, 0.0);
131         return;
132     }
133 
134     // Positive = lit side, negative = shaded side.
135     float lit = max(facing, 0.0) * light_intensity;
136     float shade = max(-facing, 0.0) * shade_intensity;
137 
138     float highlight = lit * slope;
139     float shadow = shade * slope;
140 
141     // White highlight over dark shade, both premultiplied. v_color carries
142     // the tint (and its alpha scales the whole effect).
143     vec3 rgb = v_color.rgb * highlight;
144     float alpha = highlight + shadow;
145     float a = alpha * k;
146 
147     gl_FragColor = vec4(rgb * k * a, a);
148 }