CRT-HYLLIAN.glsl (16362B)
1 // Hyllian's CRT Shader 2 3 // Copyright (C) 2011-2024 Hyllian - sergiogdb@gmail.com 4 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 12 // The above copyright notice and this permission notice shall be included in 13 // all copies or substantial portions of the Software. 14 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 // THE SOFTWARE. 22 23 /* 24 [configuration] 25 26 [OptionRangeFloat] 27 GUIName = HIGH RESOLUTION SCANLINES 28 OptionName = SCANLINES_HIRES 29 MinValue = 0.0 30 MaxValue = 1.0 31 StepAmount = 1.0 32 DefaultValue = 1.0 33 34 [OptionRangeFloat] 35 GUIName =VERTICAL SCANLINES 36 OptionName = VSCANLINES 37 MinValue = 0.0 38 MaxValue = 1.0 39 StepAmount = 1.0 40 DefaultValue = 0.0 41 42 [OptionRangeFloat] 43 GUIName = BEAM PROFILE 44 OptionName = BEAM_PROFILE 45 MinValue = 0.0 46 MaxValue = 2.0 47 StepAmount = 1.0 48 DefaultValue = 0.0 49 50 [OptionRangeFloat] 51 GUIName = HORIZONTAL FILTER PROFILE 52 OptionName = HFILTER_PROFILE 53 MinValue = 0.0 54 MaxValue = 1.0 55 StepAmount = 1.0 56 DefaultValue = 1.0 57 58 [OptionRangeFloat] 59 GUIName = COLOR BOOST 60 OptionName = COLOR_BOOST 61 MinValue = 1.0 62 MaxValue = 3.0 63 StepAmount = 0.05 64 DefaultValue = 1.40 65 66 [OptionRangeFloat] 67 GUIName = SHARPNESS HACK 68 OptionName = SHARPNESS_HACK 69 MinValue = 1.0 70 MaxValue = 4.0 71 StepAmount = 1.0 72 DefaultValue = 1.0 73 74 [OptionRangeFloat] 75 GUIName = PHOSPHOR LAYOUT 76 OptionName = PHOSPHOR_LAYOUT 77 MinValue = 0.0 78 MaxValue = 15.0 79 StepAmount = 1.0 80 DefaultValue = 1.0 81 82 [OptionRangeFloat] 83 GUIName = MASK INTENSITY 84 OptionName = MASK_INTENSITY 85 MinValue = 0.0 86 MaxValue = 1.0 87 StepAmount = 0.05 88 DefaultValue = 0.65 89 90 [OptionRangeFloat] 91 GUIName = MIN BEAM WIDTH 92 OptionName = BEAM_MIN_WIDTH 93 MinValue = 0.0 94 MaxValue = 1.0 95 StepAmount = 0.01 96 DefaultValue = 0.86 97 98 [OptionRangeFloat] 99 GUIName = MAX BEAM WIDTH 100 OptionName = BEAM_MAX_WIDTH 101 MinValue = 0.0 102 MaxValue = 1.0 103 StepAmount = 0.01 104 DefaultValue = 1.0 105 106 [OptionRangeFloat] 107 GUIName = SCANLINES STRENGTH 108 OptionName = SCANLINES_STRENGTH 109 MinValue = 0.0 110 MaxValue = 1.0 111 StepAmount = 0.01 112 DefaultValue = 0.58 113 114 [OptionRangeFloat] 115 GUIName = SCANLINES CUTOFF 116 OptionName = SCANLINES_CUTOFF 117 MinValue = 0.0 118 MaxValue = 1000.0 119 StepAmount = 1.0 120 DefaultValue = 390.0 121 122 [OptionRangeFloat] 123 GUIName = MONITOR SUBPIXELS LAYOUT 124 OptionName = MONITOR_SUBPIXELS 125 MinValue = 0.0 126 MaxValue = 1.0 127 StepAmount = 1.0 128 DefaultValue = 0.0 129 130 [OptionRangeFloat] 131 GUIName = ANTI RINGING 132 OptionName = CRT_ANTI_RINGING 133 MinValue = 0.0 134 MaxValue = 1.0 135 StepAmount = 1.0 136 DefaultValue = 1.0 137 138 [OptionRangeFloat] 139 GUIName = INPUT GAMMA 140 OptionName = CRT_InputGamma 141 MinValue = 1.0 142 MaxValue = 3.0 143 StepAmount = 0.05 144 DefaultValue = 2.4 145 146 [OptionRangeFloat] 147 GUIName = OUTPUT GAMMA 148 OptionName = CRT_OutputGamma 149 MinValue = 1.0 150 MaxValue = 3.0 151 StepAmount = 0.05 152 DefaultValue = 2.2 153 154 [/configuration] 155 */ 156 157 158 #define GAMMA_IN(color) pow(color, vec3(GetOption(CRT_InputGamma), GetOption(CRT_InputGamma), GetOption(CRT_InputGamma))) 159 #define GAMMA_OUT(color) pow(color, vec3(1.0 / GetOption(CRT_OutputGamma), 1.0 / GetOption(CRT_OutputGamma), 1.0 / GetOption(CRT_OutputGamma))) 160 161 const vec3 Y = vec3(0.2627, 0.6780, 0.0593); 162 163 164 // A collection of CRT mask effects that work with LCD subpixel structures for 165 // small details 166 167 // author: hunterk 168 // license: public domain 169 170 // Mask code pasted from subpixel_masks.h. Masks 3 and 4 added. 171 vec3 mask_weights(vec2 coord, float mask_intensity, int phosphor_layout, float monitor_subpixels){ 172 vec3 weights = vec3(1.,1.,1.); 173 float on = 1.; 174 float off = 1.-mask_intensity; 175 vec3 red = monitor_subpixels==1.0 ? vec3(on, off, off) : vec3(off, off, on ); 176 vec3 green = vec3(off, on, off); 177 vec3 blue = monitor_subpixels==1.0 ? vec3(off, off, on ) : vec3(on, off, off); 178 vec3 magenta = vec3(on, off, on ); 179 vec3 yellow = monitor_subpixels==1.0 ? vec3(on, on, off) : vec3(off, on, on ); 180 vec3 cyan = monitor_subpixels==1.0 ? vec3(off, on, on ) : vec3(on, on, off); 181 vec3 black = vec3(off, off, off); 182 vec3 white = vec3(on, on, on ); 183 int w, z = 0; 184 185 // This pattern is used by a few layouts, so we'll define it here 186 vec3 aperture_weights = mix(magenta, green, floor(mod(coord.x, 2.0))); 187 188 if(phosphor_layout == 0) return weights; 189 190 else if(phosphor_layout == 1){ 191 // classic aperture for RGB panels; good for 1080p, too small for 4K+ 192 // aka aperture_1_2_bgr 193 weights = aperture_weights; 194 return weights; 195 } 196 197 else if(phosphor_layout == 2){ 198 // Classic RGB layout; good for 1080p and lower 199 vec3 bw3[3] = vec3[](red, green, blue); 200 201 z = int(floor(mod(coord.x, 3.0))); 202 203 weights = bw3[z]; 204 return weights; 205 } 206 207 else if(phosphor_layout == 3){ 208 // black and white aperture; good for weird subpixel layouts and low brightness; good for 1080p and lower 209 vec3 bw3[3] = vec3[](black, white, black); 210 211 z = int(floor(mod(coord.x, 3.0))); 212 213 weights = bw3[z]; 214 return weights; 215 } 216 217 else if(phosphor_layout == 4){ 218 // reduced TVL aperture for RGB panels. Good for 4k. 219 // aperture_2_4_rgb 220 221 vec3 big_ap_rgb[4] = vec3[](red, yellow, cyan, blue); 222 223 w = int(floor(mod(coord.x, 4.0))); 224 225 weights = big_ap_rgb[w]; 226 return weights; 227 } 228 229 else if(phosphor_layout == 5){ 230 // black and white aperture; good for weird subpixel layouts and low brightness; good for 4k 231 vec3 bw4[4] = vec3[](black, black, white, white); 232 233 z = int(floor(mod(coord.x, 4.0))); 234 235 weights = bw4[z]; 236 return weights; 237 } 238 239 else if(phosphor_layout == 6){ 240 // aperture_1_4_rgb; good for simulating lower 241 vec3 ap4[4] = vec3[](red, green, blue, black); 242 243 z = int(floor(mod(coord.x, 4.0))); 244 245 weights = ap4[z]; 246 return weights; 247 } 248 249 else if(phosphor_layout == 7){ 250 // 2x2 shadow mask for RGB panels; good for 1080p, too small for 4K+ 251 // aka delta_1_2x1_bgr 252 vec3 inverse_aperture = mix(green, magenta, floor(mod(coord.x, 2.0))); 253 weights = mix(aperture_weights, inverse_aperture, floor(mod(coord.y, 2.0))); 254 return weights; 255 } 256 257 else if(phosphor_layout == 8){ 258 // delta_2_4x1_rgb 259 vec3 delta[2][4] = { 260 {red, yellow, cyan, blue}, 261 {cyan, blue, red, yellow} 262 }; 263 264 w = int(floor(mod(coord.y, 2.0))); 265 z = int(floor(mod(coord.x, 4.0))); 266 267 weights = delta[w][z]; 268 return weights; 269 } 270 271 else if(phosphor_layout == 9){ 272 // delta_1_4x1_rgb; dunno why this is called 4x1 when it's obviously 4x2 /shrug 273 vec3 delta1[2][4] = { 274 {red, green, blue, black}, 275 {blue, black, red, green} 276 }; 277 278 w = int(floor(mod(coord.y, 2.0))); 279 z = int(floor(mod(coord.x, 4.0))); 280 281 weights = delta1[w][z]; 282 return weights; 283 } 284 285 else if(phosphor_layout == 10){ 286 // delta_2_4x2_rgb 287 vec3 delta[4][4] = { 288 {red, yellow, cyan, blue}, 289 {red, yellow, cyan, blue}, 290 {cyan, blue, red, yellow}, 291 {cyan, blue, red, yellow} 292 }; 293 294 w = int(floor(mod(coord.y, 4.0))); 295 z = int(floor(mod(coord.x, 4.0))); 296 297 weights = delta[w][z]; 298 return weights; 299 } 300 301 else if(phosphor_layout == 11){ 302 // slot mask for RGB panels; looks okay at 1080p, looks better at 4K 303 vec3 slotmask[4][6] = { 304 {red, green, blue, red, green, blue,}, 305 {red, green, blue, black, black, black}, 306 {red, green, blue, red, green, blue,}, 307 {black, black, black, red, green, blue,} 308 }; 309 310 w = int(floor(mod(coord.y, 4.0))); 311 z = int(floor(mod(coord.x, 6.0))); 312 313 // use the indexes to find which color to apply to the current pixel 314 weights = slotmask[w][z]; 315 return weights; 316 } 317 318 else if(phosphor_layout == 12){ 319 // slot mask for RGB panels; looks okay at 1080p, looks better at 4K 320 vec3 slotmask[4][6] = { 321 {black, white, black, black, white, black,}, 322 {black, white, black, black, black, black}, 323 {black, white, black, black, white, black,}, 324 {black, black, black, black, white, black,} 325 }; 326 327 w = int(floor(mod(coord.y, 4.0))); 328 z = int(floor(mod(coord.x, 6.0))); 329 330 // use the indexes to find which color to apply to the current pixel 331 weights = slotmask[w][z]; 332 return weights; 333 } 334 335 else if(phosphor_layout == 13){ 336 // based on MajorPainInTheCactus' HDR slot mask 337 vec3 slot[4][8] = { 338 {red, green, blue, black, red, green, blue, black}, 339 {red, green, blue, black, black, black, black, black}, 340 {red, green, blue, black, red, green, blue, black}, 341 {black, black, black, black, red, green, blue, black} 342 }; 343 344 w = int(floor(mod(coord.y, 4.0))); 345 z = int(floor(mod(coord.x, 8.0))); 346 347 weights = slot[w][z]; 348 return weights; 349 } 350 351 else if(phosphor_layout == 14){ 352 // same as above but for RGB panels 353 vec3 slot2[4][10] = { 354 {red, yellow, green, blue, blue, red, yellow, green, blue, blue }, 355 {black, green, green, blue, blue, red, red, black, black, black}, 356 {red, yellow, green, blue, blue, red, yellow, green, blue, blue }, 357 {red, red, black, black, black, black, green, green, blue, blue } 358 }; 359 360 w = int(floor(mod(coord.y, 4.0))); 361 z = int(floor(mod(coord.x, 10.0))); 362 363 weights = slot2[w][z]; 364 return weights; 365 } 366 367 else if(phosphor_layout == 15){ 368 // slot_3_7x6_rgb 369 vec3 slot[6][14] = { 370 {red, red, yellow, green, cyan, blue, blue, red, red, yellow, green, cyan, blue, blue}, 371 {red, red, yellow, green, cyan, blue, blue, red, red, yellow, green, cyan, blue, blue}, 372 {red, red, yellow, green, cyan, blue, blue, black, black, black, black, black, black, black}, 373 {red, red, yellow, green, cyan, blue, blue, red, red, yellow, green, cyan, blue, blue}, 374 {red, red, yellow, green, cyan, blue, blue, red, red, yellow, green, cyan, blue, blue}, 375 {black, black, black, black, black, black, black, black, red, red, yellow, green, cyan, blue} 376 }; 377 378 w = int(floor(mod(coord.y, 6.0))); 379 z = int(floor(mod(coord.x, 14.0))); 380 381 weights = slot[w][z]; 382 return weights; 383 } 384 385 386 387 else return weights; 388 } 389 390 // Horizontal cubic filter. 391 // Some known filters use these values: 392 393 // B = 0.5, C = 0.0 => A sharp almost gaussian filter. 394 // B = 0.0, C = 0.0 => Hermite cubic filter. 395 // B = 1.0, C = 0.0 => Cubic B-Spline filter. 396 // B = 0.0, C = 0.5 => Catmull-Rom Spline filter. 397 // B = C = 1.0/3.0 => Mitchell-Netravali cubic filter. 398 // B = 0.3782, C = 0.3109 => Robidoux filter. 399 // B = 0.2620, C = 0.3690 => Robidoux Sharp filter. 400 401 // For more info, see: http://www.imagemagick.org/Usage/img_diagrams/cubic_survey.gif 402 403 mat4x4 get_hfilter_profile() 404 { 405 float bf = 1.0; 406 float cf = 0.0; 407 408 if (GetOption(HFILTER_PROFILE) == 1) {bf = 1.0/3.0; cf = 1.0/3.0;} 409 410 return mat4x4( (-bf - 6.0*cf)/6.0, (3.0*bf + 12.0*cf)/6.0, (-3.0*bf - 6.0*cf)/6.0, bf/6.0, 411 (12.0 - 9.0*bf - 6.0*cf)/6.0, (-18.0 + 12.0*bf + 6.0*cf)/6.0, 0.0, (6.0 - 2.0*bf)/6.0, 412 -(12.0 - 9.0*bf - 6.0*cf)/6.0, (18.0 - 15.0*bf - 12.0*cf)/6.0, (3.0*bf + 6.0*cf)/6.0, bf/6.0, 413 (bf + 6.0*cf)/6.0, -cf, 0.0, 0.0); 414 415 416 } 417 418 419 #define scanlines_strength (4.0*profile.x) 420 #define beam_min_width profile.y 421 #define beam_max_width profile.z 422 #define color_boost profile.w 423 424 425 vec4 get_beam_profile() 426 { 427 vec4 bp = vec4(GetOption(SCANLINES_STRENGTH), GetOption(BEAM_MIN_WIDTH), GetOption(BEAM_MAX_WIDTH), GetOption(COLOR_BOOST)); 428 429 if (BEAM_PROFILE == 1) bp = vec4(0.58, 0.86, 1.00, 1.60); // Catmull-rom 430 if (BEAM_PROFILE == 2) bp = vec4(0.58, 0.72, 1.00, 1.75); // Catmull-rom 431 432 return bp; 433 } 434 435 436 void main() 437 { 438 vec2 vTexCoord = GetCoordinates(); 439 vec2 SourceSize = 1.0 / GetInvNativePixelSize(); // This work with previous build. 440 441 vec4 profile = get_beam_profile(); 442 443 vec2 TextureSize = mix(vec2(SourceSize.x * GetOption(SHARPNESS_HACK), SourceSize.y), vec2(SourceSize.x, SourceSize.y * GetOption(SHARPNESS_HACK)), GetOption(VSCANLINES)); 444 445 vec2 dx = mix(vec2(1.0/TextureSize.x, 0.0), vec2(0.0, 1.0/TextureSize.y), GetOption(VSCANLINES)); 446 vec2 dy = mix(vec2(0.0, 1.0/TextureSize.y), vec2(1.0/TextureSize.x, 0.0), GetOption(VSCANLINES)); 447 448 vec2 pix_coord = vTexCoord.xy*TextureSize.xy - vec2(0.5, 0.5); 449 450 vec2 tc = ( (SCANLINES_HIRES > 0.5) ? (mix(vec2(floor(pix_coord.x), pix_coord.y), vec2(pix_coord.x, floor(pix_coord.y)), GetOption(VSCANLINES)) + vec2(0.5, 0.5)) : (floor(pix_coord) + vec2(0.5, 0.5)) )/TextureSize; 451 452 pix_coord = mix(pix_coord, pix_coord.yx, GetOption(VSCANLINES)); 453 454 vec2 fp = fract(pix_coord); 455 456 vec3 c00 = GAMMA_IN(SampleLocation(tc - dx ).xyz); 457 vec3 c01 = GAMMA_IN(SampleLocation(tc ).xyz); 458 vec3 c02 = GAMMA_IN(SampleLocation(tc + dx ).xyz); 459 vec3 c03 = GAMMA_IN(SampleLocation(tc + 2.0*dx ).xyz); 460 461 vec3 c10 = (SCANLINES_HIRES > 0.5) ? c00 : GAMMA_IN(SampleLocation(tc - dx +dy ).xyz); 462 vec3 c11 = (SCANLINES_HIRES > 0.5) ? c01 : GAMMA_IN(SampleLocation(tc +dy ).xyz); 463 vec3 c12 = (SCANLINES_HIRES > 0.5) ? c02 : GAMMA_IN(SampleLocation(tc + dx +dy ).xyz); 464 vec3 c13 = (SCANLINES_HIRES > 0.5) ? c03 : GAMMA_IN(SampleLocation(tc + 2.0*dx +dy ).xyz); 465 466 mat4x4 invX = get_hfilter_profile(); 467 468 mat4x3 color_matrix0 = mat4x3(c00, c01, c02, c03); 469 mat4x3 color_matrix1 = mat4x3(c10, c11, c12, c13); 470 471 vec4 invX_Px = vec4(fp.x*fp.x*fp.x, fp.x*fp.x, fp.x, 1.0) * invX; 472 vec3 color0 = color_matrix0 * invX_Px; 473 vec3 color1 = color_matrix1 * invX_Px; 474 475 // Get min/max samples 476 vec3 min_sample0 = min(c01,c02); 477 vec3 max_sample0 = max(c01,c02); 478 vec3 min_sample1 = min(c11,c12); 479 vec3 max_sample1 = max(c11,c12); 480 481 // Anti-ringing 482 vec3 aux = color0; 483 color0 = clamp(color0, min_sample0, max_sample0); 484 color0 = mix(aux, color0, GetOption(CRT_ANTI_RINGING) * step(0.0, (c00-c01)*(c02-c03))); 485 aux = color1; 486 color1 = clamp(color1, min_sample1, max_sample1); 487 color1 = mix(aux, color1, GetOption(CRT_ANTI_RINGING) * step(0.0, (c10-c11)*(c12-c13))); 488 489 float pos0 = fp.y; 490 float pos1 = 1 - fp.y; 491 492 vec3 lum0 = mix(vec3(beam_min_width), vec3(beam_max_width), color0); 493 vec3 lum1 = mix(vec3(beam_min_width), vec3(beam_max_width), color1); 494 495 vec3 d0 = scanlines_strength*pos0/(lum0*lum0+0.0000001); 496 vec3 d1 = scanlines_strength*pos1/(lum1*lum1+0.0000001); 497 498 d0 = exp(-d0*d0); 499 d1 = exp(-d1*d1); 500 501 vec3 color = (TextureSize.y <= SCANLINES_CUTOFF) ? (color0*d0+color1*d1) : GAMMA_IN(SampleLocation(vTexCoord).xyz); 502 503 color = color_boost*GAMMA_OUT(color); 504 505 vec2 mask_coords =vTexCoord.xy * GetWindowSize().xy; 506 507 mask_coords = mix(mask_coords.xy, mask_coords.yx, GetOption(VSCANLINES)); 508 509 color.rgb*=GAMMA_OUT(mask_weights(mask_coords, GetOption(MASK_INTENSITY), int(GetOption(PHOSPHOR_LAYOUT)), GetOption(MONITOR_SUBPIXELS))); 510 511 SetOutput(vec4(color, 1.0)); 512 }