Mercurial > repos > blastem
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 |