comparison shaders/ntsc.f.glsl @ 2410:f1574b22d5d9

Update Sik's NTSC shader
author Michael Pavone <pavone@retrodev.com>
date Thu, 04 Jan 2024 22:12:03 -0800
parents 49bd818ec9d8
children 9f3008f91bec
comparison
equal deleted inserted replaced
2409:f8ce89498e11 2410:f1574b22d5d9
1 //****************************************************************************** 1 //******************************************************************************
2 // NTSC composite simulator for BlastEm 2 // NTSC composite simulator for BlastEm, fixed rainbow frequency edition
3 // Shader by Sik, based on BlastEm's default shader 3 // Shader by Sik, based on BlastEm's default shader
4 // 4 //
5 // Now with gamma correction (NTSC = 2.5 gamma, sRGB = 2.2 gamma) 5 // Now with gamma correction (NTSC = 2.5 gamma, sRGB = 2.2 gamma*)
6 // *sorta, sRGB isn't exactly a gamma curve, but close enough
6 // 7 //
7 // It works by converting from RGB to YIQ and then encoding it into NTSC, then 8 // It works by converting from RGB to YIQ and then encoding it into NTSC, then
8 // trying to decode it back. The lossy nature of the encoding process results in 9 // trying to decode it back. The lossy nature of the encoding process results in
9 // the rainbow effect. It also accounts for the differences between H40 and H32 10 // the rainbow effect. It also accounts for the differences between H40 and H32
10 // mode as it computes the exact colorburst cycle length. 11 // mode as it computes the exact colorburst cycle length.
13 // pixels by sampling seven points (in 0.25 colorburst cycle intervals), that 14 // pixels by sampling seven points (in 0.25 colorburst cycle intervals), that
14 // seems to be enough to give decent filtering (four samples are used for 15 // seems to be enough to give decent filtering (four samples are used for
15 // low-pass filtering, but we need seven because decoding chroma also requires 16 // low-pass filtering, but we need seven because decoding chroma also requires
16 // four samples so we're filtering over overlapping samples... just see the 17 // four samples so we're filtering over overlapping samples... just see the
17 // comments in the I/Q code to understand). 18 // comments in the I/Q code to understand).
19 //
20 // Thanks to Tulio Adriano for helping adjust the frequency of the banding.
18 //****************************************************************************** 21 //******************************************************************************
19 22
20 uniform mediump float width; 23 uniform mediump float width;
21 uniform sampler2D textures[2]; 24 uniform sampler2D textures[2];
22 uniform mediump vec2 texsize; 25 uniform mediump vec2 texsize;
23 varying mediump vec2 texcoord; 26 varying mediump vec2 texcoord;
27 uniform int curfield;
28 uniform int scanlines;
24 29
25 // Converts from RGB to YIQ 30 // Converts from RGB to YIQ
26 mediump vec3 rgba2yiq(vec4 rgba) 31 mediump vec3 rgba2yiq(vec4 rgba)
27 { 32 {
28 return vec3( 33 return vec3(
49 ); 54 );
50 } 55 }
51 56
52 void main() 57 void main()
53 { 58 {
54 // Use first pair of lines for hard line edges 59 // The coordinate of the pixel we're supposed to access
55 // Use second pair of lines for soft line edges 60 // In interlaced mode, the entire screen is shifted down by half a scanline,
56 mediump float modifiedY0 = (floor(texcoord.y * texsize.y + 0.25) + 0.5) / texsize.y; 61 // y_offset is used to account for this.
57 mediump float modifiedY1 = (floor(texcoord.y * texsize.y - 0.25) + 0.5) / texsize.y; 62 mediump float y_offset = float(curfield) * -0.5 / texsize.y;
58 //mediump float modifiedY0 = (texcoord.y * texsize.y + 0.75) / texsize.y; 63 mediump float x = texcoord.x;
59 //mediump float modifiedY1 = (texcoord.y * texsize.y + 0.25) / texsize.y; 64 mediump float y = texcoord.y + y_offset;
60
61 // Used by the mixing when fetching texels, related to the way BlastEm
62 // handles interlaced mode (nothing to do with composite)
63 mediump float factorY = (sin(texcoord.y * texsize.y * 6.283185307) + 1.0) * 0.5;
64 65
65 // Horizontal distance of half a colorburst cycle 66 // Horizontal distance of half a colorburst cycle
66 mediump float factorX = (1.0 / texsize.x) / 170.667 * 0.5 * (width - 27.0); 67 mediump float factorX = (1.0 / texsize.x) / 170.667 * 0.5 * width;
67 68
68 // sRGB has a gamma of 2.2 while NTSC has a gamma of 2.5 69 // sRGB approximates a gamma ramp of 2.2 while NTSC has a gamma of 2.5
69 // Use this value to do gamma correction of every RGB value 70 // Use this value to do gamma correction of every RGB value
70 mediump float gamma = 2.5 / 2.2; 71 mediump float gamma = 2.5 / 2.2;
71 72
72 // Where we store the sampled pixels. 73 // Where we store the sampled pixels.
73 // [0] = current pixel 74 // [0] = current pixel
79 // [6] = 1 2/4 colorburst cycles earlier 80 // [6] = 1 2/4 colorburst cycles earlier
80 mediump float phase[7]; // Colorburst phase (in radians) 81 mediump float phase[7]; // Colorburst phase (in radians)
81 mediump float raw[7]; // Raw encoded composite signal 82 mediump float raw[7]; // Raw encoded composite signal
82 83
83 // Sample all the pixels we're going to use 84 // Sample all the pixels we're going to use
84 mediump float x = texcoord.x;
85 for (int n = 0; n < 7; n++, x -= factorX * 0.5) { 85 for (int n = 0; n < 7; n++, x -= factorX * 0.5) {
86 // Compute colorburst phase at this point 86 // Compute colorburst phase at this point
87 phase[n] = x / factorX * 3.1415926; 87 phase[n] = x / factorX * 3.1415926;
88 88
89 // Decode RGB into YIQ and then into composite 89 // Decode RGB into YIQ and then into composite
90 // Reading two textures is a BlastEm thing :P (the two fields in 90 raw[n] = yiq2raw(rgba2yiq(
91 // interlaced mode, that's taken as-is from the stock shaders) 91 texture2D(textures[curfield], vec2(x, y))
92 raw[n] = yiq2raw(mix( 92 ), phase[n]);
93 rgba2yiq(texture2D(textures[1], vec2(x, modifiedY1))),
94 rgba2yiq(texture2D(textures[0], vec2(x, modifiedY0))),
95 factorY
96 ), phase[n]);
97 } 93 }
98 94
99 // Decode Y by averaging over the the whole sampled cycle (effectively 95 // Decode Y by averaging over the last whole sampled cycle (effectively
100 // filtering anything above the colorburst frequency) 96 // filtering anything above the colorburst frequency)
101 mediump float y_mix = (raw[0] + raw[1] + raw[2] + raw[3]) * 0.25; 97 mediump float y_mix = (raw[0] + raw[1] + raw[2] + raw[3]) * 0.25;
102 98
103 // Decode I and Q (see page below to understand what's going on) 99 // Decode I and Q (see page below to understand what's going on)
104 // https://codeandlife.com/2012/10/09/composite-video-decoding-theory-and-practice/ 100 // https://codeandlife.com/2012/10/09/composite-video-decoding-theory-and-practice/
152 vec4(gamma, gamma, gamma, 1.0)); 148 vec4(gamma, gamma, gamma, 1.0));
153 149
154 // If you're curious to see what the raw composite signal looks like, 150 // If you're curious to see what the raw composite signal looks like,
155 // comment out the above and uncomment the line below instead 151 // comment out the above and uncomment the line below instead
156 //gl_FragColor = vec4(raw[0], raw[0], raw[0], 1.0); 152 //gl_FragColor = vec4(raw[0], raw[0], raw[0], 1.0);
153
154 // Basic scanlines effect. This is done by multiplying the color against a
155 // "half sine" wave so the center is brighter and a narrow outer area in
156 // each scanline is noticeable darker.
157 // The weird constant in the middle line is 1-sqrt(2)/4 and it's used to
158 // make sure that the average multiplied value across the whole screen is 1
159 // to preserve the original brightness.
160 if (scanlines != 0) {
161 mediump float mult = sin(y * texsize.y * 3.1415926);
162 mult = abs(mult) * 0.5 + 0.646446609;
163 gl_FragColor *= vec4(mult, mult, mult, 1.0);
164 }
157 } 165 }