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
|
#version 450
// pardes's Shadertoy prefix (docs/render-pipeline.md §6), put before every
// post pass: a bundled one in shaders/post/ at build time, a user's file at
// run time. The uniform names, types and std140 order are ghostty 1.3.2's
// (src/renderer/shaders/shadertoy_prefix.glsl), so a shader written for
// ghostty runs here; only the bindings are SDL GPU's (a fragment shader's
// textures in set 2, its uniform blocks in set 3). Y is down everywhere, as
// on ghostty's Metal renderer: fragCoord, iChannel0's uv and the cursor
// rectangles share one top-left space, and iCurrentCursor is (left x,
// BOTTOM-edge y, width, height) in framebuffer pixels.
layout(set = 3, binding = 0, std140) uniform Globals {
uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;
uniform float iFrameRate;
uniform int iFrame;
uniform float iChannelTime[4];
uniform vec3 iChannelResolution[4];
uniform vec4 iMouse;
uniform vec4 iDate;
uniform float iSampleRate;
uniform vec4 iCurrentCursor;
uniform vec4 iPreviousCursor;
uniform vec4 iCurrentCursorColor;
uniform vec4 iPreviousCursorColor;
uniform int iCurrentCursorStyle;
uniform int iPreviousCursorStyle;
uniform int iCursorVisible;
uniform float iTimeCursorChange;
uniform float iTimeFocus;
uniform int iFocus;
uniform vec3 iPalette[256];
uniform vec3 iBackgroundColor;
uniform vec3 iForegroundColor;
uniform vec3 iCursorColor;
uniform vec3 iCursorText;
uniform vec3 iSelectionForegroundColor;
uniform vec3 iSelectionBackgroundColor;
};
// pardes's own: 1 when the window is opaque, so a pass cannot punch holes
// in it through its alpha.
layout(set = 3, binding = 1, std140) uniform PardesPost {
float pardesOpaque;
};
#define CURSORSTYLE_BLOCK 0
#define CURSORSTYLE_BLOCK_HOLLOW 1
#define CURSORSTYLE_BAR 2
#define CURSORSTYLE_UNDERLINE 3
#define CURSORSTYLE_LOCK 4
layout(set = 2, binding = 0) uniform sampler2D iChannel0;
layout(location = 0) out vec4 _fragColor;
#define texture2D texture
void mainImage(out vec4 fragColor, in vec2 fragCoord);
void main() {
mainImage(_fragColor, gl_FragCoord.xy);
if (pardesOpaque > 0.5) _fragColor.a = 1.0;
}
// The shader's own lines count from 1, so glslc's errors name them.
#line 1
// Crt: a slot-mask tube's light, with nothing that moves a pixel. There is
// no barrel: every click lands where it is drawn, rails and grips at the
// window's edges included. What is left is light: a beam profile for the
// scanlines, an aperture mask, a soft bloom of the brightest ink, a faint
// vignette and the hum of a live tube. Everything is worked out in linear
// light and dithered back into 8 bits, so the vignette never bands. It is a
// Shadertoy file: pardes puts its prefix before it (shaders/post/prefix.glsl).
const float TAU = 6.28318530718;
const float CHROMA_PX = 0.35; // red and blue apart, at the corners
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.05;
const float SCAN_PERIOD_PX = 3.0;
const float SCAN_STRENGTH = 0.07;
const float MASK_PERIOD_PX = 3.0;
const float MASK_STRENGTH = 0.018;
const float VIGNETTE_STRENGTH = 0.12; // at the far corners only
const float HUM_STRENGTH = 0.004;
const float HUM_HZ = 0.11;
const float EXPOSURE = 1.015;
vec3 toLinear(vec3 c) {
return mix(pow((c + 0.055) / 1.055, vec3(2.4)), c / 12.92, lessThanEqual(c, vec3(0.04045)));
}
vec3 toSrgb(vec3 c) {
c = max(c, vec3(0.0));
return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, c * 12.92, lessThanEqual(c, vec3(0.0031308)));
}
vec3 scene(vec2 uv) {
return toLinear(texture(iChannel0, uv).rgb);
}
vec3 brightPass(vec2 uv) {
vec3 c = scene(uv);
float peak = max(c.r, max(c.g, c.b));
return c * smoothstep(BLOOM_THRESHOLD, BLOOM_THRESHOLD + BLOOM_SOFTNESS, peak);
}
// Interleaved gradient noise: a dither with no visible pattern.
float ign(vec2 p) {
return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715))));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 size = iResolution.xy;
vec2 texel = 1.0 / size;
vec2 uv = fragCoord / size;
vec2 cc = uv * 2.0 - 1.0;
float r2 = dot(cc, cc);
// The beam converges least at the corners: red and blue drift apart
// there, by a third of a pixel.
vec2 chroma = cc * (CHROMA_PX * r2 * 0.5) * texel;
vec3 col = vec3(scene(uv + chroma).r, scene(uv).g, scene(uv - chroma).b);
vec2 b = texel * BLOOM_RADIUS_PX;
vec3 bloom = brightPass(uv + vec2(b.x, 0.0)) + brightPass(uv - vec2(b.x, 0.0)) +
brightPass(uv + vec2(0.0, b.y)) + brightPass(uv - vec2(0.0, b.y));
col += bloom * (0.25 * BLOOM_STRENGTH);
// A beam is brightest on its line and falls off between lines; the
// mask is three phosphor stripes a triad. Both average to one, so the
// theme keeps its brightness.
float line = 0.5 + 0.5 * cos(fragCoord.y * TAU / SCAN_PERIOD_PX);
col *= 1.0 - SCAN_STRENGTH * (line * line - 0.375) / 0.625;
col *= 1.0 + MASK_STRENGTH * cos(fragCoord.x * TAU / MASK_PERIOD_PX + vec3(0.0, TAU / 3.0, 2.0 * TAU / 3.0));
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 - iTime * HUM_HZ) * TAU);
col *= vignette * hum * EXPOSURE;
vec3 srgb = toSrgb(col);
srgb += (ign(fragCoord + float(iFrame % 64) * vec2(5.588238, 5.588238)) - 0.5) / 255.0;
fragColor = vec4(clamp(srgb, 0.0, 1.0), texture(iChannel0, uv).a);
}
|