forked from Themaister/slang-shaders
-
Notifications
You must be signed in to change notification settings - Fork 97
/
Copy pathcrt-beam-simulator.slang
462 lines (400 loc) · 22.8 KB
/
crt-beam-simulator.slang
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
#version 450
/*********************************************************************************************************************/
//
// Blur Busters CRT Beam Simulator BFI
// With Seamless Gamma Correction
//
// From Blur Busters Area 51 Display Science, Research & Engineering
// https://www.blurbusters.com/area51
//
// The World's First Realtime Blur-Reducing CRT Simulator
// Best for 60fps on 240-480Hz+ Displays, Still Works on 120Hz+ Displays
// Original Version 2022. Publicly Released 2024.
//
// CREDIT: Teamwork of Mark Rejhon @BlurBusters & Timothy Lottes @NOTimothyLottes
// Gamma corrected CRT simulator in a shader using clever formula-by-scanline trick
// (easily can generate LUTs, for other workflows like FPGAs or Javascript)
// - @NOTimothyLottes provided the algorithm for per-pixel BFI (Variable MPRT, higher MPRT for bright pixels)
// - @BlurBusters provided the algorithm for the CRT electron beam (2022, publicly released for first time)
//
// Contact Blur Busters for help integrating this in your product (emulator, fpga, filter, display firmware, video processor)
//
// This new algorithm has multiple breakthroughs:
//
// - Seamless; no banding*! (*Monitor/OS configuration: SDR=on, HDR=off, ABL=off, APL=off, gamma=2.4)
// - Phosphor fadebehind simulation in rolling scan.
// - Works on LCDs and OLEDs.
// - Variable per-pixel MPRT. Spreads brighter pixels over more refresh cycles than dimmer pixels.
// - No image retention on LCDs or OLEDs.
// - No integer divisor requirement. Recommended but not necessary (e.g. 60fps 144Hz works!)
// - Gain adjustment (less motion blur at lower gain values, by trading off brightness)
// - Realtime (for retro & emulator uses) and slo-mo modes (educational)
// - Great for softer 60Hz motion blur reduction, less eyestrain than classic 60Hz BFI/strobe.
// - Algorithm can be ported to shader and/or emulator and/or FPGA and/or display firmware.
//
// For best real time CRT realism:
//
// - Reasonably fast performing GPU (many integrated GPUs are unable to keep up)
// - Fastest GtG pixel response (A settings-modified OLED looks good with this algorithm)
// - As much Hz per CRT Hz! (960Hz better than 480Hz better than 240Hz)
// - Integer divisors are still better (just not mandatory)
// - Brightest SDR display with linear response (no ABL, no APL), as HDR boost adds banding
// (unless you can modify the firmware to make it linear brightness during a rolling scan)
//
// *** IMPORTANT ***
// *** DISPLAY REQUIREMENTS ***
//
// - Best for gaming LCD or OLED monitors with fast pixel response.
// - More Hz per simulated CRT Hz is better (240Hz, 480Hz simulates 60Hz tubes more accurately than 120Hz).
// - OLED (SDR mode) looks better than LCD, but still works on LCD
// - May have minor banding with very slow GtG, asymmetric-GtG (VA LCDs), or excessively-overdriven.
// - Designed for sample & hold displays with excess refresh rate (LCDs and OLEDs);
// Not intended for use with strobed or impulsed displays. Please turn off your displays' BFI/strobing.
// This is because we need 100% software control of the flicker algorithm to simulate a CRT beam.
//
// SDR MODE RECOMMENDED FOR NOW (Due to predictable gamma compensation math)
//
// - Best results occur on display configured to standard SDR gamma curve and ABL/APL disabled to go 100% bandfree
// - Please set your display gamma to 2.2 or 2.4, turn off ABL/APL in display settings, and set your OLED to SDR mode.
// - Will NOT work well with some FALD and MiniLED due to backlight lagbehind effects.
// - Need future API access to OLED ABL/ABL algorithm to compensate for OLED ABL/APL windowing interference with algorithm.
// - This code is heavily commented because of the complexity of the algorithm.
//
/*********************************************************************************************************************/
//
// MIT License
//
// Copyright 2024 Mark Rejhon (@BlurBusters) & Timothy Lottes (@NOTimothyLottes)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/*********************************************************************************************************************/
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
uint TotalSubFrames;
uint CurrentSubFrame;
float GAIN_VS_BLUR, GAMMA, FPS_DIVISOR, LCD_ANTI_RETENTION_TOGGLE, POS_MOD;
} params;
//------------------ Constants migrated to runtime params --------------------//
// Brightness-vs-motionblur tradeoff for bright pixel.
// - Defacto simulates fast/slow phosphor.
// - 1.0 is unchanged brightness (same as non-CRT, but no blur reduction for brightest pixels, only for dimmer pixels).
// - 0.5 is half brightness spread over fewer frames (creates lower MPRT persistence for darker pixels).
// - ~0.7 recommended for 240Hz+, ~0.5 recommended for 120Hz due to limited inHz:outHz ratio.
#pragma parameter GAIN_VS_BLUR "Brightness vs Clarity" 0.7 0.01 1.0 0.1
#define GAIN_VS_BLUR params.GAIN_VS_BLUR
// Your display's gamma value. Necessary to prevent horizontal-bands artifacts.
#pragma parameter GAMMA "Gamma" 2.4 1.0 5.0 0.01
#define GAMMA params.GAMMA
// Reduced frame rate mode
// - This can be helpful to see individual CRT-simulated frames better (educational!)
// - 1.0 is framerate=Hz, 0.5 is framerate being half of Hz, 0.1 is framerate being 10% of real Hz.
#pragma parameter FPS_DIVISOR "FPS Divisor (debug)" 1.0 1.0 10.0 1.0
#define FPS_DIVISOR (1. / params.FPS_DIVISOR)
//^^ Slow down or speed up the simulation
#pragma parameter LCD_ANTI_RETENTION_TOGGLE "LCD Anti-Retention On/Off" 1.0 0.0 1.0 1.0
bool LCD_ANTI_RETENTION = bool(params.LCD_ANTI_RETENTION_TOGGLE);
#pragma parameter POS_MOD "Raster Position Mod" 0.0 -10.0 10.0 0.01
#define POS_MOD params.POS_MOD
//----------------------------------------------------------------------------//
//-----------------------Shadertoy Compatibility Macros-----------------------//
// We include these definitions to assist other environments (untested)
#define iResolution params.OutputSize.xy // viewport resolution (in pixels)
#define iTime (params.FrameCount / 60.0) // shader playback time (in seconds)
//uniform float iTimeDelta; // render time (in seconds)
#define iFrameRate (params.TotalSubFrames * 60.0) // shader frame rate
#define iFrame (params.FrameCount * params.TotalSubFrames + params.CurrentSubFrame) // shader playback frame
//uniform float iChannelTime[4]; // channel playback time (in seconds)
//uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
//uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
#define iChannel0 Source // input channel 0
//uniform sampler2D iChannel1; // input channel 1
//uniform sampler2D iChannel2; // input channel 2
//uniform sampler2D iChannel3; // input channel 3
//uniform vec4 iDate; // (year, month, day, time in seconds)
#define fragColor FragColor // output
#define fragCoord (vTexCoord.xy * params.OutputSize.xy) // more consistent than gl_FragCoord
//----------------------------------------------------------------------------//
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
} global;
//------------------------------------------------------------------------------------------------
// Constants Definitions
// Play with the documented constants!
// - REALTIME: Use FRAMES_PER_HZ=4 for 240Hz and FRAMES_PER_HZ=8 for 480Hz, to simulate a 60Hz tube in realtime
// - SLOMO: Use crazy large FRAMES_PER_HZ numbers to watch a CRT tube like a slo-motion video. Try FRAMES_PER_HZ=100!
// - FRAMESTEP: Use low frame rates to inspect frames. Try FRAMES_PER_HZ=8 and FPS_DIVISOR=0.02!
// All are floats (keep a .0 for integers)
#define MOTION_SPEED 10.0
// Ratio of native Hz per CRT Hz. More native Hz per CRT Hz simulates CRT better.
// - Use 4.0 for 60fps at 240Hz realtime.
// - Use 2.4 for 60fps at 144Hz realtime.
// - Use 2.75 for 60fps at 165Hz realtime.
// - Use ~100 for super-slo-motion.
// - Best to keep it integer divisor but not essential (works!)
#define FRAMES_PER_HZ params.TotalSubFrames
//^^automating this to work with whatever the user's subframe setting is
// Splitscreen versus mode for comparing to non-CRT-simulated
#define SPLITSCREEN 0 // 1 to enable splitscreen to compare to non-CRT, 0 to disable splitscreen
#define SPLITSCREEN_X 0.50 // For user to compare; horizontal splitscreen percentage (0=verticals off, 0.5=left half, 1=full sim).
#define SPLITSCREEN_Y 0.00 // For user to compare; vertical splitscreen percentage (0=horizontal off, 0.5=bottom half, 1=full sim).
#define SPLITSCREEN_BORDER_PX 2 // Splitscreen border thickness in pixels
#define SPLITSCREEN_MATCH_BRIGHTNESS 1 // 1 to match brightness of CRT, 0 for original brightness of original frame
// LCD SAVER SYSTEM
// - Prevents image retention from BFI interfering with LCD voltage polarity inversion algorithm
// - When LCD_ANTI_RETENTION is enabled:
// - Automatically prevents FRAMES_PER_HZ from remaining an even integer by conditionally adding a slew float.
// - FRAMES_PER_HZ 2 becomes 2.001, 4 becomes 4.001, and 6 becomes 6.001, etc.
// - Scientific Reason: https://forums.blurbusters.com/viewtopic.php?t=7539 BFI interaction with LCD voltage polarity inversion
// - Known Side effect: You've decoupled the CRT simulators' own VSYNC from the real displays' VSYNC. But magically, there's no tearing artifacts :-)
// - Not needed for OLEDs, safe to turn off, but should be ON by default to be foolproof.
//#define LCD_ANTI_RETENTION true
//^^moved to runtime parameter
#define LCD_INVERSION_COMPENSATION_SLEW 0.001
// CRT SCAN DIRECTION. Can be useful to counteract an OS rotation of your display
// - 1 default (top to bottom), recommended
// - 2 reverse (bottom to top)
// - 3 portrait (left to right)
// - 4 reverse portrait (right to left)
#define SCAN_DIRECTION 2
//^^ compensate for RetroArch's weird approach to origin by reversing the scan
//-------------------------------------------------------------------------------------------------
// Utility Macros
#define clampPixel(a) clamp(a, vec3(0.0), vec3(1.0))
// Selection Function: Returns 'b' if 'p' is true, else 'a'.
float SelF1(float a, float b, bool p) { return p ? b : a; }
#define IS_INTEGER(x) (floor(x) == x)
#define IS_EVEN_INTEGER(x) (IS_INTEGER(x) && IS_INTEGER(x/2.0))
// LCD SAVER (prevent image retention)
// Adds a slew to FRAMES_PER_HZ when ANTI_RETENTION is enabled and FRAMES_PER_HZ is an exact even integer.
// We support non-integer FRAMES_PER_HZ, so this is a magically convenient solution
float EFFECTIVE_FRAMES_PER_HZ = (LCD_ANTI_RETENTION && IS_EVEN_INTEGER(float(FRAMES_PER_HZ)))
? float(FRAMES_PER_HZ) + LCD_INVERSION_COMPENSATION_SLEW
: float(FRAMES_PER_HZ);
//-------------------------------------------------------------------------------------------------
// sRGB Encoding and Decoding Functions, to gamma correct/uncorrect
// Encode linear color to sRGB. (applies gamma curve)
float linear2srgb(float c){
vec3 j = vec3(0.0031308 * 12.92, 12.92, 1.0 / GAMMA);
vec2 k = vec2(1.055, -0.055);
return clamp(j.x, c * j.y, pow(c, j.z) * k.x + k.y);
}
vec3 linear2srgb(vec3 c){
return vec3(linear2srgb(c.r), linear2srgb(c.g), linear2srgb(c.b));
}
// Decode sRGB color to linear. (undoes gamma curve)
float srgb2linear(float c){
vec3 j = vec3(0.04045, 1.0 / 12.92, GAMMA);
vec2 k = vec2(1.0 / 1.055, 0.055 / 1.055);
return SelF1(c * j.y, pow(c * k.x + k.y, j.z), c > j.x);
}
vec3 srgb2linear(vec3 c){
return vec3(srgb2linear(c.r), srgb2linear(c.g), srgb2linear(c.b));
}
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
//------------------------------------------------------------------------------------------------
// Gets pixel from the unprocessed framebuffer.
//
// Placeholder for accessing the 3 trailing unprocessed frames (for simulating CRT on)
// - Frame counter represents simulated CRT refresh cycle number.
// - Always assign numbers to your refresh cycles. For reliability, keep a 3 frame trailing buffer.
// - We index by frame counter because it is necessary for blending adjacent CRT refresh cycles,
// for the phosphor fade algorithm on old frame at bottom, and new frames at top.
// - Framebuffer to retrieve from should be unscaled (e.g. original game resolution or emulator resolution).
// - (If you do optional additional processing such as scaling+scanlines+masks, do it post-processing after this stage)
// DEMO version:
// - We cheat by horizontally shifting shifted pixel reads from a texture.
// PRODUCTION version:
// - Put your own code to retrieve a pixel from your series of unprocessed frame buffers.
// IMPORTANT: For integration into firmware/software/emulators/games, this must be executed
// at refresh cycle granularity independently of your underlying games' framerate!
// There are three independent frequencies involved:
// - Native Hz (your actual physical display)
// - Simulated CRT Hz (Hz of simulated CRT tube)
// - Underlying content frame rate (this shader doesn't need to know; TODO: Unless you plan to simulate VRR-CRT)
//
vec3 getPixelFromOrigFrame(vec2 uv, float getFromHzNumber, float currentHzCounter)
{
// We simulate missing framebuffers (for accurate real world case)
if ((getFromHzNumber > currentHzCounter) || // Frame not rendered yet
(getFromHzNumber < currentHzCounter - 2.0)) { // Frame over 3 frames ago
return vec3(0.0, 0.0, 0.0);
}
// Continuous horizontal shift depending on hzCounter
float shiftAmount = 1.0;//MOTION_SPEED / 1000.0;
//^^disabling this since we'll be providing our own blurry motion :)
float baseShift = fract(getFromHzNumber * shiftAmount);
// We'll offset uv.x by baseShift, and round-off to screen coordinates to avoid seam artifacts
float px = 1.0 / iResolution.x;
uv.x = mod(uv.x + baseShift + px*0.1, 1.0) - px*0.1;
// Sample texture with no mip (textureLod)
vec4 c = textureLod(iChannel0, uv, 0.0);
return c.rgb;
}
//-------------------------------------------------------------------------------------------------
// CRT Rolling Scan Simulation With Phosphor Fade + Brightness Redistributor Algorithm
//
// New variable 'per-pixel MPRT' algorithm that mimics CRT phosphor decay too.
// - We emit as many photons as possible as early as possible, and if we can't emit it all (e.g. RGB 255)
// then we continue emitting in the next refresh cycle until we've hit our target (gamma-compensated).
// - This is a clever trick to keep CRT simulation brighter but still benefit motion clarity of most colors.
// Besides, real CRT tubes behave roughly similar too! (overexcited phosphor take longer to decay)
// - This also concurrently produces a phosphor-fade style behavior.
// - Win-win!
//
// Parameters:
// - c2: total brightness * framesPerHz per channel.
// - crtRasterPos: normalized raster position [0..1] representing current scan line
// - phaseOffset: fractional start of the brightness interval [0..1] (0.0 at top, 1.0 at bottom).
// - framesPerHz: Number of frames per Hz. (Does not have to be integer divisible!)
//
vec3 getPixelFromSimulatedCRT(vec2 uv, float crtRasterPos, float crtHzCounter, float framesPerHz)
{
// Get pixels from three consecutive refresh cycles
vec3 pixelPrev2 = srgb2linear(getPixelFromOrigFrame(uv, crtHzCounter - 2.0, crtHzCounter));
vec3 pixelPrev1 = srgb2linear(getPixelFromOrigFrame(uv, crtHzCounter - 1.0, crtHzCounter));
vec3 pixelCurr = srgb2linear(getPixelFromOrigFrame(uv, crtHzCounter, crtHzCounter));
vec3 result = vec3(0.0);
// Compute "photon budgets" for all three cycles
float brightnessScale = framesPerHz * GAIN_VS_BLUR;
vec3 colorPrev2 = pixelPrev2 * brightnessScale;
vec3 colorPrev1 = pixelPrev1 * brightnessScale;
vec3 colorCurr = pixelCurr * brightnessScale;
#if SCAN_DIRECTION == 1
float tubePos = (1.0 - uv.y); // Top to bottom
#elif SCAN_DIRECTION == 2
float tubePos = uv.y; // Bottom to top
#elif SCAN_DIRECTION == 3
float tubePos = uv.x; // Left to right
#elif SCAN_DIRECTION == 4
float tubePos = (1.0 - uv.x); // Right to left
#endif
// Process each color channel independently
for (int ch = 0; ch < 3; ch++)
{
// Get brightness lengths for all three cycles
float Lprev2 = colorPrev2[ch];
float Lprev1 = colorPrev1[ch];
float Lcurr = colorCurr[ch];
if (Lprev2 <= 0.0 && Lprev1 <= 0.0 && Lcurr <= 0.0) {
result[ch] = 0.0;
continue;
}
// TODO: Optimize to use only 2 frames.
// Unfortunately I need all 3 right now because if I only do 2,
// I get artifacts at either top OR bottom edge (can't eliminate both)
// What I may do is use a phase offset (e.g. input framebuffer chain
// rotates forward in middle of emulated CRT Hz), as a workaround, and
// see if that solves the problem and reduces the queue to 2.
// (Will attempt later)
// Convert normalized values to frame space
float tubeFrame = tubePos * framesPerHz;
float fStart = crtRasterPos * framesPerHz;
float fEnd = fStart + 1.0;
// Define intervals for all three trailing refresh cycles
float startPrev2 = tubeFrame - framesPerHz;
float endPrev2 = startPrev2 + Lprev2;
float startPrev1 = tubeFrame;
float endPrev1 = startPrev1 + Lprev1;
float startCurr = tubeFrame + framesPerHz; // Fix seam for top edge
float endCurr = startCurr + Lcurr;
// Calculate overlaps for all three cycles
#define INTERVAL_OVERLAP(Astart, Aend, Bstart, Bend) max(0.0, min(Aend, Bend) - max(Astart, Bstart))
float overlapPrev2 = INTERVAL_OVERLAP(startPrev2, endPrev2, fStart, fEnd);
float overlapPrev1 = INTERVAL_OVERLAP(startPrev1, endPrev1, fStart, fEnd);
float overlapCurr = INTERVAL_OVERLAP(startCurr, endCurr, fStart, fEnd);
// Sum all overlaps for final brightness
float tempvar = overlapPrev2 + overlapPrev1 + overlapCurr;
if(ch == 0) result.x = tempvar;
if(ch == 1) result.y = tempvar;
if(ch == 2) result.z = tempvar;
}
return linear2srgb(result);
}
void main()
{
// disable the effect if we don't have any subframes
FragColor = texture(Source, vTexCoord);
if(params.TotalSubFrames < 2) return;
else{
// uv: Normalized coordinates ranging from (0,0) at the bottom-left to (1,1) at the top-right.
vec2 uv = vTexCoord.xy;
vec4 c = vec4(0.0, 0.0, 0.0, 1.0);
//-------------------------------------------------------------------------------------------------
// CRT beam calculations
// Frame counter, which may be compensated by slo-mo modes (FPS_DIVISOR). Does not need to be integer divisible.
float effectiveFrame = floor(float(iFrame) * FPS_DIVISOR);
// Normalized raster position [0..1] representing current position of simulated CRT electron beam
float crtRasterPos = mod(effectiveFrame, EFFECTIVE_FRAMES_PER_HZ) / EFFECTIVE_FRAMES_PER_HZ + POS_MOD;
// CRT refresh cycle counter
float crtHzCounter = floor(effectiveFrame / EFFECTIVE_FRAMES_PER_HZ);
#if SPLITSCREEN == 1
//-------------------------------------------------------------------------------------------------
// Splitscreen processing
// crtTube: Boolean indicating whether the current pixel is within the CRT-BFI region.
// When splitscreen is off, apply CRT-BFI to entire screen
bool crtArea = !((uv.x > SPLITSCREEN_X) && (uv.y > SPLITSCREEN_Y));
// Calculate border regions (in pixels)
float borderXpx = abs(fragCoord.x - SPLITSCREEN_X * iResolution.x);
float borderYpx = abs(fragCoord.y - SPLITSCREEN_Y * iResolution.y);
// Border only exists in the non-BFI region (x > SPLITSCREEN_X || y > SPLITSCREEN_Y)
bool inBorderX = borderXpx < float(SPLITSCREEN_BORDER_PX) && uv.y > SPLITSCREEN_Y;
bool inBorderY = borderYpx < float(SPLITSCREEN_BORDER_PX) && uv.x > SPLITSCREEN_X;
bool inBorder = (SPLITSCREEN == 1) && (inBorderX || inBorderY);
// We #ifdef the if statement away for shader efficiency (though this specific one didn't affect performance)
if (crtArea) {
#endif
//-----------------------------------------------------------------------------------------
// Get CRT simulated version of pixel
fragColor.rgb = getPixelFromSimulatedCRT(uv, crtRasterPos, crtHzCounter, EFFECTIVE_FRAMES_PER_HZ);
#if SPLITSCREEN == 1
}
else if (!inBorder) {
fragColor.rgb = getPixelFromOrigFrame(uv, crtHzCounter, crtHzCounter);
#if SPLITSCREEN_MATCH_BRIGHTNESS == 1
// Brightness compensation for unprocessed pixels through similar gamma-curve (match gamma of simulated CRT)
fragColor.rgb = srgb2linear(fragColor.rgb) * GAIN_VS_BLUR;
fragColor.rgb = clampPixel(linear2srgb(fragColor.rgb));
#endif
}
#endif
}
}
//-------------------------------------------------------------------------------------------------
// Credits Reminder:
// Please credit BLUR BUSTERS & TIMOTHY LOTTES if this algorithm is used in your project/product.
// Hundreds of hours of research was done on related work that led to this algorithm.
//-------------------------------------------------------------------------------------------------