Patchy NTSC - Bunch of last minute improvements (#629)

* Patchy NTSC - Fix hue offset on 2C02E

* Patchy NTSC - Fully update patchy-color.slang

Updated Patchy Color to have all of Patchy NTSC's features.

* Add slangp file for Patchy Color

Now, Patchy Color is officially usable, instead of being hidden and unfinished.

* Patchy NTSC - Add NES real hardware video capture colors

* Patchy NTSC - Fix bizzare incorrect smearing levels

Seriously, why did I leave it like that?

* Patchy NTSC - Don't apply Gen/MD palette on test patterns

* Patchy NTSC - Add chromatic adaptation for some D93 LUTs

* blah

blah

* Patchy NTSC - Set default LUT to Trinitron

* Patchy NTSC - Update presets based on fixed smearing behavior

Returned the hue rotations and saturations to defaults, and increased the threshold for smearing. The defaults don't show the smear out-of-the-box anymore; you will only see it if you turn up your contrast and/or saturation.

* Patchy NTSC - Use video-captured NES color by default
This commit is contained in:
PlainOldPants 2024-08-29 14:44:23 +00:00 committed by GitHub
parent 4cf85d3147
commit b9e012f25c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 635 additions and 100 deletions

68
misc/patchy-color.slangp Normal file
View File

@ -0,0 +1,68 @@
shaders = "3"
feedback_pass = "0"
shader0 = "../ntsc/shaders/patchy-ntsc/patchy-color.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "1.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "../ntsc/shaders/patchy-ntsc/trilinearLUT-switchable.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "../ntsc/shaders/patchy-ntsc/linear-to-srgb.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "true"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5;PhosphorSamplerLUT6"
PhosphorSamplerLUT1 = "../ntsc/shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "../ntsc/shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "../ntsc/shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../ntsc/shaders/patchy-ntsc/P22_J_D93.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"
PhosphorSamplerLUT6 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D93.png"
PhosphorSamplerLUT6_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT6_mipmap = "false"
PhosphorSamplerLUT6_linear = "false"

View File

@ -85,14 +85,14 @@ scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_contrast = "0.725000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_width_uncropped = "256.000000"
pn_height_uncropped = "240.000000"
pn_nes_enable = "1.000000"
pn_nes_real_capture = "1.0"
pn_rgb_smear_rate = "1.15"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5;PhosphorSamplerLUT6"
PhosphorSamplerLUT1 = "../ntsc/shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
@ -114,3 +114,13 @@ PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../ntsc/shaders/patchy-ntsc/P22_J_D93.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"
PhosphorSamplerLUT6 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D93.png"
PhosphorSamplerLUT6_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT6_mipmap = "false"
PhosphorSamplerLUT6_linear = "false"

View File

@ -84,8 +84,6 @@ scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_genesis_jailbar_enable = "1.000000"
pn_genesis_jailbar_offset = "0.312500"
@ -93,25 +91,37 @@ pn_scanline_dur = "47.699997"
pn_color_init_offset = "0.700000"
pn_modulator_luma_filter_type = "2.000000"
pn_modulator_luma_res = "220.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
pn_rgb_smear_rate = "1.15"
PhosphorSamplerLUT1 = "shaders/patchy-ntsc/P22_80s_D65.png"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5;PhosphorSamplerLUT6"
PhosphorSamplerLUT1 = "../ntsc/shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2 = "../ntsc/shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3 = "../ntsc/shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../ntsc/shaders/patchy-ntsc/P22_J_D93.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"
PhosphorSamplerLUT6 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D93.png"
PhosphorSamplerLUT6_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT6_mipmap = "false"
PhosphorSamplerLUT6_linear = "false"

View File

@ -84,8 +84,6 @@ scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_genesis_palette = "1.0"
pn_genesis_jailbar_enable = "1.000000"
@ -94,25 +92,37 @@ pn_scanline_dur = "47.699997"
pn_color_init_offset = "0.700000"
pn_modulator_luma_filter_type = "2.000000"
pn_modulator_luma_res = "220.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
pn_rgb_smear_rate = "1.15"
PhosphorSamplerLUT1 = "shaders/patchy-ntsc/P22_80s_D65.png"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5;PhosphorSamplerLUT6"
PhosphorSamplerLUT1 = "../ntsc/shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2 = "../ntsc/shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3 = "../ntsc/shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../ntsc/shaders/patchy-ntsc/P22_J_D93.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"
PhosphorSamplerLUT6 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D93.png"
PhosphorSamplerLUT6_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT6_mipmap = "false"
PhosphorSamplerLUT6_linear = "false"

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -53,7 +53,9 @@ layout(std140, set = 0, binding = 0) uniform UBO
float pc_color;
float pc_tint;
float pc_unused6;
float pc_gamma;
float pc_gamma_type;
float pc_g_CRT_l;
float pc_power_gamma;
} global;
#pragma parameter pc_unused1 "===== Patchy Color Settings =====" 0.0 0.0 0 1
@ -65,10 +67,10 @@ layout(std140, set = 0, binding = 0) uniform UBO
#pragma parameter pc_unused3 "== Console settings (Does work with test patterns) ==" 0 0 0 1
#pragma parameter pc_console "Console: Other | Gen/MD | NES/FC (raw palette)" 0 0 2 1
#pragma parameter pc_nes_lut "NES: Use real capture (2C02G) | Use formulas" 0 0 1 1
#pragma parameter pc_nes_lut "NES: Use real capture (probably 2C02G) | Use formulas" 0 0 1 1
#pragma parameter pc_nes_version "NES PPU formula: 2C02G (NTSC) | 2C02E (NTSC) | PAL" 0 0 2 1
//#pragma parameter pc_nes_alt_voltages "(Reccomend 0 (off)) NES: Use outdated voltages" 0 0 1 1
#pragma parameter pc_genesis_needfix "MD emu: BlastEm (accurate) | GenPlusGX (Needs correcting)" 0 0 1 1
#pragma parameter pc_genesis_needfix "MD emu: BlastEm (Leave as-is) | GenPlusGX (Apply correction)" 0 0 1 1
#pragma parameter pc_unused4 "=== Composite video demodulation controls ===" 0 0 0 1
#pragma parameter pc_enableComposite "Enable composite demodulator simulation" 1.0 0.0 1.0 1.0
@ -85,7 +87,9 @@ layout(std140, set = 0, binding = 0) uniform UBO
#pragma parameter pc_tint "Tint (Hue Rotation) (Rec. exactly 0.0)" 0 -45 45 0.5
#pragma parameter pc_unused6 "=== Gamma and phosphors ===" 0 0 0 1
#pragma parameter pc_gamma "Gamma" 2.5 2.3 2.6 0.01
#pragma parameter pc_gamma_type "Gamma: BT.1886 + Grade black lift fix | BT.1886 | Power | sRGB" 1 0 3 1
#pragma parameter pc_g_CRT_l "Black lift fix approximate gamma" 2.50 2.30 2.60 0.01
#pragma parameter pc_power_gamma "Power gamma" 2.4 2.2 3.0 0.01
#define pi 3.14159265358
@ -233,22 +237,40 @@ vec3 createNesColorYuv(vec3 c) {
float skew;
if(global.pc_nes_version < 0.5) {
// 2C02G
skew = level * -5.0 / 180.0 * pi; // -5 degrees
skew = (3 - level) * -5.0 / 180.0 * pi; // -5 degrees
} else if(global.pc_nes_version < 1.5) {
// 2C02E
skew = level * -2.5 / 180.0 * pi; // -2.5 degrees
skew = (3 - level) * -2.5 / 180.0 * pi; // -2.5 degrees
} else {
// In PAL, the alternating phase causes this color error to be cancelled out.
skew = 0.0;
}
// Also from lidnariq's post:
float burstLow = (0.148 - black) / (white - black);
float burstHigh = (0.524 - black) / (white - black);
float burstMid = (burstLow + burstHigh) / 2;
vec2 burstUV = vec2(0);
for(int i = 0; i < 12; i++) {
resultYiq.y += (signal[i] - resultYiq.x) * sin((i + 3) * pi / 6 - skew);
resultYiq.z += (signal[i] - resultYiq.x) * cos((i + 3) * pi / 6 - skew);
resultYiq.y += (signal[i] - resultYiq.x) * sin((i + 3) * pi / 6 + skew);
resultYiq.z += (signal[i] - resultYiq.x) * cos((i + 3) * pi / 6 + skew);
if(i >= 5 && i <= 10) {
burstUV.x += (burstHigh - burstMid) * sin(i * pi / 6);
burstUV.y += (burstHigh - burstMid) * cos(i * pi / 6);
} else {
burstUV.x += (burstLow - burstMid) * sin(i * pi / 6);
burstUV.y += (burstLow - burstMid) * cos(i * pi / 6);
}
}
resultYiq.yz *= 1.0 / 12.0;
burstUV *= 1.0 / 12.0;
// 1.1 comes from the NES's white (color 0x20 or 0x30) being at roughly 110 IRE.
// 0.2 comes from the standard NTSC colorburst amplitude.
// The result is still quite different from my real hardware capture.
resultYiq.yz *= (0.2 / 1.1) / sqrt(pow(burstUV.x, 2) + pow(burstUV.y, 2));
return resultYiq;
}
@ -573,31 +595,36 @@ vec3 lookupNesColor(vec3 nesFormat) {
}
vec3 retreiveNesColorYuv(vec3 nesFormat) {
mat3 yuvMat;
//if(global.pc_nes_lut_newyuv < 0.5) {
// // https://en.wikipedia.org/wiki/Y%E2%80%B2UV as of 27 Jul 2024
// // Standard matrix for BT.470
// yuvMat = mat3x3(
// 0.299, -0.14713, 0.615,
// 0.587, -0.28886, -0.51499,
// 0.114, 0.436, -0.10001
// );
//} else {
// https://en.wikipedia.org/wiki/Y%E2%80%B2UV as of 27 Jul 2024
// Standard matrix for BT.709
yuvMat = mat3(0.2126, -0.09991, 0.615,
0.7152, -0.33609, -0.55861,
0.0722, 0.436, -0.05639);
//}
vec3 colYuv;
if(global.pc_nes_lut < 0.5) {
// Lookup table (a.k.a. color palette) is enabled.
mat3 yuvMat;
//if(global.pc_nes_lut_newyuv < 0.5) {
// // https://en.wikipedia.org/wiki/Y%E2%80%B2UV as of 27 Jul 2024
// // Standard matrix for BT.470
// yuvMat = mat3x3(
// 0.299, -0.14713, 0.615,
// 0.587, -0.28886, -0.51499,
// 0.114, 0.436, -0.10001
// );
//} else {
// https://en.wikipedia.org/wiki/Y%E2%80%B2UV as of 27 Jul 2024
// Standard matrix for BT.709
yuvMat = mat3(0.2126, -0.09991, 0.615,
0.7152, -0.33609, -0.55861,
0.0722, 0.436, -0.05639);
//}
return yuvMat * lookupNesColor(nesFormat);
colYuv = yuvMat * lookupNesColor(nesFormat);
} else {
return mat3x3(
1, 0, 0, // Compared to my real capture, the computed colors are off by half an NES PPU cycle.
0, cos(-15.0 * pi / 180.0), -sin(-15.0 * pi / 180.0),
0, sin(-15.0 * pi / 180.0), cos(-15.0 * pi / 180.0)
) * createNesColorYuv(nesFormat);
colYuv = createNesColorYuv(nesFormat);
}
if(global.pc_enableComposite < 0.5) {
return mat3( // BT.470 / Rec. 601 YUV to RGB matrix
1.0000000, 1.0, 1.0,
-0.000000029378826483, -0.396552562713623050, 2.031872510910034000,
1.1383928060531616, -0.5800843834877014, 0.0000000000000000) * colYuv;
} else {
return colYuv;
}
}
@ -866,11 +893,36 @@ float nesMaxRgb() {
return max(maxRgb, 1.0); // 1.0 is the maximum pure white on the NES.
}
float EOTF_1886a_default(float color, float brightness, float contrast) {
// From Rec. ITU-R BT.1886
float Vc = 0.35, // Vc, a1, and a2 are directly from the paper
a1 = 2.6,
a2 = 3.0,
Lw = contrast;
float V = color,
b = brightness, // The paper recommends about either 0 or 0.1.
k = Lw / pow(1.0 + b, a1);
if(V < Vc) {
return k * pow(Vc + b, a1 - a2) * pow(V + b, a2);
} else {
return k * pow(V + b, a1);
}
}
vec3 EOTF_1886a_default_f3(vec3 color, float brightness, float contrast) {
color.r = EOTF_1886a_default(color.r, brightness, contrast);
color.g = EOTF_1886a_default(color.g, brightness, contrast);
color.b = EOTF_1886a_default(color.b, brightness, contrast);
return color;
}
// CRT EOTF Function
// Taken from Grade
//----------------------------------------------------------------------
#define CRT_l -(100000.*log((72981.-500000./(3.*max(2.3,global.pc_gamma)))/9058.))/945461.
#define CRT_l -(100000.*log((72981.-500000./(3.*max(2.3,global.pc_g_CRT_l)))/9058.))/945461.
float EOTF_1886a(float color, float bl, float brightness, float contrast) {
@ -1020,6 +1072,9 @@ vec3 testPattern(vec2 pos) {
}
}
// For sRGB_to_linear
#include "../../../include/colorspace-tools.h"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
@ -1048,7 +1103,7 @@ void main()
if(global.pc_console > 0.5) {
if(global.pc_console < 1.5) { // Sega Genesis / MegaDrive
if(global.pc_genesis_needfix > 0.5) // Genesis gamma correction--BlastEm already performs this fix for you
if(global.pc_genesis_needfix > 0.5 && global.pc_testpattern < 0.5) // Genesis gamma correction--BlastEm already performs this fix for you
changedColor = fixGenColor(changedColor);
}
else if(global.pc_console < 2.5) { // NES PPU
@ -1066,7 +1121,7 @@ void main()
if(global.pc_console < 1.5)
changedColor = toYuv * changedColor;
float tint = global.pc_tint * pi / 180.0;
float tint = -global.pc_tint * pi / 180.0;
changedColor.gb *= mat2(
cos(tint), -sin(tint),
sin(tint), cos(tint)
@ -1074,18 +1129,20 @@ void main()
changedColor.gb *= global.pc_color;
changedColor = toRgb * changedColor;
// changedColor *= global.pc_contrast;
// changedColor.r += global.pc_brightness / (1 + global.pc_brightness) * global.pc_contrast;
} else {
// When not simulating Composite video, simulate RGB instead.
// Only the Contrast and Brightness knobs work in RGB.
// changedColor = (changedColor + global.pc_brightness) / (1 + global.pc_brightness) * global.pc_contrast;
// changedColor *= global.pc_contrast;
// The setting for 1.0 contrast's behavior doesn't matter in RGB because the white ramp and RGB space are of equal size.
}
changedColor = EOTF_1886a_f3(changedColor, CRT_l, global.pc_brightness, global.pc_contrast);
if(global.pc_gamma_type < 0.5) {
changedColor = EOTF_1886a_f3(changedColor, CRT_l, global.pc_brightness, global.pc_contrast);
} else if(global.pc_gamma_type < 1.5) {
changedColor = EOTF_1886a_default_f3(changedColor, global.pc_brightness, global.pc_contrast);
} else if(global.pc_gamma_type < 2.5) {
changedColor = pow((changedColor + global.pc_brightness) / (1 + global.pc_brightness), vec3(global.pc_power_gamma)) * global.pc_contrast;
} else {
float contr = global.pc_contrast <= 1.001 ? linear_to_sRGB(vec3(global.pc_contrast), 2.4).r : 1.055 * pow(global.pc_contrast, 1.0 / 2.4) - 0.055; // 2.4 instead of 2.2 is not a mistake.
changedColor = (changedColor + global.pc_brightness) / (1 + global.pc_brightness) * contr;
changedColor = sRGB_to_linear(changedColor, 2.4); // Beware that this function clamps between 0 and 1.
}
FragColor = vec4(changedColor, originalColor.a);
FragColor = vec4(clamp(changedColor, 0, 1), originalColor.a);
}

View File

@ -681,6 +681,370 @@ vec2 uvDemodPickable(float type, float chroma, float phase, float tint) {
}
}
//////////////////////////////////////////////////
// NES real hardware video capture lookup table //
//////////////////////////////////////////////////
// The current video capture is of low quality. I am going to redo it soon.
// The problem is that it is captured at only 25% saturation (and 25% contrast), which is extremely low.
// The video capture can be done with a much higher saturation. The goal is to undersaturate to avoid clamping over/under 0.0 or 1.0.
vec3 nesRealCaptureLookup(vec3 yBmyRmy, float saturation) {
mat3x2 yuvAxisPts = yuvAxisPointsSynced();
float xr, yr, xg, yg, xb, yb;
xr = yuvAxisPts[0][1] * cos(yuvAxisPts[0][0]);
yr = yuvAxisPts[0][1] * sin(yuvAxisPts[0][0]);
xg = yuvAxisPts[1][1] * cos(yuvAxisPts[1][0]);
yg = yuvAxisPts[1][1] * sin(yuvAxisPts[1][0]);
xb = yuvAxisPts[2][1] * cos(yuvAxisPts[2][0]);
yb = yuvAxisPts[2][1] * sin(yuvAxisPts[2][0]);
// YUV to Y B-Y R-Y matrix
mat3 toYBmyRmy = mat3(1, 0, 0,
0, xb, xr,
0, yb, yr);
// YUV to RGB matrix
mat3 toRgb = mat3(1, 1, 1,
xr, xg, xb,
yr, yg, yb);
vec3 YIQ = mat3(1, 0, 0, // 30 instead of 33 is not a mistake. The hue offsets below were based on GTU-famicom's output, which has its YIQ off by exactly 3 degrees.
0, -sin(30.0 * pi / 180.0), cos(30.0 * pi / 180.0),
0, cos(30.0 * pi / 180.0), sin(30.0 * pi / 180.0)) *
inverse(toYBmyRmy) * yBmyRmy;
// Grays. I only approximated the three grays in the 00-column and put pure black for 1d. I'll probably get the remaining ones from the 0d column sometime later.
float unsaturatedLevels[4] = {0.0, (0.616 - 0.312) / (1.100 - 0.312), (0.840 - 0.312) / (1.100 - 0.312), 1.0}; // Calculated from DAC.slang
// The two equivalent whites 20 and 30 are only one white in this.
float whiteRed[] = {0, 0.26171875, 0.41796875, 0.62109375, };
float whiteGreen[] = {0, 0.19921875, 0.3984375, 0.5, };
float whiteBlue[] = {0, 0.3203125, 0.5390625, 0.7421875, };
// Colors that aren't gray, i.e. x1 thru xC for x = 0, 1, 2, 3
float levelSats[4] = {sqrt((16 + 9) / 255.0), sqrt((3 * 16) / 255.0), sqrt((3 * 16 + 2) / 255.0), sqrt(9.0 / 255.0)}; // Distance of cartesian point (I, Q) from origin (0, 0). Screenshotted from shader test output.
float saturatedLevels[4] = {((0.228 + 0.616) / 2. - 0.312) / (1.100 - 0.312), ((0.312 + 0.840) / 2. - 0.312) / (1.100 - 0.312), ((0.552 + 1.100) / 2. - 0.312) / (1.100 - 0.312), ((0.880 + 1.100) / 2. - 0.312) / (1.100 - 0.312)}; // Calculated from DAC.slang
float levelHues[12] = {0.2890625, 0.3672419921875, 0.4453125, 0.53125, 0.609375, 0.69140625, 0.76953125, 0.890625, 0.96875, 0.0546875, 0.1328125, 0.20703125}; // Screenshotted from shader test output
float level0Red[] = {0.06640625, 0.11328125, 0.16015625, 0.30078125, 0.3203125, 0.33203125, 0.26171875, 0.12890625, 0.0, 0.0, 0.0, 0.05078125, };
float level0Green[] = {0.0546875, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.046875, 0.08203125, 0.109375, 0.08984375, 0.04296875, };
float level0Blue[] = {0.45703125, 0.5, 0.44140625, 0.328125, 0.0703125, 0.0, 0.0, 0.0, 0.0, 0.0, 0.12109375, 0.328125, };
float level1Red[] = {0.1640625, 0.16796875, 0.3359375, 0.4453125, 0.5078125, 0.5, 0.4609375, 0.37109375, 0.125, 0.0, 0.0, 0.08203125, };
float level1Green[] = {0.09375, 0.08203125, 0.0, 0.0, 0.08203125, 0.08984375, 0.14453125, 0.2265625, 0.265625, 0.2890625, 0.2734375, 0.27734375, };
float level1Blue[] = {0.69921875, 0.76953125, 0.7265625, 0.5859375, 0.328125, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22265625, 0.51171875, };
float level2Red[] = {0.1328125, 0.265625, 0.3984375, 0.5234375, 0.56640625, 0.625, 0.55859375, 0.52734375, 0.359375, 0.21875, 0.0, 0.125, };
float level2Green[] = {0.41015625, 0.26953125, 0.1953125, 0.203125, 0.19921875, 0.28125, 0.328125, 0.3828125, 0.37890625, 0.44140625, 0.4296875, 0.47265625, };
float level2Blue[] = {0.80859375, 0.73046875, 0.6953125, 0.70703125, 0.58203125, 0.3359375, 0.07421875, 0.0, 0.0, 0.1484375, 0.3984375, 0.63671875, };
float level3Red[] = {0.46875, 0.50390625, 0.5390625, 0.59765625, 0.6328125, 0.57421875, 0.609375, 0.58203125, 0.51953125, 0.4375, 0.41796875, 0.390625, };
float level3Green[] = {0.51953125, 0.48046875, 0.48046875, 0.4765625, 0.4765625, 0.4375, 0.49609375, 0.48828125, 0.48828125, 0.4765625, 0.4765625, 0.4765625, };
float level3Blue[] = {0.796875, 0.78125, 0.76953125, 0.8046875, 0.7265625, 0.56640625, 0.5390625, 0.4765625, 0.48046875, 0.52734375, 0.56640625, 0.6015625, };
// The saturation amounts were screenshotted from GTU-famicom, which does not properly bandpass the chroma signal.
// That results in GTU-famicom's chroma being oversaturated, due to the NES's chroma signal being a square wave instead of a sinusoid.
// Those incorrect saturation amounts can be fixed by just calculating them properly.
float lowLevels[4] = {(0.228 - 0.312) / (1.100 - 0.312), 0.0, (0.522 - 0.312) / (1.1 - 0.312), (0.880 - 0.312) / (1.1 - 0.312)};
float highLevels[4] = {(0.616 - 0.312) / (1.1 - 0.312), (0.84 - 0.312) / (1.1 - 0.312), 1.0, 1.0};
for(int i = 0; i < 4; i++) {
float amplitude = (highLevels[i] - lowLevels[i]) / 2;
levelSats[i] = 0.0;
for(int j = 0; j < 12; j++) {
levelSats[i] += 2 * amplitude * pow(sin(j / 12.0 * pi / 2.0), 2.0) / 12.0; // Use a sine wave instead of a square wave, to roughly simulate bandpassing. Result is a slight underestimate.
}
}
// Screw all that. Replace that entire eyeballed palette with a direct video capture of the NES's composite video.
// I originally wrote this code months ago, so pardon the mess. I've carefully looked through this
// horrible mess of code and made sure it works. The only problem is that emphasis bits aren't implemented.
// I do not know how I would implement emphasis bits into this lookup, but the result is already close enough for me.
float pal[192] = {
0.478321, 0.478585, 0.478334,
0.366470, 0.403287, 0.500952,
0.400869, 0.390277, 0.509457,
0.429738, 0.383858, 0.496003,
0.461984, 0.383553, 0.462478,
0.464060, 0.381734, 0.399996,
0.456407, 0.392188, 0.335543,
0.429468, 0.403545, 0.299086,
0.404554, 0.416319, 0.288624,
0.360529, 0.423274, 0.301706,
0.335662, 0.426387, 0.352844,
0.330750, 0.423530, 0.401436,
0.341014, 0.418906, 0.454200,
0.364416, 0.364810, 0.364445,
0.368611, 0.369176, 0.368467,
0.368611, 0.369176, 0.368467,
0.558871, 0.559355, 0.558862,
0.397998, 0.459932, 0.570550,
0.445642, 0.440000, 0.594072,
0.486899, 0.428728, 0.585057,
0.527970, 0.429614, 0.550925,
0.537326, 0.425318, 0.467786,
0.530366, 0.436656, 0.377826,
0.502618, 0.451730, 0.328883,
0.470762, 0.468125, 0.308494,
0.410478, 0.479321, 0.318534,
0.372913, 0.485220, 0.374387,
0.361928, 0.482245, 0.437718,
0.366940, 0.480164, 0.507869,
0.368611, 0.369176, 0.368467,
0.368611, 0.369176, 0.368467,
0.368611, 0.369176, 0.368467,
0.648117, 0.648838, 0.648073,
0.479182, 0.551627, 0.647106,
0.520994, 0.532234, 0.681346,
0.562577, 0.519448, 0.680616,
0.602906, 0.518809, 0.653828,
0.624149, 0.510424, 0.573186,
0.621879, 0.523408, 0.486425,
0.598359, 0.535614, 0.428962,
0.570986, 0.553840, 0.400425,
0.512108, 0.564480, 0.401383,
0.471310, 0.572906, 0.444626,
0.450232, 0.571801, 0.509053,
0.455226, 0.571961, 0.579762,
0.454777, 0.455136, 0.454750,
0.368611, 0.369176, 0.368467,
0.368611, 0.369176, 0.368467,
0.648117, 0.648838, 0.648073,
0.581149, 0.612569, 0.642884,
0.592490, 0.605032, 0.665907,
0.610979, 0.600103, 0.666567,
0.630066, 0.598694, 0.649834,
0.636469, 0.596019, 0.623470,
0.641093, 0.600434, 0.590489,
0.627745, 0.604366, 0.564505,
0.619065, 0.613878, 0.551681,
0.592630, 0.617272, 0.548023,
0.580770, 0.619989, 0.564797,
0.571081, 0.620166, 0.590528,
0.568069, 0.622819, 0.618941,
0.568471, 0.569152, 0.568223,
0.368611, 0.369176, 0.368467,
0.368611, 0.369176, 0.368467
};
#define OVERWRITE_COLOR(pal_offset, array_offset, arrays) \
arrays##Red[array_offset] = (pal[(pal_offset) * 3] - 0.368467) / (0.648117 - 0.368467); \
arrays##Green[array_offset] = (pal[(pal_offset) * 3 + 1] - 0.368467) / (0.648117 - 0.368467); \
arrays##Blue[array_offset] = (pal[(pal_offset) * 3 + 2] - 0.368467) / (0.648117 - 0.368467);
#define OVERWRITE_ROW(rowNumber) \
for(int i = 1; i <= 12; i++) { \
OVERWRITE_COLOR(i + (rowNumber) * 16, (i-1) % 12, level##rowNumber); \
}
#define OVERWRITE_PALETTE \
OVERWRITE_ROW(0); \
OVERWRITE_ROW(1); \
OVERWRITE_ROW(2); \
OVERWRITE_ROW(3); \
OVERWRITE_COLOR(30, 0, white); \
OVERWRITE_COLOR(0, 1, white); \
OVERWRITE_COLOR(16, 2, white); \
OVERWRITE_COLOR(48, 3, white);
OVERWRITE_PALETTE;
int unsatLevelI = -1; // Integer part. Can be 0, 1, or 2. If the level is 3 or higher, it is represented as 2 with a fractional part greater than 1.
float unsatLevelF; // Fractional part. If I=0, -1<F<1. If I=1, 0<F<1. If I=2, 0<F<2.
do {
if(unsatLevelI == -1)
unsatLevelI = 0;
else
unsatLevelI++; // This if-statement is stupid, but my stupid computer requires me to do this...
float unsatYRemaining = YIQ.r - unsaturatedLevels[unsatLevelI];
unsatLevelF = unsatYRemaining / (unsaturatedLevels[unsatLevelI + 1] - unsaturatedLevels[unsatLevelI]);
} while(unsatLevelI < 3 && unsatLevelF > 1.0);
// Note that the color does not cap at 0 and 1; it can be negative or greater than 1 too.
vec3 unsaturatedColor = vec3(whiteRed[unsatLevelI], whiteGreen[unsatLevelI], whiteBlue[unsatLevelI]) * (1 - unsatLevelF) +
unsatLevelF * vec3(whiteRed[unsatLevelI + 1], whiteGreen[unsatLevelI + 1], whiteBlue[unsatLevelI + 1]);
int satLevelI = -1; // Integer part. Can be 0, 1, or 2. If the level is 3 or higher, it is represented as 2 with a fractional part greater than 1.
float satLevelF; // Fractional part. If I=0, -1<F<1. If I=1, 0<F<1. If I=2, 0<F<2.
do {
if(satLevelI < 0)
satLevelI = 0;
else
satLevelI++;
float satYRemaining = YIQ.r - saturatedLevels[satLevelI];
satLevelF = satYRemaining / (saturatedLevels[satLevelI + 1] - saturatedLevels[satLevelI]);
} while(satLevelI < 2 && satLevelF > 1.0);
float hue = atan(YIQ.g, YIQ.b) / 2 / pi + 0.5;
int floorI = -1; // Hue integer that's less than hue.
int ceilI = 0; // Hue integer that's greater than hue.
float partI; // Fractional part of hue.
// Performed a linear regression on screenshotted test output.
// As long as the preset's settings are all nearest-neighbor at 8x horizontal resolution,
// this is the same for all setups.
float hueFitted = (hue - 0.0418747) / 0.0826453 + 12.0 - 3.0;
floorI = int(floor(hueFitted) + 0.5);
ceilI = floorI + 1;
partI = hueFitted - floorI;
while(floorI >= 12)
floorI -= 12;
while(ceilI >= 12)
ceilI -= 12;
// Old code using an array of screenshotted values.
// do {
// floorI++;
// ceilI = (floorI + 1) % 12;
// float floorH = levelHues[floorI];
// float ceilH = levelHues[ceilI];
// if(floorH < ceilH) {
// partI = (hue - floorH) / (ceilH - floorH);
// } else if(hue <= ceilH) {
// partI = (hue + 1 - floorH) / (ceilH + 1 - floorH);
// } else if(hue >= floorH) {
// partI = (hue - floorH) / (ceilH + 1 - floorH);
// } else { // hue >= ceilH && hue <= floorH
// continue;
// }
// } while(floorI < 12 && (partI >= 1.0 || partI < 0.0));
vec3 floorLow, ceilLow, floorHigh, ceilHigh;
switch(satLevelI) {
case 0:
floorLow = vec3(level0Red[floorI], level0Green[floorI], level0Blue[floorI]);
ceilLow = vec3(level0Red[ceilI], level0Green[ceilI], level0Blue[ceilI]);
floorHigh = vec3(level1Red[floorI], level1Green[floorI], level1Blue[floorI]);
ceilHigh = vec3(level1Red[ceilI], level1Green[ceilI], level1Blue[ceilI]);
// vec3 floorDiff = floorHigh - floorLow;
// vec3 ceilDiff = ceilHigh - ceilLow;
// while(satLevelF < 0) {
// satLevelF++;
// floorLow -= floorDiff;
// floorHigh -= floorDiff;
// ceilLow -= ceilDiff;
// ceilHigh -= ceilDiff;
// }
break;
case 1:
floorLow = vec3(level1Red[floorI], level1Green[floorI], level1Blue[floorI]);
ceilLow = vec3(level1Red[ceilI], level1Green[ceilI], level1Blue[ceilI]);
floorHigh = vec3(level2Red[floorI], level2Green[floorI], level2Blue[floorI]);
ceilHigh = vec3(level2Red[ceilI], level2Green[ceilI], level2Blue[ceilI]);
break;
case 2:
floorLow = vec3(level2Red[floorI], level2Green[floorI], level2Blue[floorI]);
ceilLow = vec3(level2Red[ceilI], level2Green[ceilI], level2Blue[ceilI]);
floorHigh = vec3(level3Red[floorI], level3Green[floorI], level3Blue[floorI]);
ceilHigh = vec3(level3Red[ceilI], level3Green[ceilI], level3Blue[ceilI]);
break;
}
// I've since learned that each of these calculations can easily be replaced with mix().
vec3 saturatedLow = floorLow + partI * (ceilLow - floorLow);
vec3 saturatedHigh = floorHigh + partI * (ceilHigh - floorHigh);
vec3 saturatedColor;
float expectedSaturationRaw;
if(satLevelI == 0 && satLevelF < 0.0) {
// For some palettes, it might be better to use the normal "else" clause instead of this special case.
expectedSaturationRaw = levelSats[0];
saturatedColor = saturatedLow + (YIQ.r - saturatedLevels[0]) / (unsaturatedLevels[1] - unsaturatedLevels[0]) * (vec3(whiteRed[1] - whiteRed[0], whiteGreen[1] - whiteGreen[0], whiteBlue[1] - whiteBlue[0]));
} else if(satLevelI == 2 && satLevelF > 1.0) {
expectedSaturationRaw = levelSats[3];
saturatedColor = saturatedHigh + (YIQ.r - saturatedLevels[3]) / (unsaturatedLevels[3] - unsaturatedLevels[2]) * (vec3(whiteRed[3] - whiteRed[2], whiteGreen[3] - whiteGreen[2], whiteBlue[3] - whiteBlue[2]));
} else {
saturatedColor = saturatedLow + satLevelF * (saturatedHigh - saturatedLow); // saturatedLow * (1 - satLevelF) + saturatedHigh * satLevelF;
expectedSaturationRaw = levelSats[satLevelI] + satLevelF * (levelSats[satLevelI + 1] - levelSats[satLevelI]);
}
float saturationRaw = sqrt(YIQ.g * YIQ.g + YIQ.b * YIQ.b);
float saturationNorm = saturationRaw / expectedSaturationRaw;
//RGB = vec3(saturationNorm);
vec3 RGB = unsaturatedColor + saturationNorm * (saturatedColor - unsaturatedColor);
return toRgb *
mat3(1, 0, 0,
0, saturation, 0,
0, 0, saturation) *
mat3(0.2126, -0.09991, 0.615, // Rec. 709 YUV matrix.
0.7152, -0.33609, -0.55861, // My video capture used Rec. 709.
0.0722, 0.436, -0.05639) * RGB;
}
//////////////////
// Inverse EOTF //
//////////////////
// CRT EOTF function taken from Grade; modified to perform its steps backward.
#define CRT_l -(100000.*log((72981.-500000./(3.*max(2.3,global.pn_g_CRT_l)))/9058.))/945461.
float EOTF_1886a_inverse_max(float bl, float brightness, float contrast) {
// Defaults:
// Black Level = 0.1
// Brightness = 0
// Contrast = 100
const float wl = 100.0;
float b = pow(bl, 1./2.4);
float a = pow(wl, 1./2.4)-b;
// b = (brightness-50.) / 250. + b/a; // -0.20 to +0.20
b = (brightness * 100.) / 250. + b/a; // -0.20 to +0.20
//a = contrast!=50. ? pow(2.,(contrast-50.)/50.) : 1.; // 0.50 to +2.00
a = contrast;
const float Vc = 0.35; // Offset
float Lw = wl/100. * a; // White level
float Lb = min( b * a,Vc); // Black level
const float a1 = 2.6; // Shoulder gamma
const float a2 = 3.0; // Knee gamma
float k = Lw /pow(1. + Lb, a1);
float sl = k * pow(Vc + Lb, a1-a2); // Slope for knee gamma
// What input value results in 1.0?
float bc = 0.00446395*pow(bl,1.23486);
float color = 1.0;
color = pow(color, 1.0 / (1.0-0.00843283*pow(bl,1.22744)));
color = color / (1.0 / (1.0 - bc)) + bc;
color = pow(color / k, 1.0 / a1) - Lb;
return color;
}
float EOTF_1886a_default_inverse_max(float brightness, float contrast) {
// From Rec. ITU-R BT.1886
float Vc = 0.35, // Vc, a1, and a2 are directly from the paper
a1 = 2.6,
a2 = 3.0,
Lw = contrast;
float V = 1.0,
b = brightness, // The paper recommends about either 0 or 0.1.
k = Lw / pow(1.0 + b, a1);
// What value of V gives a result of 1.0?
// 1.0 = k * pow(V + b, a1)
// pow(1.0 / k, 1.0 / a1) - b = V
return pow(1.0 / k, 1.0 / a1) - b;
}
float gammaInverseMaxPickable() {
if(global.pn_gamma_type < 0.5) {
return 1.0 / EOTF_1886a_inverse_max(CRT_l, global.pn_knob_brightness, global.pn_knob_contrast);
} else if(global.pn_gamma_type < 1.5) {
return 1.0 / EOTF_1886a_default_inverse_max(global.pn_knob_brightness, global.pn_knob_contrast);
} else if(global.pn_gamma_type < 2.5) {
return pow(global.pn_knob_contrast, 1.0 / global.pn_power_gamma);
} else {
float contr = 1.055 * pow(global.pn_knob_contrast, 1.0 / 2.4) - 0.055; // 2.4 instead of 2.2 is not a mistake.
// rgb = (rgb + global.pn_knob_brightness) / (1 + global.pn_knob_brightness) * contr;
// rgb * (1 + global.pn_knob_brightness) / contr - global.pn_knob_brightness = rgb
return 1.0 / ((1 + global.pn_knob_brightness) / contr - global.pn_knob_brightness);
}
}
#endif // __patchy_ntsc_inc_filters_inc__

View File

@ -34,6 +34,7 @@ layout(std140, set = 0, binding = 0) uniform UBO
pn_color_screen_offset,
pn_nes_enable,
pn_nes_phase_mod,
pn_nes_real_capture,
pn_nes_hue_skew,
pn_genesis_palette,
pn_genesis_jailbar_enable,
@ -122,7 +123,8 @@ layout(std140, set = 0, binding = 0) uniform UBO
#pragma parameter pn_comment_nes_palette "== Note: Use Mesen (not FCEUmm) and change palette to Raw ==" 0 0 0 1
#pragma parameter pn_nes_enable "Enable NES Raw Palette Mode" 0 0 1 1
#pragma parameter pn_nes_phase_mod "NES Battletoads / Battletoads Double Dragon Phase Cycle" 0 0 1 1
#pragma parameter pn_nes_hue_skew "NES colors emulated PPU version: 2C02G | 2C02E" 0 0 1 1
#pragma parameter pn_nes_real_capture "NES color fix: Hue-skew | Real video capture LUT (unfinished)" 0 0 1 1
#pragma parameter pn_nes_hue_skew "NES hue-skew PPU version: 2C02G | 2C02E" 0 0 1 1
#pragma parameter pn_comment_genesis_jailbar "=== Genesis Settings ===" 0 0 0 1
#pragma parameter pn_genesis_palette "Genesis Plus GX color fix (not for BlastEm or other consoles)" 0 0 1 1

View File

@ -282,7 +282,7 @@ void main()
} else {
if(global.pn_genesis_palette > 0.5) {
if(global.pn_genesis_palette > 0.5 && global.pn_test_pattern < 0.5) {
colorIn.rgb = fixGenColor(colorIn.rgb);
}

View File

@ -72,20 +72,19 @@ void main()
if(global.pn_nes_enable < 0.5) {
luma *= global.pn_color_amplitude; // Correction by the color carrier amplitude
} else {
}
float tint = global.pn_knob_tint * pi / 180.0;
if(global.pn_nes_enable > 0.5 && global.pn_nes_real_capture < 0.5) {
// From lidnariq's measurements at http://forums.nesdev.org/viewtopic.php?p=159266#p159266
// 30 = colorburst max
// -23 = colorburst min
// 110 = white
// 0 = black
float nesAmp = (30.0 - -23.0) / 2.0 / (110.0 - 0.0);
float stdAmp = 0.2; // Standard colorburst amplitude in NTSC
float stdAmp = 0.2 * 100.0 / 110.0; // Standard colorburst amplitude in NTSC
chroma *= stdAmp / nesAmp;
}
float tint = global.pn_knob_tint * pi / 180.0;
if(global.pn_nes_enable > 0.5) {
float saturatedLevels[4] = {((0.228 + 0.616) / 2. - 0.312) / (1.100 - 0.312), ((0.312 + 0.840) / 2. - 0.312) / (1.100 - 0.312), ((0.552 + 1.100) / 2. - 0.312) / (1.100 - 0.312), ((0.880 + 1.100) / 2. - 0.312) / (1.100 - 0.312)}; // Calculated from DAC.slang from GTU-famicom
int satLevelI = -1; // Integer part. Can be 0, 1, or 2. If the level is 3 or higher, it is represented as 2 with a fractional part greater than 1.
@ -105,16 +104,13 @@ void main()
float skew;
if(global.pn_nes_hue_skew < 0.5) {
// 2C02G
skew = (satLevelI + satLevelF) * -5.0 / 180.0 * pi; // -5 degrees
skew = -5.0 / 180.0 * pi; // -5 degrees
} else {
// 2C02E
skew = (satLevelI + satLevelF) * -2.5 / 180.0 * pi; // -2.5 degrees
skew = -2.5 / 180.0 * pi; // -2.5 degrees
}
// 15 degree rotation was a very close match against real hardware video capture.
// It makes sense because it's half of an NES PPU cycle.
// The change is done here instead of in the video signal for better precision, due to the video signal sample resolution being the same as the NES PPU cycle rate.
tint += -skew - 15.0 / 180.0 * pi;
tint += (3.0 - (satLevelI + satLevelF)) * skew;
}
vec2 bmyRmy = uvDemodPickable(global.pn_demodulator_chroma_filter_type, chroma, phase, tint);

View File

@ -52,9 +52,16 @@ void main()
// A slight lowpass on B-Y and R-Y after demodulating
vec2 rmybmy = lowpassGTU(240).gb;
vec3 rgb = YBmyRmyToRGBMatrix() * vec3(y, rmybmy * global.pn_knob_saturation);
rgb *= global.pn_knob_contrast;
// rgb += global.pn_knob_brightness; // Brightness will be handled later.
vec3 rgb;
if(global.pn_nes_enable > 0.5 && global.pn_nes_real_capture > 0.5) {
rgb = nesRealCaptureLookup(vec3(y, rmybmy), global.pn_knob_saturation);
} else {
rgb = YBmyRmyToRGBMatrix() * vec3(y, rmybmy * global.pn_knob_saturation);
}
rgb = (rgb + global.pn_knob_brightness) / (1 + global.pn_knob_brightness);
rgb *= gammaInverseMaxPickable();
if(global.pn_rgb_smear_enable > 0.5)
rgb = min(vec3(global.pn_rgb_smear_clamp), rgb);

View File

@ -146,20 +146,20 @@ void main()
rgb = min(col, 1);
}
rgb /= global.pn_knob_contrast;
rgb /= gammaInverseMaxPickable();
rgb = rgb * (1.0 + global.pn_knob_brightness) - global.pn_knob_brightness;
if(global.pn_gamma_type < 0.5) {
FragColor = vec4(EOTF_1886a_f3(rgb, CRT_l, global.pn_knob_brightness, global.pn_knob_contrast), 1.0);
} else if(global.pn_gamma_type < 1.5) {
FragColor = vec4(clamp(EOTF_1886a_default_f3(rgb, global.pn_knob_brightness, global.pn_knob_contrast), 0, 1), 1.0);
} else if(global.pn_gamma_type < 2.5) {
rgb = max(rgb * (1.0 - global.pn_knob_brightness) + global.pn_knob_brightness, 0);
rgb = max((rgb + global.pn_knob_brightness) / (1.0 + global.pn_knob_brightness), 0);
rgb *= pow(global.pn_knob_contrast, 1.0 / global.pn_power_gamma);
FragColor = vec4(pow(min(rgb, vec3(1)), vec3(global.pn_power_gamma)), 1.0);
} else {
// The different values 2.2 and 2.4 are not a mistake.
rgb = max(rgb * (1.0 - global.pn_knob_brightness) + global.pn_knob_brightness, 0);
rgb *= global.pn_knob_contrast <= 1.0 ? linear_to_sRGB(vec3(global.pn_knob_contrast), 2.4).r : pow(global.pn_knob_contrast, 1.0 / 2.2);
float contr = 1.055 * pow(global.pn_knob_contrast, 1.0 / 2.4) - 0.055; // 2.4 instead of 2.2 is not a mistake.
rgb = (rgb + global.pn_knob_brightness) / (1 + global.pn_knob_brightness) * contr;
FragColor = vec4(sRGB_to_linear(rgb, 2.4), 1.0);
}
}

View File

@ -39,9 +39,9 @@ layout(std140, set = 0, binding = 0) uniform UBO
mat4 MVP;
} global;
#pragma parameter lut_toggle "Gamut: sRGB, P22-80s, P22-90s, P22-J, Trinitron P22" 0 0 4 1
#pragma parameter lut_index "Reference White: SMPTE-C (D65) | NTSC-U (C) | NTSC-J (D93)" 1 0 2 1
//#pragma parameter lut_chroma_adapt "(For Trinitron Only) Chromatic Adaptation to D65" 0 0 1 1
#pragma parameter lut_toggle "Gamut: sRGB, P22-80s, P22-90s, P22-J, Trinitron P22" 4 0 4 1
#pragma parameter lut_index "White point: SMPTE-C (D65) | NTSC-U (C) | NTSC-J (9300K+27MPCD)" 1 0 2 1
#pragma parameter lut_chroma_adapt "(For P22-J and Trinitron w/ NTSC-J) Chromatic Adaptation to D65" 0 0 1 1
#pragma stage vertex
layout(location = 0) in vec4 Position;
@ -63,7 +63,8 @@ layout(set = 0, binding = 3) uniform sampler2D PhosphorSamplerLUT1;
layout(set = 0, binding = 4) uniform sampler2D PhosphorSamplerLUT2;
layout(set = 0, binding = 5) uniform sampler2D PhosphorSamplerLUT3;
layout(set = 0, binding = 6) uniform sampler2D PhosphorSamplerLUT4;
//layout(set = 0, binding = 7) uniform sampler2D PhosphorSamplerLUT5;
layout(set = 0, binding = 7) uniform sampler2D PhosphorSamplerLUT5;
layout(set = 0, binding = 8) uniform sampler2D PhosphorSamplerLUT6;
#define saturate(c) clamp(c, 0.0, 1.0)
@ -72,9 +73,11 @@ void main()
vec2 LUT_Size = textureSize(PhosphorSamplerLUT1, 0); // All are the same size... right?
vec3 imgColor = texture(Source, vTexCoord.xy).rgb;
// Chromatic adaptation is available for P22-J and Trinitron P22 when using white D93.
bool doChromaAdapt = params.lut_toggle > 2.5 && params.lut_index > 1.5 && params.lut_chroma_adapt > 0.5;
// Force disable chromatic adaptation if a chroma-adapted LUT is missing.
// The current version lacks any chroma-adapted LUTs whatsoever, so the check is commented out.
// if(params.lut_chroma_adapt < 0.5 || params.lut_toggle < 2.5) {
if(!doChromaAdapt) {
// Change the white balance ourselves, and use the D65-to-D65 LUT.
float Wx, Wy;
@ -131,7 +134,7 @@ void main()
vec3 normConsts = cNormConsts / d65NormConsts;
normConsts /= max(normConsts.r, max(normConsts.g, normConsts.b));
imgColor *= normConsts; // Scale R, G, and B to get the white point that we want.
// }
}
if(params.lut_toggle < 0.5) {
FragColor = vec4(imgColor, 1.0);
@ -172,15 +175,23 @@ void main()
RcGcBf = (texture(SamplerLUT, vec2(floors.b + ceils.r, ceils.g))).xyz; \
RcGcBc = (texture(SamplerLUT, vec2(ceils.b + ceils.r, ceils.g))).xyz;
if(params.lut_toggle < 1.5) {
eightSamples(PhosphorSamplerLUT1);
} else if(params.lut_toggle < 2.5) {
eightSamples(PhosphorSamplerLUT2);
} else if (params.lut_toggle < 3.5) {
eightSamples(PhosphorSamplerLUT3);
} else {
eightSamples(PhosphorSamplerLUT4);
}
if(doChromaAdapt) {
if(params.lut_toggle < 3.5) {
eightSamples(PhosphorSamplerLUT5);
} else {
eightSamples(PhosphorSamplerLUT6);
}
} else {
if(params.lut_toggle < 1.5) {
eightSamples(PhosphorSamplerLUT1);
} else if(params.lut_toggle < 2.5) {
eightSamples(PhosphorSamplerLUT2);
} else if (params.lut_toggle < 3.5) {
eightSamples(PhosphorSamplerLUT3);
} else {
eightSamples(PhosphorSamplerLUT4);
}
}
#undef eightSamples