404 lines
21 KiB
GLSL
404 lines
21 KiB
GLSL
// From Shadertoy https://www.shadertoy.com/view/XfKfWd
|
|
#ifdef NOT_SHADERTOY
|
|
// We include these definitions to assist other environments (untested)
|
|
uniform vec3 iResolution; // viewport resolution (in pixels)
|
|
uniform float iTime; // shader playback time (in seconds)
|
|
uniform float iTimeDelta; // render time (in seconds)
|
|
uniform float iFrameRate; // shader frame rate
|
|
uniform int iFrame; // 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
|
|
uniform sampler2D iChannel0; // 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)
|
|
#endif
|
|
|
|
/*********************************************************************************************************************/
|
|
//
|
|
// 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.
|
|
//
|
|
/*********************************************************************************************************************/
|
|
|
|
//------------------------------------------------------------------------------------------------
|
|
// 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 butter.
|
|
// - 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 4.0
|
|
|
|
// Your display's gamma value. Necessary to prevent horizontal-bands artifacts.
|
|
#define GAMMA 2.4
|
|
|
|
// 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 piels).
|
|
// - 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.
|
|
#define GAIN_VS_BLUR 0.7
|
|
|
|
// Splitscreen versus mode for comparing to non-CRT-simulated
|
|
#define SPLITSCREEN 1 // 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
|
|
|
|
// 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.
|
|
#define FPS_DIVISOR 1.0 // Slow down or speed up the simulation
|
|
|
|
// 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
|
|
#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 1
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// 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
|
|
const 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));
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------
|
|
// 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 = MOTION_SPEED / 1000.0;
|
|
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
|
|
result[ch] = overlapPrev2 + overlapPrev1 + overlapCurr;
|
|
}
|
|
|
|
return linear2srgb(result);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Main Image Function
|
|
//
|
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
|
|
// uv: Normalized coordinates ranging from (0,0) at the bottom-left to (1,1) at the top-right.
|
|
vec2 uv = fragCoord / iResolution.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;
|
|
|
|
// 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 LOTTE if this algorithm is used in your project/product.
|
|
// Hundreds of hours of research was done on related work that led to this algorithm.
|
|
//-------------------------------------------------------------------------------------------------
|