From acaade10628ed3478ef9837868693050bdd31b24 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 26 Nov 2025 00:39:00 +0900 Subject: [PATCH] better CRT/Composite shader --- assets/disk0/home/jpdectest.js | 22 +- assets/disk0/home/jpdectesthigh.js | 22 +- .../src/net/torvald/tsvm/shader_crt_post.frag | 322 ++++++++++++++++++ tsvm_executable/src/net/torvald/tsvm/VMGUI.kt | 199 +---------- 4 files changed, 337 insertions(+), 228 deletions(-) create mode 100644 tsvm_core/src/net/torvald/tsvm/shader_crt_post.frag diff --git a/assets/disk0/home/jpdectest.js b/assets/disk0/home/jpdectest.js index 884691d..7c1940b 100644 --- a/assets/disk0/home/jpdectest.js +++ b/assets/disk0/home/jpdectest.js @@ -2,26 +2,18 @@ if (!exec_args[1]) { printerrln("Usage: jpdectest image.jpg") } -filesystem.open("A", exec_args[1], "R") +const fullFilePath = _G.shell.resolvePathInput(exec_args[1]) +const file = files.open(fullFilePath.full) +const fileLen = file.size +const infile = sys.malloc(file.size); file.pread(infile, fileLen, 0) -let status = com.getStatusCode(0) -let infile = undefined -if (0 != status) return status - - -let fileLen = filesystem.getFileLen("A") -println(`DMA reading ${fileLen} bytes from disk...`) -infile = sys.malloc(fileLen) -dma.comToRam(0, 0, infile, fileLen) - - -println("decoding") +//println("decoding") // decode const [imgw, imgh, channels, imageData] = graphics.decodeImageResample(infile, fileLen, -1, -1) -println(`dim: ${imgw}x${imgh}`) -println(`converting to displayable format...`) +//println(`dim: ${imgw}x${imgh}`) +//println(`converting to displayable format...`) // convert colour graphics.setGraphicsMode(0) diff --git a/assets/disk0/home/jpdectesthigh.js b/assets/disk0/home/jpdectesthigh.js index 9b45b17..9c49d7f 100644 --- a/assets/disk0/home/jpdectesthigh.js +++ b/assets/disk0/home/jpdectesthigh.js @@ -2,26 +2,18 @@ if (!exec_args[1]) { printerrln("Usage: jpdectesthigh image.jpg") } -filesystem.open("A", exec_args[1], "R") +const fullFilePath = _G.shell.resolvePathInput(exec_args[1]) +const file = files.open(fullFilePath.full) +const fileLen = file.size +const infile = sys.malloc(file.size); file.pread(infile, fileLen, 0) -let status = com.getStatusCode(0) -let infile = undefined -if (0 != status) return status - - -let fileLen = filesystem.getFileLen("A") -println(`DMA reading ${fileLen} bytes from disk...`) -infile = sys.malloc(fileLen) -dma.comToRam(0, 0, infile, fileLen) - - -println("decoding") +//println("decoding") // decode const [imgw, imgh, channels, imageData] = graphics.decodeImageResample(infile, fileLen, -1, -1) -println(`dim: ${imgw}x${imgh}`) -println(`converting to displayable format...`) +//println(`dim: ${imgw}x${imgh}`) +//println(`converting to displayable format...`) // convert colour graphics.setGraphicsMode(4) diff --git a/tsvm_core/src/net/torvald/tsvm/shader_crt_post.frag b/tsvm_core/src/net/torvald/tsvm/shader_crt_post.frag new file mode 100644 index 0000000..5cba0bb --- /dev/null +++ b/tsvm_core/src/net/torvald/tsvm/shader_crt_post.frag @@ -0,0 +1,322 @@ + + +// ============================================================================ +// CRT + NTSC Composite/S-Video Signal Simulation Shader (Enhanced Version) +// ============================================================================ +// Features: +// - Runtime-switchable composite/S-Video mode (no recompilation) +// - Adjustable signal and CRT parameters via uniforms +// - Accurate NTSC color artifact simulation +// - Animated dot crawl effect +// - Trinitron phosphor mask +// - Optional bloom/glow effect +// ============================================================================ + +// === UNIFORMS === +uniform float time = 0.0; // Frame count +uniform vec2 resolution = vec2(640.0, 480.0); // Virtual resolution (e.g., 640x480) +uniform sampler2D u_texture; // Input texture +uniform vec2 flip = vec2(0.0, 0.0); // UV flip control (0,1 = flip Y) + +// Signal mode: 0 = S-Video, 1 = Composite, 2 = CGA Composite +// Can be changed at runtime without recompilation +uniform int signalMode = 1; // Default should be 1 for composite + +// CGA-specific settings +uniform float cgaHue; // Hue adjustment for CGA (default: 0.0, range: -PI to PI) +uniform float cgaSaturation; // Saturation multiplier for CGA (default: 1.0) + +// Optional adjustable parameters (set reasonable defaults if not provided) +uniform float lumaFilterWidth; // Default: 1.5 +uniform float chromaIFilterWidth; // Default: 3.5 +uniform float chromaQFilterWidth; // Default: 6.0 +uniform float compositeFilterWidth; // Default: 1.5 +uniform float phosphorIntensity; // Default: 0.25 +uniform float scanlineIntensity; // Default: 0.12 + +in vec2 v_texCoords; +out vec4 fragColor; + +// === CONSTANTS === +const float PI = 3.14159265358979323846; +const float TAU = 6.28318530717958647692; + +// NTSC color subcarrier: 3.579545 MHz +// At 640 pixels for ~52.6µs active video: cycles/pixel ≈ 0.2917 +const float CC_PER_PIXEL = 0.2917; + +// CGA specific: 14.318 MHz pixel clock = exactly 4× color subcarrier +// This means exactly 4 pixels per color cycle = 0.25 cycles per pixel +const float CGA_CC_PER_PIXEL = 0.25; + +// Filter kernel radius (samples to each side) +const int FILTER_RADIUS = 12; + +// === COLOR SPACE CONVERSION === +// GLSL matrices are column-major +const mat3 RGB_TO_YIQ = mat3( +0.299, 0.596, 0.211, // Column 0: R coefficients for Y,I,Q +0.587, -0.274, -0.523, // Column 1: G coefficients +0.114, -0.322, 0.312 // Column 2: B coefficients +); + +const mat3 YIQ_TO_RGB = mat3( +1.000, 1.000, 1.000, // Column 0: Y coefficients for R,G,B +0.956, -0.272, -1.107, // Column 1: I coefficients +0.621, -0.647, 1.704 // Column 2: Q coefficients +); + +// === DEFAULT VALUES === +// Used when uniforms aren't set (value of 0) +float getLumaFilter() { + return lumaFilterWidth > 0.0 ? lumaFilterWidth : 1.15; +} +float getChromaIFilter() { + return chromaIFilterWidth > 0.0 ? chromaIFilterWidth : 3.5; +} +float getChromaQFilter() { + return chromaQFilterWidth > 0.0 ? chromaQFilterWidth : 6.0; +} +float getCompositeFilter() { + return compositeFilterWidth > 0.0 ? compositeFilterWidth : 1.35; +} +float getPhosphorStrength() { + return phosphorIntensity > 0.0 ? phosphorIntensity : 0.25; +} +float getScanlineStrength() { + return scanlineIntensity > 0.0 ? scanlineIntensity : 0.12; +} +float getCgaSaturation() { + return cgaSaturation > 0.0 ? cgaSaturation : 1.0; +} + +// === HELPER FUNCTIONS === + +float gaussianWeight(float x, float sigma) { + return exp(-0.5 * x * x / (sigma * sigma)); +} + +vec3 sampleTexture(vec2 uv) { + return texture(u_texture, clamp(uv, 0.0, 1.0)).rgb; +} + +float calcCarrierPhase(float pixelX, float pixelY, float frameOffset) { + float phase = pixelX * TAU * CC_PER_PIXEL; + phase += pixelY * PI; // 180° per line (from 227.5 cycles/line) + phase += frameOffset; + return phase; +} + +float encodeComposite(vec3 rgb, float phase) { + vec3 yiq = RGB_TO_YIQ * rgb; + return yiq.x + yiq.y * cos(phase) + yiq.z * sin(phase); +} + +// === COMPOSITE SIGNAL DECODE === +vec3 decodeComposite(vec2 uv, vec2 texelSize, float basePhase) { + float compFilter = getCompositeFilter(); + float iFilter = getChromaIFilter(); + float qFilter = getChromaQFilter(); + + float yAccum = 0.0, iAccum = 0.0, qAccum = 0.0; + float yWeight = 0.0, iWeight = 0.0, qWeight = 0.0; + + for (int i = -FILTER_RADIUS; i <= FILTER_RADIUS; i++) { + float offset = float(i); + vec2 sampleUV = uv + vec2(offset * texelSize.x, 0.0); + + vec3 srcRGB = sampleTexture(sampleUV); + float samplePhase = basePhase + offset * TAU * CC_PER_PIXEL; + float composite = encodeComposite(srcRGB, samplePhase); + + // Low-pass for luma + float yw = gaussianWeight(offset, compFilter); + yAccum += composite * yw; + yWeight += yw; + + // Demodulate and filter chroma + float iw = gaussianWeight(offset, iFilter); + float qw = gaussianWeight(offset, qFilter); + + iAccum += composite * cos(samplePhase) * 2.0 * iw; + qAccum += composite * sin(samplePhase) * 2.0 * qw; + + iWeight += iw; + qWeight += qw; + } + + vec3 yiq = vec3(yAccum / yWeight, iAccum / iWeight, qAccum / qWeight); + return YIQ_TO_RGB * yiq; +} + +// === S-VIDEO SIGNAL DECODE === +vec3 decodeSVideo(vec2 uv, vec2 texelSize, float basePhase) { + float yFilter = getLumaFilter(); + float iFilter = getChromaIFilter(); + float qFilter = getChromaQFilter(); + + float yAccum = 0.0, iAccum = 0.0, qAccum = 0.0; + float yWeight = 0.0, iWeight = 0.0, qWeight = 0.0; + + for (int i = -FILTER_RADIUS; i <= FILTER_RADIUS; i++) { + float offset = float(i); + vec2 sampleUV = uv + vec2(offset * texelSize.x, 0.0); + + vec3 srcRGB = sampleTexture(sampleUV); + vec3 yiq = RGB_TO_YIQ * srcRGB; + + float samplePhase = basePhase + offset * TAU * CC_PER_PIXEL; + float chromaSignal = yiq.y * cos(samplePhase) + yiq.z * sin(samplePhase); + + // Luma is separate - no cross-color + float yw = gaussianWeight(offset, yFilter); + yAccum += yiq.x * yw; + yWeight += yw; + + // Chroma demodulation + float iw = gaussianWeight(offset, iFilter); + float qw = gaussianWeight(offset, qFilter); + + iAccum += chromaSignal * cos(samplePhase) * 2.0 * iw; + qAccum += chromaSignal * sin(samplePhase) * 2.0 * qw; + + iWeight += iw; + qWeight += qw; + } + + vec3 yiqOut = vec3(yAccum / yWeight, iAccum / iWeight, qAccum / qWeight); + return YIQ_TO_RGB * yiqOut; +} + +// === CGA COMPOSITE DECODE === +// CGA has exactly 4 pixels per color cycle (14.318 MHz / 3.579545 MHz = 4) +// This creates the famous artifact colors from specific bit patterns +vec3 decodeCGAComposite(vec2 uv, vec2 texelSize, float pixelX, float pixelY) { + // CGA-specific filter widths - slightly different from generic NTSC + // CGA monitors typically had less filtering, making artifacts more pronounced + float yFilter = 1.2; + float chromaFilter = 2.5; + + // CGA color burst phase - this determines the base hue + // Adjusted to match the canonical CGA artifact color palette + float cgaPhaseOffset = cgaHue + PI * 0.5; // Adjust for correct color alignment + + // CGA doesn't have the 227.5 cycle per line offset in the same way + // The phase is more deterministic based on pixel position + float basePhase = pixelX * TAU * CGA_CC_PER_PIXEL + cgaPhaseOffset; + + // Odd lines have 180° phase shift (creates the alternating pattern) + if (mod(pixelY, 2.0) >= 1.0) { + basePhase += PI; + } + + float yAccum = 0.0, iAccum = 0.0, qAccum = 0.0; + float yWeight = 0.0, chromaWeight = 0.0; + + // Use smaller filter radius for sharper CGA look + const int CGA_RADIUS = 8; + + for (int i = -CGA_RADIUS; i <= CGA_RADIUS; i++) { + float offset = float(i); + vec2 sampleUV = uv + vec2(offset * texelSize.x, 0.0); + + // CGA outputs either black (0) or white (1) in 640x200 mode + // Get the source value (treating as monochrome for artifact generation) + vec3 srcRGB = sampleTexture(sampleUV); + float srcLuma = dot(srcRGB, vec3(0.299, 0.587, 0.114)); + + // For CGA artifact colors, we use the luma as the composite signal level + // In reality, CGA outputs either 0V or ~0.7V for the two states + float composite = srcLuma; + + float samplePhase = basePhase + offset * TAU * CGA_CC_PER_PIXEL; + + // Low-pass filter for luma + float yw = gaussianWeight(offset, yFilter); + yAccum += composite * yw; + yWeight += yw; + + // Demodulate chroma + float cw = gaussianWeight(offset, chromaFilter); + iAccum += composite * cos(samplePhase) * 2.0 * cw; + qAccum += composite * sin(samplePhase) * 2.0 * cw; + chromaWeight += cw; + } + + float y = yAccum / yWeight; + float i = (iAccum / chromaWeight) * getCgaSaturation(); + float q = (qAccum / chromaWeight) * getCgaSaturation(); + + // Convert to RGB + vec3 rgb = YIQ_TO_RGB * vec3(y, i, q); + + return rgb; +} + +// === TRINITRON PHOSPHOR MASK === +vec3 trinitronMask(vec2 screenPos) { + float strength = getPhosphorStrength(); + float outputX = screenPos.x * 2.0; // 2x display scale + float stripe = mod(outputX, 3.0); + + float bleed = 0.15; + vec3 mask; + + if (stripe < 1.0) { + mask = vec3(1.0, bleed, bleed); + } else if (stripe < 2.0) { + mask = vec3(bleed, 1.0, bleed); + } else { + mask = vec3(bleed, bleed, 1.0); + } + + float compensation = 1.0 / (0.333 + 0.667 * bleed); + mask *= compensation * 0.85; + + return mix(vec3(1.0), mask, strength); +} + +// === SCANLINE MASK === +float scanlineMask(vec2 screenPos) { + float strength = getScanlineStrength(); + float outputY = screenPos.y * 2.0; // 2x display scale + + float scanline = sin(outputY * PI); + scanline = scanline * 0.5 + 0.5; + scanline = pow(scanline, 0.4); + + return mix(1.0 - strength, 1.0, scanline); +} + +// === MAIN === +void main() { + vec2 uv = v_texCoords; + uv.x = mix(uv.x, 1.0 - uv.x, flip.x); + uv.y = mix(uv.y, 1.0 - uv.y, flip.y); + + vec2 texelSize = 1.0 / resolution; + float pixelX = uv.x * resolution.x; + float pixelY = uv.y * resolution.y; + + // Frame phase for dot crawl (4-frame cycle) + float framePhase = mod(time, 4.0) * PI * 0.5; + float basePhase = calcCarrierPhase(pixelX, pixelY, framePhase); + + // Decode signal based on mode + vec3 rgb; + if (signalMode == 2) { + // CGA Composite mode - deterministic artifact colors + rgb = decodeCGAComposite(uv, texelSize, pixelX, pixelY); + } else if (signalMode == 1) { + rgb = decodeComposite(uv, texelSize, basePhase); + } else { + rgb = decodeSVideo(uv, texelSize, basePhase); + } + + // CRT display effects + vec2 screenPos = vec2(pixelX, pixelY); +// rgb *= trinitronMask(screenPos); +// rgb *= scanlineMask(screenPos); + + fragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0); +} diff --git a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt index 6e9ca76..32c46c1 100644 --- a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt +++ b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt @@ -97,7 +97,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe camera.update() batch.projectionMatrix = camera.combined - crtShader = loadShaderInline(CRT_POST_SHADER2) + crtShader = loadShaderInline(Gdx.files.classpath("net/torvald/tsvm/shader_crt_post.frag").readString()) gpuFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false) winFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false) @@ -556,200 +556,3 @@ void main() { } """ - -const val CRT_POST_SHADER2 = """ -#ifdef GL_ES -precision mediump float; -#endif - -in vec4 v_color; -in vec4 v_generic; -in vec2 v_texCoords; -uniform sampler2D u_texture; -uniform vec2 resolution = vec2(640.0, 480.0); -out vec4 fragColor; - -uniform float time = 0.0; - -const int SUBS = 6; // horizontal subsamples per pixel (oversampling) -const float PI = 3.14159265359; - -// --- RGB <-> YIQ (NTSC-ish) --- -vec3 rgb2yiq(vec3 rgb) { - float y = dot(rgb, vec3(0.299, 0.587, 0.114)); - float i = dot(rgb, vec3(0.596, -0.274, -0.322)); - float q = dot(rgb, vec3(0.211, -0.523, 0.312)); - return vec3(y, i, q); -} - -vec3 yiq2rgb(vec3 yiq) { - float y = yiq.x, i = yiq.y, q = yiq.z; - vec3 r = vec3( - y + 0.956*i + 0.621*q, - y - 0.272*i - 0.647*q, - y - 1.106*i + 1.703*q - ); - return clamp(r, 0.0, 1.0); -} - -// --- Parameters you can tweak --- -float subcarrierCyclesPerScanline = 227.5; // NTSC approx cycles per scanline (spatial) -float chromaGain = 2.2; // strength of chroma modulation in encode -float chromaLPF_radius = 3.6; // lowpass radius in pixels for chroma (larger = more bleed) -float lumaLPF_radius = 0.7; // lowpass for luma (small = sharp) -float chromaPhaseDrift = 1.1; // extra phase offset (use time to animate dot crawl) -float subsampleSpanPixels = 2.0 / 3.0; // how wide the subsample footprint is (in pixels) - -// Simple 1D gaussian weight (not normalized here) -float gaussWeight(float x, float r) { - return exp(- (x*x) / (2.0 * r * r)); -} - -// Sample a tiny neighborhood and get averaged Y/I/Q and also do composite modulation -// We oversample horizontally to simulate the analog sampling along the scanline. -void compositeAtUV(in vec2 uv, out float avgComposite, out float avgY, out float avgDemodI, out float avgDemodQ) { - avgComposite = 0.0; - avgY = 0.0; - avgDemodI = 0.0; - avgDemodQ = 0.0; - - // pixel coordinates - float px = uv.x * resolution.x; - float py = uv.y * resolution.y; - // subcarrier spatial frequency (cycles per pixel horizontally) - // cycles per scanline / pixels per scanline = cycles per pixel (approx) - float cycles_per_pixel = subcarrierCyclesPerScanline / resolution.x; - // time-varying phase for dot-crawl - float linePhase = (py * subcarrierCyclesPerScanline) * 2.0 * PI + chromaPhaseDrift * time; - - // do SUBS evenly-spaced subsamples across this pixel horizontally - float totalWeight = 0.0; - for (int s = 0; s < SUBS; ++s) { - float t = (float(s) + 0.5) / float(SUBS) - 0.5; // -0.5 .. +0.5 - float sx = px + t * subsampleSpanPixels; // sample x in pixel space - vec2 sUV = vec2(sx / resolution.x, uv.y); - - vec3 col = texture2D(u_texture, sUV).rgb; - vec3 yiq = rgb2yiq(col); - float Y = yiq.x; - float I = yiq.y; - float Q = yiq.z; - - // subcarrier phase at this horizontal sample - float phi = linePhase + sx * cycles_per_pixel * 2.0 * PI; - - // composite waveform value (analog mix) - float carrierCos = cos(phi); - float carrierSin = sin(phi); - float composite = Y + chromaGain * (I * carrierCos + Q * carrierSin); - - // demodulation multipliers (we will low-pass by averaging) - float demodI = composite * carrierCos; - float demodQ = composite * carrierSin; - - // weight: use gaussian centered on pixel center - float w = gaussWeight(t * subsampleSpanPixels, 0.6); // narrower weighting - totalWeight += w; - - avgComposite += composite * w; - avgY += Y * w; - avgDemodI += demodI * w; - avgDemodQ += demodQ * w; - } - - // normalize the local averages (this approximates a basic low-pass) - avgComposite /= totalWeight; - avgY /= totalWeight; - avgDemodI /= totalWeight; - avgDemodQ /= totalWeight; - - // --- additional spatial low-pass filtering to mimic channel bandwidth --- - // We'll sample neighbours on x and average with Gaussian weights to emulate chroma/luma LPF - // Note: we do a tiny neighborhood (±2 pixels) to save performance. - float wsumC = 1.0; - float wsumY = 1.0; - float csum = avgComposite; - float idsum = avgDemodI; - float qdsum = avgDemodQ; - float ysum = avgY; - - // radius in pixels for chroma or luma affects weights below; we'll use same kernel but scale contributions - int taps = 2; - for (int i = -taps; i <= taps; ++i) { - if (i == 0) continue; - float off = float(i); - // use chroma radius for chroma-related weights, luma radius for luma - float weightC = gaussWeight(off, chromaLPF_radius); - float weightY = gaussWeight(off, lumaLPF_radius); - - vec2 sampleUV = vec2((px + off) / resolution.x, uv.y); - vec3 ncol = texture2D(u_texture, sampleUV).rgb; - vec3 nyiq = rgb2yiq(ncol); - float nY = nyiq.x; - float nI = nyiq.y; - float nQ = nyiq.z; - - float nPhi = linePhase + (px + off) * cycles_per_pixel * 2.0 * PI; - float nComposite = nY + chromaGain * (nI * cos(nPhi) + nQ * sin(nPhi)); - float ndemodI = nComposite * cos(nPhi); - float ndemodQ = nComposite * sin(nPhi); - - csum += nComposite * weightC; - idsum += ndemodI * weightC; - qdsum += ndemodQ * weightC; - wsumC += weightC; - - ysum += nY * weightY; - wsumY += weightY; - } - - avgComposite = csum / wsumC; - avgY = ysum / wsumY; - avgDemodI = idsum / wsumC; - avgDemodQ = qdsum / wsumC; -} - -// main -void mainImage(out vec4 frag_Color, in vec2 frag_Coord) { - vec2 uv = frag_Coord.xy / resolution.xy; - uv.y = 1.0 - uv.y; - - // Get composite and demod values centered at uv - float compositeVal, Yval, demodI, demodQ; - compositeAtUV(uv, compositeVal, Yval, demodI, demodQ); - - // Low-pass the demodulators to extract I & Q (simple normalization) - // In a real demodulator you'd low-pass filter demod*carrier; here we've already averaged spatially, - // so just apply a gain normalization and small smoothing. - // Normalize I/Q by average carrier power (~0.5) - float carrierPower = 0.5; - float I_rec = demodI / max(carrierPower, 1e-5); - float Q_rec = demodQ / max(carrierPower, 1e-5); - - // Optionally reduce chroma bandwidth / smear by blurring (simulate narrow chroma filter) - // We'll lerp the recovered chroma towards zero based on chromaLPF_radius - float chromaBlurFactor = smoothstep(0.0, 5.0, chromaLPF_radius); // 0..1 - I_rec *= 1.0 - 0.65 * chromaBlurFactor; - Q_rec *= 1.0 - 0.65 * chromaBlurFactor; - - // For luma, we can optionally low-pass Yval slightly to simulate limited luma bandwidth - // but keep it mostly sharp - float Y_lp = Yval; // already lightly filtered above - // final reconstructed yiq - vec3 yiqRec = vec3(Y_lp, I_rec, Q_rec); - vec3 rgbRec = yiq2rgb(yiqRec); - - // Optional: blend with original luminance to keep text legible (tweakable) - // vec3 orig = texture2D(u_texture, uv).rgb; - // rgbRec = mix(rgbRec, orig, 0.25); - - frag_Color = vec4(rgbRec, 1.0); -} - -void main() { - vec2 fragCoord = gl_FragCoord.xy; - vec4 outcol; - mainImage(outcol, fragCoord); - fragColor = outcol; -} -""" \ No newline at end of file