1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
#version 450
// The Crt builtin's post-process. Screen point cc shows the scene at
// uv' = cc*(1 + BARREL*r2), and src/gui/crt.zig maps mouse input through that
// same transform — keep them equal or clicks drift near the edges.
layout(set = 2, binding = 0) uniform sampler2D u_scene;
layout(std140, set = 3, binding = 0) uniform CrtUniforms {
float time_seconds;
float frame;
vec2 viewport;
// x = CRT, y = ripple, z = glitch. One scene pass can combine them.
vec4 effects;
} u_crt;
layout(location = 0) out vec4 o_col;
const float TAU = 6.28318530718;
const float BARREL = 0.016; // MUST match src/gui/crt.zig barrel
const float EDGE_SOFTNESS_PX = 3.5;
const float CHROMA_PX = 0.28; // radial R/B separation in physical pixels at r2=1
const float BLOOM_RADIUS_PX = 1.75;
const float BLOOM_THRESHOLD = 0.60; // linear-light channel peak
const float BLOOM_SOFTNESS = 0.25;
const float BLOOM_STRENGTH = 0.045;
const float SCAN_PERIOD_PX = 3.0;
const float SCAN_STRENGTH = 0.055;
const float MASK_PERIOD_PX = 3.0;
const float MASK_STRENGTH = 0.015;
const float VIGNETTE_STRENGTH = 0.11; // 11% only at the extreme corners
const float HUM_STRENGTH = 0.0025;
const float NOISE_STRENGTH = 0.0020;
const float EXPOSURE = 1.012;
vec3 srgbToLinear(vec3 c) {
vec3 low = c / 12.92;
vec3 high = pow((c + 0.055) / 1.055, vec3(2.4));
return mix(high, low, lessThanEqual(c, vec3(0.04045)));
}
vec3 linearToSrgb(vec3 c) {
c = max(c, vec3(0.0));
vec3 low = c * 12.92;
vec3 high = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055;
return mix(high, low, lessThanEqual(c, vec3(0.0031308)));
}
vec3 sceneLinear(vec2 uv) {
return srgbToLinear(texture(u_scene, uv).rgb);
}
vec3 brightPass(vec2 uv) {
vec3 c = sceneLinear(uv);
float peak = max(c.r, max(c.g, c.b));
return c * smoothstep(BLOOM_THRESHOLD, BLOOM_THRESHOLD + BLOOM_SOFTNESS, peak);
}
float noiseAt(vec2 p) {
vec3 q = fract(vec3(p.xyx) * 0.1031);
q += dot(q, q.yzx + 33.33);
return fract((q.x + q.y) * q.z);
}
// The non-CRT scene effects deliberately distort the source coordinates,
// rather than remapping the final colors. Themes retain their palette and the
// effects remain composable with the linear-light CRT treatment below.
vec2 sceneUv(vec2 uv) {
if (u_crt.effects.y > 0.5) {
vec2 centered = uv - 0.5;
float radius = length(centered);
float envelope = 1.0 - smoothstep(0.04, 0.72, radius);
float wave = sin(radius * 42.0 - u_crt.time_seconds * 5.2);
uv += centered / max(radius, 0.00001) * wave * envelope * 0.0032;
}
if (u_crt.effects.z > 0.5) {
float band = floor(uv.y * 96.0);
float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0)));
float tear = smoothstep(0.955, 1.0, seed);
uv.x += (noiseAt(vec2(seed, band)) - 0.5) * tear * 0.020;
}
return uv;
}
// The scene, ALPHA INCLUDED: with a see-through ground the grid arrives
// premultiplied and partly empty, and a pass that dropped .a would turn the
// desktop behind a `*_transparent` theme into opaque black the moment Crt,
// Ripple or Glitch came on.
vec4 sceneSrgb(vec2 uv) {
vec2 warped = sceneUv(uv);
vec4 center = texture(u_scene, warped);
if (u_crt.effects.z <= 0.5) return center;
float band = floor(warped.y * 96.0);
float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0)));
float tear = smoothstep(0.955, 1.0, seed);
vec2 chroma = vec2((0.35 + 1.25 * tear) / u_crt.viewport.x, 0.0);
return vec4(
texture(u_scene, warped + chroma).r,
center.g,
texture(u_scene, warped - chroma).b,
center.a
);
}
vec3 effectSceneLinear(vec2 uv) {
return srgbToLinear(sceneSrgb(uv).rgb);
}
void main() {
vec2 size = u_crt.viewport;
vec2 texel = 1.0 / size;
vec2 cc = gl_FragCoord.xy / size * 2.0 - 1.0;
float r2 = dot(cc, cc);
vec2 uv = cc * (1.0 + BARREL * r2 * u_crt.effects.x) * 0.5 + 0.5;
// Ripple and glitch are useful without the CRT. They only move samples;
// skip every tube-specific light/mask/exposure adjustment.
if (u_crt.effects.x <= 0.5) {
// The scene texture is already sRGB. Avoid a needless transfer-curve
// round trip so a coordinate-only effect preserves theme bytes.
vec4 s = sceneSrgb(uv);
o_col = vec4(clamp(s.rgb, 0.0, 1.0), clamp(s.a, 0.0, 1.0));
return;
}
vec3 col = vec3(0.0);
// Outside the tube nothing is drawn, so nothing is covered either: the
// old opaque black there would have punched a rectangle out of the
// compositor's backdrop.
float alpha = 0.0;
float edge = min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y));
if (edge > 0.0) {
vec3 center = effectSceneLinear(uv);
vec2 chroma = cc * (CHROMA_PX * r2) * texel;
// Each radial sample follows the same ripple/glitch path from its own
// pre-warp coordinate. This is also how the Metal kernel composes the
// effects; sampling a once-warped center here made combined effects
// diverge between the two GUI backends.
col = vec3(
effectSceneLinear(uv + chroma).r,
center.g,
effectSceneLinear(uv - chroma).b
);
vec2 bloom_px = texel * BLOOM_RADIUS_PX;
vec3 bloom = brightPass(uv + vec2(bloom_px.x, 0.0)) +
brightPass(uv - vec2(bloom_px.x, 0.0)) +
brightPass(uv + vec2(0.0, bloom_px.y)) +
brightPass(uv - vec2(0.0, bloom_px.y));
col += bloom * (0.25 * BLOOM_STRENGTH);
// The mask has unit mean and the scan loss stays small; neither needs
// the old global lift that remapped every theme toward white.
float scan_wave = 0.5 + 0.5 * cos(gl_FragCoord.y * TAU / SCAN_PERIOD_PX);
col *= 1.0 - SCAN_STRENGTH * scan_wave * scan_wave;
vec3 mask = 1.0 + MASK_STRENGTH * cos(
gl_FragCoord.x * TAU / MASK_PERIOD_PX + vec3(0.0, TAU / 3.0, 2.0 * TAU / 3.0)
);
col *= mask;
vec2 edge_px = min(uv, 1.0 - uv) * size;
float tube = smoothstep(0.0, EDGE_SOFTNESS_PX, min(edge_px.x, edge_px.y));
float vignette = 1.0 - VIGNETTE_STRENGTH * pow(clamp(r2 * 0.5, 0.0, 1.0), 1.6);
float hum = 1.0 + HUM_STRENGTH * sin((uv.y * 1.35 - u_crt.time_seconds * 0.11) * TAU);
col *= tube * vignette * hum * EXPOSURE;
// The tube's edge falloff dims coverage as well as colour; everything
// else here is a light adjustment and leaves it alone.
alpha = sceneSrgb(uv).a * tube;
float luma = dot(col, vec3(0.2126, 0.7152, 0.0722));
vec2 noise_seed = gl_FragCoord.xy + vec2(u_crt.frame * 17.0, u_crt.frame * 59.0);
col += (noiseAt(noise_seed) - 0.5) * NOISE_STRENGTH * (0.15 + 0.85 * clamp(luma, 0.0, 1.0));
}
o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), clamp(alpha, 0.0, 1.0));
}
|