Rough translated on AI and here it’s what is doing and result
// Input: either a texture with RGB or you can feed color another way
#define uPixelClock 13.5e6 // pixel clock in Hz (e.g. 13.5e6 typical for SD sampling)
#define uSubcarrierHz 3.57e6 // chroma subcarrier freq (NTSC = 3579545.0, PAL = 4433618.0)
#define uModePAL 0.0 // 0 = NTSC, 1 = PAL (pal alternation enabled)
#define uChromaGain 1.0 // scale factor for chroma carrier amplitude
#define uDeltaRdeg 1.0 // ΔR in degrees (datasheet typical ~ +1°)
#define uDeltaBdeg 4.0 // ΔB in degrees (datasheet typical ~ +4°)
#define uBurstPhaseDeg 180.0 // burst phase relative to R-Y (deg). Datasheet implies ~180°
#define uPixelsPerLine SourceSize.x // visible pixels per line (e.g. 720)
// coefficients: Rec.601-like luminance for simple RGB->Y
const vec3 Ycoeff = vec3(0.299, 0.587, 0.114);
// Helper
float deg2rad(float d) { return d * 3.14159265358979323846 / 180.0; }
void main() {
// Read the RGB input
vec3 rgb = texture(Source, vTexCoord).rgb;
// Compute luminance and color-difference signals
float Y = dot(rgb, Ycoeff); // 0..1
float RY = rgb.r - Y; // R - Y
float BY = rgb.b - Y; // B - Y
// Convert ΔR/ΔB and burst phase to radians
float deltaR = deg2rad(uDeltaRdeg);
float deltaB = deg2rad(uDeltaBdeg);
float burstPhase = deg2rad(uBurstPhaseDeg);
// Calculate pixel time index t_pixel
// We compute an approximate continuous time value per pixel:
// t_pixel = (lineIndex * pixelsPerLine + pixelIndex) / pixelClock
// For simplicity we use pixel coordinates to derive these values.
float pixelIndex = floor(vTexCoord.y*SourceSize.y)*SourceSize.x + vTexCoord.x*SourceSize.x;
//float t_pixel = pixelIndex / uPixelClock;
// Subcarrier phase
float phase = 2.0 * 3.14159265358979323846 * pixelIndex * uSubcarrierHz / uPixelClock;
// PAL alternation: flip one carrier axis 180° every line
if (uModePAL == 1) {
// flip sign every other line (equivalent to add PI)
if (mod(floor(vTexCoord.y*SourceSize.y), 2.0) > 0.5) {
phase += 3.14159265358979323846; // add pi on odd lines
}
}
// Apply small datasheet phase errors (ΔR, ΔB). The datasheet defines ΔR = eR - 90°,
// but here we allow direct tuning offsets (additive).
float phaseR = phase + deltaR; // used with sin()
float phaseB = phase + deltaB; // used with cos()
// Build modulated chroma (carrier)
// We use sin for R-Y and cos for B-Y to represent quadrature (90° separation)
float chroma = (RY * sin(phaseR) + BY * cos(phaseB)) * uChromaGain;
// Compose outputs:
// YO: luminance + sync (we don't generate sync here; if needed add negative pulses)
// CO: chroma (modulated) - real hardware filters & levels would differ; normalize
// VO: composite = Y + chroma (clip to 0..1)
float composite = clamp(Y + chroma, 0.0, 1.0);
// Optionally: overlay the burst reference (for debugging), gate it during burst region
// (Not implemented: you'd need horizontal timing + burst window timings.)
// Output as grayscale visualization of composite on all channels
FragColor = vec4(vec3(composite), 1.0);
}
Had to correct some dumb errors of AI like using gl_FragCoord for pixel index but otherwise pretty amazing job in translating to a shader,