summaryrefslogtreecommitdiff
path: root/shaders/gl_crt_fragment.glsl
blob: 73dcfec548f5e9cdfd41e967d98783053a17e03c (plain)
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
out vec4 outcolor;
in vec2 frag_texture_coord;

uniform vec2 resolution;
uniform vec2 src_image_size;
uniform float brightness;
uniform vec4 tone_data;
uniform bool crt_emulation;
uniform bool apply_mask;
uniform sampler2D iChannel0;

//==============================================================
//                  CRTS SHADER PORTABILITY
//==============================================================
#define CrtsF1 float
#define CrtsF2 vec2
#define CrtsF3 vec3
#define CrtsF4 vec4
#define CrtsFractF1 fract
#define CrtsRcpF1(x) (1.0/(x))
#define CrtsSatF1(x) clamp((x),0.0,1.0)

CrtsF1 CrtsMax3F1(CrtsF1 a, CrtsF1 b, CrtsF1 c) {
	return max(a, max(b, c));
}

//==============================================================
//                    FETCH FUNCTION
//==============================================================
CrtsF3 CrtsFetch(CrtsF2 uv) {
	const float bias = 0.002533333;
	return max(texture(iChannel0, uv, -16.0).rgb, vec3(bias));
}

//==============================================================
//                      PHOSPHOR MASK
//==============================================================
CrtsF3 CrtsMask(CrtsF2 pos, CrtsF1 dark) {
#ifdef CRTS_MASK_GRILLE
	CrtsF3 m = CrtsF3(dark, dark, dark);
	CrtsF1 x = CrtsFractF1(pos.x * (1.0/3.0));
	if(x < (1.0/3.0)) m.r = 1.0;
	else if(x < (2.0/3.0)) m.g = 1.0;
	else m.b = 1.0;
	return m;
#endif

#ifdef CRTS_MASK_GRILLE_LITE
	CrtsF3 m = CrtsF3(1.0, 1.0, 1.0);
	CrtsF1 x = CrtsFractF1(pos.x * (1.0/3.0));
	if(x < (1.0/3.0)) m.r = dark;
	else if(x < (2.0/3.0)) m.g = dark;
	else m.b = dark;
	return m;
#endif

#ifdef CRTS_MASK_NONE
	return CrtsF3(1.0, 1.0, 1.0);
#endif

#ifdef CRTS_MASK_SHADOW
	pos.x += pos.y * 3.0;
	CrtsF3 m = CrtsF3(dark, dark, dark);
	CrtsF1 x = CrtsFractF1(pos.x * (1.0/6.0));
	if(x < (1.0/3.0)) m.r = 1.0;
	else if(x < (2.0/3.0)) m.g = 1.0;
	else m.b = 1.0;
	return m;
#endif
}

//==============================================================
//                      CRTS FILTER
//==============================================================
CrtsF3 CrtsFilter(CrtsF2 ipos, CrtsF2 inputSizeDivOutputSize, CrtsF2 halfInputSize, CrtsF2 rcpInputSize, CrtsF2 rcpOutputSize, CrtsF2 twoDivOutputSize, CrtsF1 inputHeight, CrtsF2 warp, CrtsF1 thin, CrtsF1 blur, CrtsF1 mask, CrtsF4 tone) {
	// Apply warp
	// Convert to {-1 to 1} range
	CrtsF2 pos = ipos * twoDivOutputSize - CrtsF2(1.0, 1.0);
	// Distort pushes image outside {-1 to 1} range
	pos *= CrtsF2(
		1.0 + (pos.y * pos.y) * warp.x,
		1.0 + (pos.x * pos.x) * warp.y);
	// Vignette disabled - use rounded corners in composite shader instead
	CrtsF1 vin = 1.0;
	// Leave in {0 to inputSize}
	pos = pos * halfInputSize + halfInputSize;

	// Snap to center of first scanline
	CrtsF1 y0 = floor(pos.y - 0.5) + 0.5;

	// Snap to center of one of four pixels
	CrtsF1 x0 = floor(pos.x - 1.5) + 0.5;
	// Initial UV position
	CrtsF2 p = CrtsF2(x0 * rcpInputSize.x, y0 * rcpInputSize.y);
	// Fetch 4 nearest texels from 2 nearest scanlines
	CrtsF3 colA0 = CrtsFetch(p);
	p.x += rcpInputSize.x;
	CrtsF3 colA1 = CrtsFetch(p);
	p.x += rcpInputSize.x;
	CrtsF3 colA2 = CrtsFetch(p);
	p.x += rcpInputSize.x;
	CrtsF3 colA3 = CrtsFetch(p);
	p.y += rcpInputSize.y;
	CrtsF3 colB3 = CrtsFetch(p);
	p.x -= rcpInputSize.x;
	CrtsF3 colB2 = CrtsFetch(p);
	p.x -= rcpInputSize.x;
	CrtsF3 colB1 = CrtsFetch(p);
	p.x -= rcpInputSize.x;
	CrtsF3 colB0 = CrtsFetch(p);

	// Vertical filter - Scanline intensity using cosine wave
	CrtsF1 off = pos.y - y0;
	CrtsF1 pi2 = 6.28318530717958;
	CrtsF1 hlf = 0.5;
	CrtsF1 scanA = cos(min(0.5,   off  * thin     ) * pi2) * hlf + hlf;
	CrtsF1 scanB = cos(min(0.5, (-off) * thin + thin) * pi2) * hlf + hlf;

	// Horizontal kernel is simple gaussian filter
	CrtsF1 off0 = pos.x - x0;
	CrtsF1 off1 = off0 - 1.0;
	CrtsF1 off2 = off0 - 2.0;
	CrtsF1 off3 = off0 - 3.0;
	CrtsF1 pix0 = exp2(blur * off0 * off0);
	CrtsF1 pix1 = exp2(blur * off1 * off1);
	CrtsF1 pix2 = exp2(blur * off2 * off2);
	CrtsF1 pix3 = exp2(blur * off3 * off3);
	CrtsF1 pixT = CrtsRcpF1(pix0 + pix1 + pix2 + pix3);
	// Get rid of wrong pixels on edge
	pixT *= vin;
	scanA *= pixT;
	scanB *= pixT;
	// Apply horizontal and vertical filters
	CrtsF3 color =
		(colA0 * pix0 + colA1 * pix1 + colA2 * pix2 + colA3 * pix3) * scanA +
		(colB0 * pix0 + colB1 * pix1 + colB2 * pix2 + colB3 * pix3) * scanB;

	// Apply phosphor mask
	color *= CrtsMask(ipos, mask);

	// Tonal control, start by protecting from /0
	CrtsF1 peak = max(1.0 / (256.0 * 65536.0),
		CrtsMax3F1(color.r, color.g, color.b));
	// Compute the ratios of {R,G,B}
	CrtsF3 ratio = color * CrtsRcpF1(peak);
	// Apply tonal curve to peak value
	peak = pow(peak, tone.x);
	peak = peak * CrtsRcpF1(peak * tone.y + tone.z);
	// Apply saturation
	ratio = pow(ratio, CrtsF3(tone.w, tone.w, tone.w));
	// Reconstruct color
	return ratio * peak;
}

//==============================================================
//                         MAIN
//==============================================================
void main() {
	// Add half_pixel offset to reduce aliasing from warp
	vec2 half_pixel = 0.5 / src_image_size;
	vec2 fragCoord = vec2(frag_texture_coord.x, 1.0 - frag_texture_coord.y) + half_pixel;

	if(crt_emulation) {
		outcolor.rgb = CrtsFilter(
			fragCoord.xy * resolution + vec2(0.5),
			src_image_size / resolution,
			src_image_size * vec2(0.5),
			1.0 / src_image_size,
			1.0 / resolution,
			2.0 / resolution,
			src_image_size.y,
			vec2(1.0 / 24.0, 1.0 / 16.0),
			INPUT_THIN,
			INPUT_BLUR,
			INPUT_MASK,
			tone_data
		);
		outcolor.rgb *= brightness;
	} else {
		outcolor.rgb = texture(iChannel0, fragCoord, -16.0).rgb;
	}

	// Apply rounded corner mask (matching composite shader) only when bloom is disabled
	if(apply_mask) {
		vec2 ipos = frag_texture_coord * resolution;
		vec2 pos = ipos * (2.0 / resolution) - vec2(1.0);
		vec2 warp = vec2(1.0 / 24.0, 1.0 / 16.0);
		pos *= vec2(1.0 + (pos.y * pos.y) * warp.x, 1.0 + (pos.x * pos.x) * warp.y);
		vec2 uv = (pos + vec2(1.0)) * 0.5;

		float corner_radius = 0.05;
		vec2 edge_distance = abs(pos) - vec2(1.0 - corner_radius);
		float dist = length(max(edge_distance, 0.0));
		float edge_softness = 0.003;
		float mask = smoothstep(corner_radius + edge_softness, corner_radius - edge_softness, dist);
		mask *= (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) ? 1.0 : 0.0;

		outcolor = vec4(outcolor.rgb * mask, 1.0);
	} else {
		outcolor = vec4(outcolor.rgb, 1.0);
	}
}