slang-shaders/misc/nes-color-decoder.slang

269 lines
6.7 KiB
Plaintext

#version 450
/*
NES NTSC Color Decoder shader
Ported from Bisqwit's C++ NES Palette Generator
https://forums.nesdev.com/viewtopic.php?p=85060#p85060
Hue Preserve Clip functions ported from Drag's Palette Generator
http://drag.wootest.net/misc/palgen.html
Use with Nestopia or FCEUmm libretro cores with the palette set to 'raw'.
*/
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
vec4 OutputSize;
vec4 OriginalSize;
vec4 SourceSize;
uint FrameCount;
} global;
layout(push_constant) uniform Push
{
float nes_saturation;
float nes_hue;
float nes_contrast;
float nes_brightness;
float nes_gamma;
float nes_sony_matrix;
float nes_clip_method;
} params;
#pragma parameter nes_saturation "Saturation" 1.0 0.0 5.0 0.05
#pragma parameter nes_hue "Hue" 0.0 -360.0 360.0 1.0
#pragma parameter nes_contrast "Contrast" 1.0 0.0 2.0 0.05
#pragma parameter nes_brightness "Brightness" 1.0 0.0 2.0 0.05
#pragma parameter nes_gamma "Gamma" 1.8 1.0 2.5 0.05
#pragma parameter nes_sony_matrix "Sony CXA2025AS US colors" 0.0 0.0 1.0 1.0
#pragma parameter nes_clip_method "Palette clipping method" 0.0 0.0 2.0 1.0
#define saturation params.nes_saturation
#define hue params.nes_hue
#define contrast params.nes_contrast
#define brightness params.nes_brightness
#define gamma params.nes_gamma
#define nes_sony_matrix params.nes_sony_matrix
#define nes_clip_method params.nes_clip_method
//comment the define out to use the "common" conversion matrix instead of the FCC sanctioned one
#define USE_FCC_MATRIX
bool wave (int p, int color)
{
return ((color + p + 8) % 12 < 6);
}
float gammafix (float f)
{
return f < 0.0 ? 0.0 : pow(f, 2.2 / gamma);
}
vec3 huePreserveClipDarken(float r, float g, float b)
{
float ratio = 1.0;
if ((r > 1.0) || (g > 1.0) || (b > 1.0))
{
float max = r;
if (g > max)
max = g;
if (b > max)
max = b;
ratio = 1.0 / max;
}
r *= ratio;
g *= ratio;
b *= ratio;
r = clamp(r, 0.0, 1.0);
g = clamp(g, 0.0, 1.0);
b = clamp(b, 0.0, 1.0);
return vec3(r, g, b);
}
vec3 huePreserveClipDesaturate(float r, float g, float b)
{
float l = (.299 * r) + (0.587 * g) + (0.114 * b);
bool ovr = false;
float ratio = 1.0;
if ((r > 1.0) || (g > 1.0) || (b > 1.0))
{
ovr = true;
float max = r;
if (g > max) max = g;
if (b > max) max = b;
ratio = 1.0 / max;
}
if (ovr)
{
r -= 1.0;
g -= 1.0;
b -= 1.0;
r *= ratio;
g *= ratio;
b *= ratio;
r += 1.0;
g += 1.0;
b += 1.0;
}
r = clamp(r, 0.0, 1.0);
g = clamp(g, 0.0, 1.0);
b = clamp(b, 0.0, 1.0);
return vec3(r, g, b);
}
vec3 MakeRGBColor(int emphasis, int level, int color)
{
float y = 0.0;
float i = 0.0;
float q = 0.0;
float r = 0.0;
float g = 0.0;
float b = 0.0;
float yiq2rgb[6];
// Color 0xE and 0xF are black
level = (color < 14) ? level : 1;
// Voltage levels, relative to synch voltage
float black = 0.518;
float white = 1.962;
float attenuation = 0.746;
const float levels[8] = float[] ( 0.350 , 0.518, 0.962, 1.550,
1.094, 1.506, 1.962, 1.962);
float low = levels[level + 4 * int(color == 0)];
float high = levels[level + 4 * int(color < 13)];
// Calculate the luma and chroma by emulating the relevant circuits:
for(int p = 0; p < 12; p++) // 12 clock cycles per pixel.
{
// NES NTSC modulator (square wave between two voltage levels):
float spot = wave(p, color) ? high : low;
// De-emphasis bits attenuate a part of the signal:
if ((bool(emphasis & 1) && wave(p, 12)) ||
(bool(emphasis & 2) && wave(p, 4)) ||
(bool(emphasis & 4) && wave(p, 8)))
{
spot *= attenuation;
}
// Normalize:
float v = (spot - black) / (white - black);
// Ideal TV NTSC demodulator:
// Apply contrast/brightness
v = (v - 0.5) * contrast + 0.5;
v *= (brightness / 12.0);
float hue_tweak = hue * 12.0 / 360.0;
y += v;
i += v * cos((3.141592653 / 6.0) * (p + hue_tweak) );
q += v * sin((3.141592653 / 6.0) * (p + hue_tweak) );
}
i *= saturation;
q *= saturation;
if (nes_sony_matrix > 0.5)
{
// Sony CXA2025AS US conversion matrix
yiq2rgb[0] = 1.630;
yiq2rgb[1] = 0.317;
yiq2rgb[2] = -0.378;
yiq2rgb[3] = -0.466;
yiq2rgb[4] = -1.089;
yiq2rgb[5] = 1.677;
}
else
{
#ifdef USE_FCC_MATRIX
// FCC sanctioned conversion matrix
yiq2rgb[0] = 0.946882;
yiq2rgb[1] = 0.623557;
yiq2rgb[2] = -0.274788;
yiq2rgb[3] = -0.635691;
yiq2rgb[4] = -1.108545;
yiq2rgb[5] = 1.709007;
#else
// commonly used conversion matrix
yiq2rgb[0] = 0.956;
yiq2rgb[1] = 0.621;
yiq2rgb[2] = -0.272;
yiq2rgb[3] = -0.647;
yiq2rgb[4] = -1.105;
yiq2rgb[5] = 1.702;
#endif
}
// Convert YIQ into RGB according to selected conversion matrix
r = gammafix(y + yiq2rgb[0] * i + yiq2rgb[1] * q);
g = gammafix(y + yiq2rgb[2] * i + yiq2rgb[3] * q);
b = gammafix(y + yiq2rgb[4] * i + yiq2rgb[5] * q);
vec3 corrected_rgb;
// Apply desired clipping method to out-of-gamut colors.
if (nes_clip_method < 0.5)
{
//If a channel is out of range (> 1.0), it's simply clamped to 1.0. This may change hue, saturation, and/or lightness.
r = clamp(r, 0.0, 1.0);
g = clamp(g, 0.0, 1.0);
b = clamp(b, 0.0, 1.0);
corrected_rgb = vec3(r, g, b);
}
else if (nes_clip_method == 1.0)
{
//If any channels are out of range, the color is darkened until it is completely in range.
corrected_rgb = huePreserveClipDarken(r, g, b);
}
else if (nes_clip_method == 2.0)
{
//If any channels are out of range, the color is desaturated towards the luminance it would've had.
corrected_rgb = huePreserveClipDesaturate(r, g, b);
}
return corrected_rgb;
}
#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;
void main()
{
vec4 c = texture(Source, vTexCoord.xy);
// Extract the chroma, level, and emphasis from the normalized RGB triplet
int color = int(floor((c.r * 15.0) + 0.5));
int level = int(floor((c.g * 3.0) + 0.5));
int emphasis = int(floor((c.b * 7.0) + 0.5));
vec3 out_color = MakeRGBColor(emphasis, level, color);
FragColor = vec4(out_color, 1.0);
}