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 }