NTSC shader discussion

For the ones written by themaister, it’s in the preset. It scales the first pass to 4x horizontal and then scales it back down later.

1 Like

Thanks!

Yeah, I don’t think I’m going to be able to do what I was wanting to do, merging GTU’s signal bandwidth (RGB/SCART Mode) and themaisters composite/s-video modes into one shader.

I mean I think it’s possible but I don’t have any idea how shoehorn all that together with it still working properly. (I know I’d need the second pass for the ntsc modes.)

My main issue is figuring how to adapt themaisters ntsc shaders to use GTU’s bandwidth for scaling. (Or to be more accurate? How to use GTU’s scaling, and still have the ntsc portion work properly when called upon.)

Love to hear any suggestions that people have?

The lazy solution I thought of would be to use @Dogway’s bandwidth shader then run ntsc adaptive after it, I’d probably modify them both so the ntsc/bandwidth shaders shared settings (pretty much only a single setting, signal mode) and add some ifs for signal switching/passthrough.(Though I’d much rather nock it down to two shader vr 3 shaders)

So is it as simple as choosing 2-phase for systems that output a 320px wide image (e.g., Genesis) and 3-phase for systems that output a 256px wide image (e.g., NES, SNES)?

In the tvout presets folder I see 256px and 320px presets for composite and s-video, but I also see separate presets for 2-phase composite and s-video and 3-phase composite and s-video. Whats the difference?

Reposting from “How crappy of a TV do you need for composite video artifacts?”

A few questions for the experts:

What should the maximum values for I/Q be when using TVout-tweaks presets, if you’re trying to emulate composite video? There should be a maximum value for these given the limited signal bandwidth of composite but I have no idea what it should be. I’ve just been eyeballing this at around 100-120, but I’d like to have something empirical to base this on.

I want to emulate different types of filters used in different TVs. Can the composite video emulation be edited to reduce/remove certain effects?

No comb filter- all the artifacts are present and horizontal resolution is limited to 260 lines.

two line analogue comb filter- reduction of rainbow artifacts, improved horizontal sharpness

three line analogue comb filter- same as two line filter, plus reduction of dot crawl and improved vertical sharpness

2D three line adaptive comb filter: dot crawl almost completely eliminated and sharpness nearly identical to S-video

3D motion adaptive comb filter: basically identical to S-video; artifacts are rare and really hard to spot; basically need a side by side comparison to see artifacts.

Also, what’s the recommended way to do composite video emulation on a digital display? I get the feeling the TVout presets are intended for outputting to a CRT but I’m new to this whole composite video thing.

1 Like

I don’t know much about this, in general, but the current ntsc shaders treat fringing and artifacting as a 1/0 on/off thing. You could try putting other values in there to see if you can get partial/variable expression of them.

EDIT: for the preset question, the default is composite, so the 256px and 320px presets are just the bare 3- and 2-phase shaders. Then you have svideo variants of them, and then each of those has a variant with scanlines.

For the I/Q bandwidth, it seems NTSC uses 4:1:1 chroma subsampling: https://en.wikipedia.org/wiki/Chroma_subsampling#4:1:1 and that puts the bandwidth of I and Q each at approximately 1/4 of the luma sample rate, for a combined bandwidth of about half. :man_shrugging:

3 Likes

I would suggest to look into some decomb filters for Avisynth and check the sources. There’s also checkmate, derainbow, etc.

So far from what I have been reading, there seems to be a “sanctioned” YIQ matrix by the FCC, which is not the 1953 one and that it’s been suggested to be used for game systems.

RGB to YIQ
 	0.299996928307425,  0.590001575542717,  0.110001496149858,
 	0.599002392519453, -0.277301256521204, -0.321701135998249,
 	0.213001700342824, -0.52510120528935,  0.312099504946526

I read that these transformations should be in linear space (commonly only the luma signal was gamma corrected), but I’m not getting correct results with that. What I indeed do is convert to video levels prior to NTSC artifacts, and then convert back before conversion to sRGB primaries. I also would like to preconvert to YCbCr before sRGB as I believe that’s what was commonly transformed to, and I can inherit its gamut by clamping. Basically trying to replicate the transformation chain of a NTSC signal.

3 Likes

So I played with Doriphor ntsc shader a bit more, implemented Y_RES, I_RES and Q_RES, then since that means nothing to must of us I added a preset system, in the shader comments there are more variations explained, to note I also implemented PAL signal emulation.

As glad as I am on how this turned out there are some concerns on putting everything together. All or most ntsc shaders on the repo rely on low pass filters (for the ntsc artifacts) to emulate bandwidth related blurriness. I would think these should be separated parts of the chain, maybe by using better FIR filters.

I also want to implement proper composite decoding ala xot shader, and modulation, artifacts, and modulate back. I will leave RGB (YPbPr) for the last if I’m still in the mood.

#version 450

layout(push_constant) uniform Push
{
    vec4 SourceSize;
    vec4 OriginalSize;
    vec4 OutputSize;
    uint FrameCount;
    float SPLIT;
    float PRESET;
    float PAL;
    float Y_RES;
    float I_RES;
    float Q_RES;
    float I_SHIFT;
    float Q_SHIFT;
    float Y_MUL;
    float I_MUL;
    float Q_MUL;
} params;

// based on Doriphor ntsc shader
// https://forums.libretro.com/t/please-show-off-what-crt-shaders-can-do/19193/1482?u=dogway

// Other NTSC
// Y:4.2   I: 0.60   Q: 0.60 (1993-2020) (for FCC NTSC Broadcast analogue standard) (band limited)
// Y:2.0   I: 0.30   Q: 0.30 (1993-2020) (for FCC NTSC VHS analogue standard) (band limited)

// Suggestions (NTSC):
// Y:4.20   I: 1.30   Q: 0.40 (1953-1993) (for FCC NTSC analogue standard -old-)
// Y:4.20   I: 1.50   Q: 0.50 (1953-1993) (for FCC NTSC analogue standard -old-)
// Y:4.20   U: 1.30   V: 1.30 (1993-1998) (for FCC NTSC analogue standard -new-) (chroma band limited)
// Y:4.20   I: 0.895  Q: 0.895(1998-2003) (for FCC NTSC digital standard 4:1:1)
// Y:4.20   I: 1.79   Q: 1.79 (1993-2020) (for FCC NTSC S-Video & digital -new- 4:2:2) (max subcarrier width)

// Suggestions (PAL):
// PAL should be a little bit more desaturated tan NTSC
// PAL chroma is also band limited -in analogue- to 1.30Mhz despite using a wider subcarrier than NTSC (4.4336)
// Y:5.0   U: 1.30   V: 1.30  PAL-B (for EBU 601 analogue standard -old-) (chroma band limited)
// Y:5.5   U: 1.30   V: 1.30  PAL-A (for EBU 601 analogue standard -old-) (System I: UK, Italy, Australia)
// Y:5.0   U: 1.80   V: 1.80  PAL-B (for EBU 601 digital standard 4:2:2)  (chroma band limited)
// Y:5.5   U: 1.80   V: 1.80  PAL-A (for EBU 601 digital standard 4:2:2)  (chroma band limited)
// Y:5.5   U: 2.217  V: 2.217 PAL-A (for EBU 601 digital standard 4:2:2) (max subcarrier width)

#pragma parameter SPLIT "Split" 0.0 -1.0 1.0 0.1
#pragma parameter PRESET "0:CUS|1:PALA|2:PALB|3:NTSC|4:NTSC411|5:NTSC422" 0.0 0.0 5.0 1.0
#pragma parameter PAL "0:NTSC 1:PAL" 0.0 0.0 1.0 1.0
#pragma parameter Y_RES "Y Mhz" 4.2 2.5 6.0 0.01
#pragma parameter I_RES "I Mhz" 1.3 0.4 4.0 0.05
#pragma parameter Q_RES "Q Mhz" 0.4 0.4 4.0 0.05
#pragma parameter I_SHIFT "I Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Q_SHIFT "Q Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Y_MUL "Y Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter I_MUL "I Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter Q_MUL "Q Multiplier" 1.0 0.0 2.0 0.1

layout(std140, set = 0, binding = 0) uniform UBO
{
    mat4 MVP;
} global;

#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;

vec4 RGB_YIQ(vec4 col)
{
    mat3 conv_mat = mat3(
    0.299996928307425,  0.590001575542717,  0.110001496149858,
    0.599002392519453, -0.277301256521204, -0.321701135998249,
    0.213001700342824, -0.52510120528935,  0.312099504946526);

    col.rgb *= conv_mat;

    return col;
}

vec4 YIQ_RGB(vec4 col)
{
    mat3 conv_mat = mat3(
    1.0,  0.946882217090069,  0.623556581986143,
    1.0, -0.274787646298978, -0.635691079187380,
    1.0, -1.108545034642030,  1.709006928406470);

    col.rgb *= conv_mat;

    return col;
}

vec4 RGBtoYUV(vec4 RGB)
 {
     mat3 conv_mat = mat3(
     0.299,    0.587,   0.114,
    -0.14713,-0.28886,  0.436,
     0.615, -0.514991, -0.10001);
 
    RGB.rgb *= conv_mat;
    return RGB;
 }

vec4 YUVtoRGB(vec4 YUV)
 {
     mat3 conv_mat = mat3(
     1.000, 0.000,   1.13983,
     1.000,-0.39465,-0.58060,
     1.000, 2.03211, 0.00000);
 
    YUV.rgb *= conv_mat;
    return YUV;
  }



// to Studio Swing (in YIQ space) (for footroom and headroom)
vec4 PCtoTV(vec4 col)
{
   col *= 255;
   col.x = ((col.x * 219) / 255) + 16;
   col.y = (((col.y - 128) * 224) / 255) + 112;
   col.z = (((col.z - 128) * 224) / 255) + 112;
   return vec4(col.xyz, 1.0) / 255;
}


// to Full Swing (in YIQ space)
vec4 TVtoPC(vec4 col)
{
   col *= 255;
   float colx = ((col.x - 16) / 219) * 255;
   float coly = (((col.y - 112) / 224) * 255) + 128;
   float colz = (((col.z - 112) / 224) * 255) + 128;
   return vec4(colx,coly,colz, 1.0) / 255;
}


void main()
{
    #define ms *pow(10.0, -9.0)
    #define MHz *pow(10.0, 9.0);

    float max_col_res_I = 0.0;
    float max_col_res_Q = 0.0;
    float max_lum_res = 0.0;

    //  88 Mhz is VHF FM modulation (NTSC-M and PAL-AB, NTSC-J uses 90 Mhz)
    //  Luma signal runs over 4.2Mhz (5.0 PAL), whereas Chroma does on 3.5795 (4.4336 PAL)
    if (params.PRESET == 0.0)
    {
        float blank =   (params.PAL==1.0) ? 12.0                      : 10.9;
        float scan_ms = (params.PAL==1.0) ? 1000000.*(1./625.)*(1./25.)-blank : \
                                            1000000.*(1./525.)*(1./30.)-blank;
        float Ch_SubC = (params.PAL==1.0) ? 390.15845                 : 315.0;
        float Y_Carr =  (params.PAL==1.0) ? 440.0                     : 369.6;
        float Y_CUS =   (params.Y_RES != 4.2) ? params.Y_RES * 88.0   : Y_Carr;

        max_col_res_I = (params.I_RES / 2.0) * scan_ms ms * Ch_SubC/88.0 MHz;
        max_col_res_Q = (params.Q_RES / 2.0) * scan_ms ms * Ch_SubC/88.0 MHz;
        max_lum_res = scan_ms ms * Y_CUS/88.0 MHz;
    }
    if (params.PRESET == 1.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_lum_res = 52.0 ms * 484.0/88.0 MHz;
    }
    if (params.PRESET == 2.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_lum_res = 52.0 ms * 440.0/88.0 MHz;
    }
    if (params.PRESET == 3.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (0.40 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }
    if (params.PRESET == 4.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }
    if (params.PRESET == 5.0)
    {
        max_col_res_I = (1.79 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (1.79 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }

    const int viewport_col_resy = int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_col_res_I)));
    const int viewport_col_resz = int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_col_res_Q)));
    const int viewport_lum_res =  int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_lum_res)));

    if(vTexCoord.x - params.SPLIT - 1.0 > 0.0 || vTexCoord.x - params.SPLIT < 0.0)
    {
        FragColor = vec4(texture(Source, vTexCoord).rgb, 1.0);
    }
    else
    {
        vec4 col = vec4(0.0, 0.0, 0.0, 1.0);

        if ((params.PRESET > 0.5) && (params.PRESET < 2.5) || (params.PAL == 1.0))
        {
            col += RGBtoYUV(texture(Source, vTexCoord));

            for(int i = 1; i < viewport_col_resy; i++)
            {
                col.y += RGBtoYUV(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).y;
            }
            for(int i = 1; i < viewport_col_resz; i++)
            {
                col.z += RGBtoYUV(texture(Source, vTexCoord - vec2((i - viewport_col_resz/2) * params.OutputSize.z, 0.0))).z;
            }
            for(int i = 1; i < viewport_lum_res; i++)
            {
                col.x += RGBtoYUV(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).x;
            }
        }
        else
        {
            col += RGB_YIQ(texture(Source, vTexCoord));

            for(int i = 1; i < viewport_col_resy; i++)
            {
                col.y += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).y;
            }
            for(int i = 1; i < viewport_col_resz; i++)
            {
                col.z += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resz/2) * params.OutputSize.z, 0.0))).z;
            }
            for(int i = 1; i < viewport_lum_res; i++)
            {
                col.x += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).x;
            }
        }


        col.y /= viewport_col_resy;
        col.z /= viewport_col_resz;
        col.x /= viewport_lum_res;
        col = PCtoTV(col);


        col.y = mod((col.y + 1.0) + params.I_SHIFT, 2.0) - 1.0;
//      col.y = 0.9 * col.y + 0.1 * col.y * col.x;
//
        col.z = mod((col.z + 1.0) + params.Q_SHIFT, 2.0) - 1.0;
//      col.z = 0.4 * col.z + 0.6 * col.z * col.x;
//      col.x += 0.5*col.y;

        col.z *= params.Q_MUL;
        col.y *= params.I_MUL;
        col.x *= params.Y_MUL;

        if ((params.PRESET > 0.5) && (params.PRESET < 2.5) || (params.PAL == 1.0))
        {
            col = clamp(col,vec4(0.0627,0.0627-0.5,0.0627-0.5,0.0),vec4(0.92157,0.94118-0.5,0.94118-0.5,1.0));
            col = YUVtoRGB(TVtoPC(col));
        }
        else
        {
            col = clamp(col,vec4(0.0627,-0.5957,-0.5226,0.0),vec4(0.92157,0.5957,0.5226,1.0));
            col = YIQ_RGB(TVtoPC(col));
        }
       
        FragColor = clamp(col,0.0,1.0);

    }
}
5 Likes

Looks good so far but I want to make sure I’m using it correctly. Should this pass go after other color related stuff? I’m guessing that the shader assumes the display is calibrated to sRGB, is that correct?

1 Like

No, not necessary, sRGB is the defacto space for digital content.

I haven’t updated my presets yet because I’m into this ntsc madness still, but the order in my opinion should be akin to the chain on a TV set. (Dedither/whatever)->NTSC->Composite->NTSC->sRGB->Grade->scanlines.

Within ntsc there’s another order I’m still to clear up, but I think it would be: RGB to YIQ -> Signal Bandwidth -> Composite -> Modulate -> Artifacts -> low pass (blur) -> Demodulate -> YIQ to sRGB

I could be wrong but signal bandwidth might be a byproduct of band passing the chroma (and luma) subcarrier, but I don’t think I have seen a full implementation that accounts for different systems like Doriphor’s shader even if it’s simpler than other ones. I’m not concerned on making a 1:1 version of the DSP, but I would like to get blurriness and colors on the ballpark a least.

There’s code I can borrow for all the remaining steps except the “low pass” which I found all too blurry (should test more).

3 Likes

@Dogway Look forward to what you end up figuring about this:

2 Likes

I was stuck at porting ntsc-signal-bandwidth to glsl, maybe @hunterk can help me with that, it’s in the “for” calls.

My intention is to take the Themaister’s ntsc modulate-artifact-demodulate part and implement into my ntsc signal bandwidth. The modulation requires a bigger resolution, so I need to multiply the blurring effect of the signal bandwidth, that means I will try removing the lowpass filter, or design a new one that’s sharper.

Currently I’m using 3 passes, ntsc with a very high resolution (so it doesn’t blur much, but also doesn’t artifact as much -bad-) and then my signal bandwidth shader. It takes time so I will be doing it in my free time.

3 Likes

Looks like it was just a bunch of compiler nitpicks; implicit casts, non-constant consts, etc.

// mod of Doriphor ntsc shader
// https://forums.libretro.com/t/please-show-off-what-crt-shaders-can-do/19193/1482

// Other NTSC
// Y:4.2   I: 0.60   Q: 0.60 (1993-2020) (for FCC NTSC Broadcast analogue standard) (band limited)
// Y:2.0   I: 0.30   Q: 0.30 (1993-2020) (for FCC NTSC VHS analogue standard) (band limited)

// Suggestions (NTSC):
// Y:4.20   I: 1.30   Q: 0.40 (1953-1993) (for FCC NTSC analogue standard -old-)
// Y:4.20   I: 1.50   Q: 0.50 (1953-1993) (for FCC NTSC analogue standard -old-)
// Y:4.20   U: 1.30   V: 1.30 (1998-2020) (for FCC NTSC digital standard 4:1:1) (chroma band limited)
// Y:4.20   U: 1.79   V: 1.79 (1998-2020) (for FCC NTSC S-Video & digital -new- 4:2:2) (max subcarrier width)

// Suggestions (PAL):
// PAL should be a little bit more desaturated than NTSC
// PAL chroma is also band limited -in analogue- to 1.30Mhz despite using a wider subcarrier than NTSC (4.4336)
// Y:5.5   U: 1.30   V: 1.30  PAL-A (for EBU 601 analogue standard -old-) (System I: UK, Italy, Australia)
// Y:5.0   U: 1.30   V: 1.30  PAL-B (for EBU 601 analogue standard -old-) (chroma band limited)
// Y:5.5   U: 1.80   V: 1.80  PAL-A (for EBU 601 digital standard 4:2:2)  (chroma band limited)
// Y:5.0   U: 1.80   V: 1.80  PAL-B (for EBU 601 digital standard 4:2:2)  (chroma band limited)
// Y:5.5   U: 2.217  V: 2.217 PAL-A (for EBU 601 digital standard 4:2:2) (max subcarrier width)


#pragma parameter SPLIT "Split" 0.0 -1.0 1.0 0.1
#pragma parameter S_PRESET "0:CUS|1:PALA|2:PALB|3:NTSC|4:NTSC411|5:NTSC422" 0.0 0.0 5.0 1.0
#pragma parameter PAL "0:NTSC 1:PAL" 0.0 0.0 1.0 1.0
#pragma parameter CH_SPC "YIQ/YUV" 0.0 0.0 1.0 1.0
#pragma parameter Y_RES "Y Mhz" 4.2 2.5 6.0 0.01
#pragma parameter I_RES "I/U Mhz" 1.3 0.4 4.0 0.05
#pragma parameter Q_RES "Q/V Mhz" 0.4 0.4 4.0 0.05
#pragma parameter I_SHIFT "I/U Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Q_SHIFT "Q/V Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Y_MUL "Y Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter I_MUL "I/U Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter Q_MUL "Q/V Multiplier" 1.0 0.0 2.0 0.1

#if defined(VERTEX)

#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#endif

#ifdef GL_ES
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

COMPAT_ATTRIBUTE vec4 VertexCoord;
COMPAT_ATTRIBUTE vec4 COLOR;
COMPAT_ATTRIBUTE vec4 TexCoord;
COMPAT_VARYING vec4 COL0;
COMPAT_VARYING vec4 TEX0;

vec4 _oPosition1;
uniform mat4 MVPMatrix;
uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;

// compatibility #defines
#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define OutSize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
    gl_Position = MVPMatrix * VertexCoord;
    TEX0.xy = TexCoord.xy;
}

#elif defined(FRAGMENT)

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_TEXTURE texture
out COMPAT_PRECISION vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D
#endif

uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;

// compatibility #defines
#define Source Texture
#define vTexCoord TEX0.xy

#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define OutSize vec4(OutputSize, 1.0 / OutputSize)

#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float SPLIT;
uniform COMPAT_PRECISION float S_PRESET;
uniform COMPAT_PRECISION float PAL;
uniform COMPAT_PRECISION float CH_SPC;
uniform COMPAT_PRECISION float Y_RES;
uniform COMPAT_PRECISION float I_RES;
uniform COMPAT_PRECISION float Q_RES;
uniform COMPAT_PRECISION float I_SHIFT;
uniform COMPAT_PRECISION float Q_SHIFT;
uniform COMPAT_PRECISION float Y_MUL;
uniform COMPAT_PRECISION float I_MUL;
uniform COMPAT_PRECISION float Q_MUL;
#else
#define SPLIT 0.0
#define S_PRESET 0.0
#define PAL 0.0
#define CH_SPC 0.0
#define Y_RES 4.2
#define I_RES 1.3
#define Q_RES 0.4
#define I_SHIFT 0.0
#define Q_SHIFT 0.0
#define Y_MUL 1.0
#define I_MUL 1.0
#define Q_MUL 1.0
#endif

vec4 RGB_YIQ(vec4 col)
{
    mat3 conv_mat = mat3(
    0.299996928307425,  0.590001575542717,  0.110001496149858,
    0.599002392519453, -0.277301256521204, -0.321701135998249,
    0.213001700342824, -0.52510120528935,  0.312099504946526);

    col.rgb *= conv_mat;

    return vec4(col.rgb, 1.0);
}

vec4 YIQ_RGB(vec4 col)
{
    mat3 conv_mat = mat3(
    1.0,  0.946882217090069,  0.623556581986143,
    1.0, -0.274787646298978, -0.635691079187380,
    1.0, -1.108545034642030,  1.709006928406470);

    col.rgb *= conv_mat;

    return vec4(col.rgb, 1.0);
}

vec4 RGBtoYUV(vec4 RGB)
 {
     mat3 conv_mat = mat3(
     0.299,    0.587,   0.114,
    -0.14713,-0.28886,  0.436,
     0.615, -0.514991, -0.10001);
 
    RGB.rgb *= conv_mat;
    return vec4(RGB.rgb, 1.0);
 }

vec4 YUVtoRGB(vec4 YUV)
 {
     mat3 conv_mat = mat3(
     1.000, 0.000,   1.13983,
     1.000,-0.39465,-0.58060,
     1.000, 2.03211, 0.00000);
 
    YUV.rgb *= conv_mat;
    return vec4(YUV.rgb, 1.0);
  }


// to Studio Swing (in YIQ space) (for footroom and headroom)
vec4 PCtoTV(vec4 col)
{
   col *= 255.;
   col.x = ((col.x * 219.) / 255.) + 16.;
   col.y = (((col.y - 128.) * 224.) / 255.) + 112.;
   col.z = (((col.z - 128.) * 224.) / 255.) + 112.;
   return vec4(col.xyz, 1.0) / 255.;
}


// to Full Swing (in YIQ space)
vec4 TVtoPC(vec4 col)
{
   col *= 255.;
   float colx = ((col.x - 16.) / 219.) * 255.;
   float coly = (((col.y - 112.) / 224.) * 255.) + 128.;
   float colz = (((col.z - 112.) / 224.) * 255.) + 128.;
   return vec4(colx,coly,colz, 1.0) / 255.;
}


void main()
{
    #define ms *pow(10.0, -9.0)
    #define MHz *pow(10.0, 9.0);

    float max_col_res_I = 0.0;
    float max_col_res_Q = 0.0;
    float max_lum_res = 0.0;

    //  88 Mhz is VHF FM modulation (NTSC-M and PAL-AB. NTSC-J uses 90 Mhz)
    //  Luma signal runs over 4.2Mhz (5.0 PAL), whereas Chroma does on 3.5795 (4.4336 PAL)
    if (S_PRESET == 0.0)
    {
        float blank =   (PAL==1.0)     ? 12.0                      : 10.9;
        float scan_ms = (PAL==1.0)     ? 1000000.*(1./625.)*(1./25.)-blank : \
                                         1000000.*(1./525.)*(1./(30./1.001))-blank;
        float Ch_SubC = (PAL==1.0)     ? 390.15845                 : 315.0;
        float Y_Carr =  (PAL==1.0)     ? 440.0                     : 369.6;
        float Y_CUS =   (Y_RES != 4.2) ? Y_RES * 88.0          : Y_Carr;

        max_col_res_I = (I_RES / 2.0) * scan_ms ms * Ch_SubC/88.0 MHz;
        max_col_res_Q = (Q_RES / 2.0) * scan_ms ms * Ch_SubC/88.0 MHz;
        max_lum_res = scan_ms ms * Y_CUS/88.0 MHz;
    }
    if (S_PRESET == 1.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_lum_res = 52.0 ms * 484.0/88.0 MHz;
    }
    if (S_PRESET == 2.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.0 ms * 390.15845/88.0 MHz;
        max_lum_res = 52.0 ms * 440.0/88.0 MHz;
    }
    if (S_PRESET == 3.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (0.40 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }
    if (S_PRESET == 4.0)
    {
        max_col_res_I = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (1.30 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }
    if (S_PRESET == 5.0)
    {
        max_col_res_I = (1.79 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_col_res_Q = (1.79 / 2.0) * 52.6 ms * 315.0/88.0 MHz;
        max_lum_res = 52.6 ms * 369.6/88.0 MHz;
    }

    int viewport_col_resy = int(ceil((OutputSize.x / TextureSize.x) * (TextureSize.x / max_col_res_I)));
    int viewport_col_resz = int(ceil((OutputSize.x / TextureSize.x) * (TextureSize.x / max_col_res_Q)));
    int viewport_lum_res =  int(ceil((OutputSize.x / TextureSize.x) * (TextureSize.x / max_lum_res)));

    if(vTexCoord.x - SPLIT - 1.0 > 0.0 || vTexCoord.x - SPLIT < 0.0)
    {
        FragColor = vec4(COMPAT_TEXTURE(Source, vTexCoord).rgb, 1.0);
    }
    else
    {
        vec4 col = vec4(0.0, 0.0, 0.0, 1.0);

        if ((S_PRESET == 0.0) || (S_PRESET == 3.0) || (CH_SPC == 0.0))
        {
            col += RGB_YIQ(COMPAT_TEXTURE(Source, vTexCoord));

            for(int i = 1; i < viewport_col_resy; i++)
            {
                col.y += RGB_YIQ(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resy/2) * OutSize.z, 0.0))).y;
            }
            for(int i = 1; i < viewport_col_resz; i++)
            {
                col.z += RGB_YIQ(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resz/2) * OutSize.z, 0.0))).z;
            }
            for(int i = 1; i < viewport_lum_res; i++)
            {
                col.x += RGB_YIQ(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resy/2) * OutSize.z, 0.0))).x;
            }
        }
        else
        {
            col += RGBtoYUV(COMPAT_TEXTURE(Source, vTexCoord));

            for(int i = 1; i < viewport_col_resy; i++)
            {
                col.y += RGBtoYUV(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resy/2) * OutSize.z, 0.0))).y;
            }
            for(int i = 1; i < viewport_col_resz; i++)
            {
                col.z += RGBtoYUV(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resz/2) * OutSize.z, 0.0))).z;
            }
            for(int i = 1; i < viewport_lum_res; i++)
            {
                col.x += RGBtoYUV(COMPAT_TEXTURE(Source, vTexCoord - vec2(float(i - viewport_col_resy/2) * OutSize.z, 0.0))).x;
            }
        }


        col.y /= float(viewport_col_resy);
        col.z /= float(viewport_col_resz);
        col.x /= float(viewport_lum_res);
        col = PCtoTV(col);


        col.y = mod((col.y + 1.0) + I_SHIFT, 2.0) - 1.0;
//      col.y = 0.9 * col.y + 0.1 * col.y * col.x;
//
        col.z = mod((col.z + 1.0) + Q_SHIFT, 2.0) - 1.0;
//      col.z = 0.4 * col.z + 0.6 * col.z * col.x;
//      col.x += 0.5*col.y;

        col.z *= Q_MUL;
        col.y *= I_MUL;
        col.x *= Y_MUL;

        if ((S_PRESET == 0.0) || (S_PRESET == 3.0) || (CH_SPC == 0.0))
        {
            col = clamp(col,vec4(0.0627,-0.5957,-0.5226,0.0),vec4(0.92157,0.5957,0.5226,1.0));
            col = YIQ_RGB(TVtoPC(col));
        }
        else
        {
            col = clamp(col,vec4(0.0627,0.0627-0.5,0.0627-0.5,0.0),vec4(0.92157,0.94118-0.5,0.94118-0.5,1.0));
            col = YUVtoRGB(TVtoPC(col));
        }

        FragColor = clamp(col,0.0,1.0);

    }
}
#endif
4 Likes

It was OutSize then, I still get confused with those. I removed the const in an attempt to fix it. Looks like I didn’t make too much of a mess haha

2 Likes

ah, yeah, that too. GLSL doesn’t have the built-in inversions on the z/w swizzles, so I made the SourceSize and OutSize (OutputSize was already taken by the built-in vec2* >_>) macros to hold them.

*note: some compilers will allow you to macro out the regular OutputSize to vec4(OutputSize.x, OutputSize.y, 1./OutputSize.x, 1./OutputSize.y), but others complain about it being a recursive macro, which is indeed bad, hence the crummy ‘OutSize’ substitution

3 Likes

@Dogway

If/when you get the time, would you mind posting some reference shots for the different signal types? From what I can see, the differences are very subtle, but I’m not sure I’m doing it right. :sweat_smile:

1 Like

Yes, it’s kinda subtle but I wanted the chroma-to-luma misalignment to be accurate depending on the region or the connection used. I found it very interesting the more I read about it as I learned how to link my experience with avisynth for digital media to the analogue media and audio. This is what chroma subsampling is to the analogue media and the luma carrier is the resolution I guess. The values are extracted from various sources, and a few of them I made them up like the max subcarrier and ntsc digital standard, not sure if those were digital “compliant”. I still need to find out the relation between this and the lowpass step to avoid a double blur.

I will post some images tomorrow along some studies on slot masks over 1080p!

3 Likes