comparison shaders/ntsc.f.glsl @ 2417:9f3008f91bec

Updated Sik's NTSC shader
author Michael Pavone <pavone@retrodev.com>
date Mon, 15 Jan 2024 17:32:20 -0800
parents f1574b22d5d9
children
comparison
equal deleted inserted replaced
2416:a1afe26a8ef0 2417:9f3008f91bec
1 //****************************************************************************** 1 //******************************************************************************
2 // NTSC composite simulator for BlastEm, fixed rainbow frequency edition 2 //***************** FOR PARAMETERS TO ADJUST SEE BELOW THIS! *******************
3 //******************************************************************************
4 // NTSC composite simulator for BlastEm, now with comb filter
3 // Shader by Sik, based on BlastEm's default shader 5 // Shader by Sik, based on BlastEm's default shader
4 // 6 //
5 // Now with gamma correction (NTSC = 2.5 gamma, sRGB = 2.2 gamma*) 7 // Now with gamma correction (NTSC = 2.5 gamma, sRGB = 2.2 gamma*)
6 // *sorta, sRGB isn't exactly a gamma curve, but close enough 8 // *sorta, sRGB isn't exactly a gamma curve, but close enough
7 // 9 //
15 // seems to be enough to give decent filtering (four samples are used for 17 // seems to be enough to give decent filtering (four samples are used for
16 // low-pass filtering, but we need seven because decoding chroma also requires 18 // low-pass filtering, but we need seven because decoding chroma also requires
17 // four samples so we're filtering over overlapping samples... just see the 19 // four samples so we're filtering over overlapping samples... just see the
18 // comments in the I/Q code to understand). 20 // comments in the I/Q code to understand).
19 // 21 //
20 // Thanks to Tulio Adriano for helping adjust the frequency of the banding. 22 // The comb filter works by comparing against the previous scanline (which means
21 //****************************************************************************** 23 // sampling twice). This is done at the composite signal step, i.e. before the
22 24 // filtering to decode back YIQ is performed.
23 uniform mediump float width; 25 //
24 uniform sampler2D textures[2]; 26 // Thanks to Tulio Adriano for helping compare against real hardware on a CRT.
25 uniform mediump vec2 texsize; 27 //******************************************************************************
26 varying mediump vec2 texcoord; 28
27 uniform int curfield; 29 // How strong is the comb filter when reducing crosstalk between Y and IQ.
28 uniform int scanlines; 30 // 0% (0.0) means no separation at all, 100% (1.0) means complete filtering.
31 // 80% seems to approximate a model 1, while 90% is closer to a model 2 or 32X.
32 const mediump float comb_strength = 0.8;
33
34 // Gamma of the TV to simulate.
35 const mediump float gamma_correction = 2.5;
36
37 //******************************************************************************
38
39 // Parameters coming from BlastEm
40 uniform mediump float width; // Screen width (depends on video mode)
41 uniform sampler2D textures[2]; // Raw display for each field
42 uniform mediump vec2 texsize; // Texture size
43 varying mediump vec2 texcoord; // Texture coordinate of current pixel
44 uniform int curfield; // Even or odd field?
45 uniform int scanlines; // Enable scanlines?
29 46
30 // Converts from RGB to YIQ 47 // Converts from RGB to YIQ
31 mediump vec3 rgba2yiq(vec4 rgba) 48 mediump vec3 rgba2yiq(vec4 rgba)
32 { 49 {
33 return vec3( 50 return vec3(
66 // Horizontal distance of half a colorburst cycle 83 // Horizontal distance of half a colorburst cycle
67 mediump float factorX = (1.0 / texsize.x) / 170.667 * 0.5 * width; 84 mediump float factorX = (1.0 / texsize.x) / 170.667 * 0.5 * width;
68 85
69 // sRGB approximates a gamma ramp of 2.2 while NTSC has a gamma of 2.5 86 // sRGB approximates a gamma ramp of 2.2 while NTSC has a gamma of 2.5
70 // Use this value to do gamma correction of every RGB value 87 // Use this value to do gamma correction of every RGB value
71 mediump float gamma = 2.5 / 2.2; 88 mediump float gamma = gamma_correction / 2.2;
72 89
73 // Where we store the sampled pixels. 90 // Where we store the sampled pixels.
74 // [0] = current pixel 91 // [0] = current pixel
75 // [1] = 1/4 colorburst cycles earlier 92 // [1] = 1/4 colorburst cycles earlier
76 // [2] = 2/4 colorburst cycles earlier 93 // [2] = 2/4 colorburst cycles earlier
77 // [3] = 3/4 colorburst cycles earlier 94 // [3] = 3/4 colorburst cycles earlier
78 // [4] = 1 colorburst cycle earlier 95 // [4] = 1 colorburst cycle earlier
79 // [5] = 1 1/4 colorburst cycles earlier 96 // [5] = 1 1/4 colorburst cycles earlier
80 // [6] = 1 2/4 colorburst cycles earlier 97 // [6] = 1 2/4 colorburst cycles earlier
81 mediump float phase[7]; // Colorburst phase (in radians) 98 mediump float phase[7]; // Colorburst phase (in radians)
82 mediump float raw[7]; // Raw encoded composite signal 99 mediump float raw_y[7]; // Luma isolated from raw composite signal
100 mediump float raw_iq[7]; // Chroma isolated from raw composite signal
83 101
84 // Sample all the pixels we're going to use 102 // Sample all the pixels we're going to use
85 for (int n = 0; n < 7; n++, x -= factorX * 0.5) { 103 for (int n = 0; n < 7; n++, x -= factorX * 0.5) {
86 // Compute colorburst phase at this point 104 // Compute colorburst phase at this point
87 phase[n] = x / factorX * 3.1415926; 105 phase[n] = x / factorX * 3.1415926;
88 106
89 // Decode RGB into YIQ and then into composite 107 // Y coordinate one scanline above
90 raw[n] = yiq2raw(rgba2yiq( 108 // Apparently GLSL doesn't allow a vec2 with a full expression for
91 texture2D(textures[curfield], vec2(x, y)) 109 // texture samplers? Whatever, putting it here (also makes the code
92 ), phase[n]); 110 // below a bit easier to read I guess?)
111 mediump float y_above = y - texcoord.y / texsize.y;
112
113 // Get the raw composite data for this scanline
114 mediump float raw1 = yiq2raw(rgba2yiq(
115 texture2D(textures[curfield], vec2(x, y))
116 ), phase[n]);
117
118 // Get the raw composite data for scanline above
119 // Note that the colorburst phase is shifted 180°
120 mediump float raw2 = yiq2raw(rgba2yiq(
121 texture2D(textures[curfield], vec2(x, y_above))
122 ), phase[n] + 3.1415926);
123
124 // Comb filter: isolate Y and IQ using the above two.
125 // Luma is obtained by adding the two scanlines, chroma will cancel out
126 // because chroma will be on opposite phases.
127 // Chroma is then obtained by cancelling this scanline from the luma
128 // to reduce the crosstalk. We don't cancel it entirely though since the
129 // filtering isn't perfect (which is why the rainbow leaks a bit).
130 raw_y[n] = (raw1 + raw2) * 0.5;
131 raw_iq[n] = raw1 - (raw1 + raw2) * (comb_strength * 0.5);
93 } 132 }
94 133
95 // Decode Y by averaging over the last whole sampled cycle (effectively 134 // Decode Y by averaging over the last whole sampled cycle (effectively
96 // filtering anything above the colorburst frequency) 135 // filtering anything above the colorburst frequency)
97 mediump float y_mix = (raw[0] + raw[1] + raw[2] + raw[3]) * 0.25; 136 mediump float y_mix = (raw_y[0] + raw_y[1] + raw_y[2] + raw_y[3]) * 0.25;
98 137
99 // Decode I and Q (see page below to understand what's going on) 138 // Decode I and Q (see page below to understand what's going on)
100 // https://codeandlife.com/2012/10/09/composite-video-decoding-theory-and-practice/ 139 // https://codeandlife.com/2012/10/09/composite-video-decoding-theory-and-practice/
101 // 140 //
102 // Retrieving I and Q out of the raw signal is done like this 141 // Retrieving I and Q out of the raw signal is done like this
124 // 163 //
125 // There are a lot of repeated values there that could be merged into one, 164 // There are a lot of repeated values there that could be merged into one,
126 // what you see below is the resulting simplification. 165 // what you see below is the resulting simplification.
127 166
128 mediump float i_mix = 167 mediump float i_mix =
129 0.125 * raw[0] * sin(phase[0]) + 168 0.125 * raw_iq[0] * sin(phase[0]) +
130 0.25 * raw[1] * sin(phase[1]) + 169 0.25 * raw_iq[1] * sin(phase[1]) +
131 0.375 * raw[2] * sin(phase[2]) + 170 0.375 * raw_iq[2] * sin(phase[2]) +
132 0.5 * raw[3] * sin(phase[3]) + 171 0.5 * raw_iq[3] * sin(phase[3]) +
133 0.375 * raw[4] * sin(phase[4]) + 172 0.375 * raw_iq[4] * sin(phase[4]) +
134 0.25 * raw[5] * sin(phase[5]) + 173 0.25 * raw_iq[5] * sin(phase[5]) +
135 0.125 * raw[6] * sin(phase[6]); 174 0.125 * raw_iq[6] * sin(phase[6]);
136 175
137 mediump float q_mix = 176 mediump float q_mix =
138 0.125 * raw[0] * cos(phase[0]) + 177 0.125 * raw_iq[0] * cos(phase[0]) +
139 0.25 * raw[1] * cos(phase[1]) + 178 0.25 * raw_iq[1] * cos(phase[1]) +
140 0.375 * raw[2] * cos(phase[2]) + 179 0.375 * raw_iq[2] * cos(phase[2]) +
141 0.5 * raw[3] * cos(phase[3]) + 180 0.5 * raw_iq[3] * cos(phase[3]) +
142 0.375 * raw[4] * cos(phase[4]) + 181 0.375 * raw_iq[4] * cos(phase[4]) +
143 0.25 * raw[5] * cos(phase[5]) + 182 0.25 * raw_iq[5] * cos(phase[5]) +
144 0.125 * raw[6] * cos(phase[6]); 183 0.125 * raw_iq[6] * cos(phase[6]);
145 184
146 // Convert YIQ back to RGB and output it 185 // Convert YIQ back to RGB and output it
147 gl_FragColor = pow(yiq2rgba(vec3(y_mix, i_mix, q_mix)), 186 gl_FragColor = pow(yiq2rgba(vec3(y_mix, i_mix, q_mix)),
148 vec4(gamma, gamma, gamma, 1.0)); 187 vec4(gamma, gamma, gamma, 1.0));
149 188