/* ====================== Based on cgwg's CRT shader Copyright (C) 2010 cgwg http://filthypants.blogspot.com/2010/12/crt-pixel-shader-filter-for-snes.html ====================== */ sampler2D rubyTexture; bool useInput; bool useOutput; float inputW; float inputH; float outputW; float outputH; float fPixelWidth, fPixelHeight; float2 rubyInputSize = 128; float2 rubyOutputSize = 128; float2 rubyTextureSize = 128; // Abbreviations //#define TEX2D(c) texture2D(rubyTexture, (c)) #define TEX2D(c) pow(tex2D(rubyTexture, (c)), inputGamma) #define FIX(c) max(abs(c), 1e-6); #define PI 3.141592653589 // Adjusts the vertical position of scanlines. Useful if the output // pixel size is large compared to the scanline width (so, scale // factors less than 4x or so). Ranges from 0.0 to 1.0. float phase = 0.0; // Assume NTSC 2.2 Gamma for linear blending float inputGamma = 2.2; // Simulate a CRT gamma of 2.5 float outputGamma = 2.5; // Controls the intensity of the barrel distortion used to emulate the // curvature of a CRT. 0.0 is perfectly flat, 1.0 is annoyingly // distorted, higher values are increasingly ridiculous. float distortion = 0.1; // Apply radial distortion to the given coordinate. float2 radialDistortion(float2 coord) { coord *= rubyTextureSize / rubyInputSize; float2 cc = coord - 0.5; float dist = dot(cc, cc) * distortion; return (coord + cc * (1.0 + dist) * dist) * rubyInputSize / rubyTextureSize; } // Calculate the influence of a scanline on the current pixel. // // 'distance' is the distance in texture coordinates from the current // pixel to the scanline in question. // 'color' is the colour of the scanline at the horizontal location of // the current pixel. float4 scanlineWeights(float dist, float4 color) { // The "width" of the scanline beam is set as 2*(1 + x^4) for // each RGB channel. float4 wid = 2.0 + 2.0 * pow(color, 4.0); // The "weights" lines basically specify the formula that gives // you the profile of the beam, i.e. the intensity as // a function of distance from the vertical center of the // scanline. In this case, it is gaussian if width=2, and // becomes nongaussian for larger widths. Ideally this should // be normalized so that the integral across the beam is // independent of its width. That is, for a narrower beam // "weights" should have a higher peak at the center of the // scanline than for a wider beam. float4 weights = dist * 3.333333; return 0.51 * exp(-pow(weights * sqrt(2.0 / wid), wid)) / (0.18 + 0.06 * wid); } float4 ps_main( float2 texCoord : TEXCOORD0 ) : COLOR { // Here's a helpful diagram to keep in mind while trying to // understand the code: // // | | | | | // ------------------------------- // | | | | | // | 01 | 11 | 21 | 31 | <-- current scanline // | | @ | | | // ------------------------------- // | | | | | // | 02 | 12 | 22 | 32 | <-- next scanline // | | | | | // ------------------------------- // | | | | | // // Each character-cell represents a pixel on the output // surface, "@" represents the current pixel (always somewhere // in the bottom half of the current scan-line, or the top-half // of the next scanline). The grid of lines represents the // edges of the texels of the underlying texture. rubyTextureSize = 1.0 / float2(fPixelWidth,fPixelHeight); rubyInputSize = rubyTextureSize * float2(inputW,inputH); rubyOutputSize = rubyTextureSize * float2(outputW,outputH); // The size of one texel, in texture-coordinates. float2 one = 1.0 / rubyTextureSize; // Texture coordinates of the texel containing the active pixel float2 xy = radialDistortion(texCoord); // Of all the pixels that are mapped onto the texel we are // currently rendering, which pixel are we currently rendering? float2 uv_ratio = frac(xy * rubyTextureSize) - 0.5; // Snap to the center of the underlying texel. xy = (floor(xy * rubyTextureSize) + 0.5) / rubyTextureSize; // Calculate Lanczos scaling coefficients describing the effect // of various neighbour texels in a scanline on the current // pixel. float4 coeffs = PI * float4(1.0 + uv_ratio.x, uv_ratio.x, 1.0 - uv_ratio.x, 2.0 - uv_ratio.x); // Prevent division by zero coeffs = FIX(coeffs); coeffs = 2.0 * sin(coeffs) * sin(coeffs / 2.0) / (coeffs * coeffs); // Normalize coeffs /= dot(coeffs, 1.0); // Calculate the effective colour of the current and next // scanlines at the horizontal location of the current pixel, // using the Lanczos coefficients above. float4 col = clamp(coeffs.x * TEX2D(xy + float2(-one.x, 0.0)) + coeffs.y * TEX2D(xy) + coeffs.z * TEX2D(xy + float2(one.x, 0.0)) + coeffs.w * TEX2D(xy + float2(2.0 * one.x, 0.0)), 0.0, 1.0); float4 col2 = clamp(coeffs.x * TEX2D(xy + float2(-one.x, one.y)) + coeffs.y * TEX2D(xy + float2(0.0, one.y)) + coeffs.z * TEX2D(xy + one) + coeffs.w * TEX2D(xy + float2(2.0 * one.x, one.y)), 0.0, 1.0); // col = pow(col, float4(inputGamma)); // col2 = pow(col2, float4(inputGamma)); // Calculate the influence of the current and next scanlines on // the current pixel. float4 weights = scanlineWeights(abs(uv_ratio.y) , col); float4 weights2 = scanlineWeights(1.0 - uv_ratio.y, col2); float3 mul_res = (col * weights + col2 * weights2).xyz; // mod_factor is the x-coordinate of the current output pixel. float mod_factor = texCoord.x * rubyOutputSize.x * rubyTextureSize.x / rubyInputSize.x; // dot-mask emulation: // Output pixels are alternately tinted green and magenta. float3 dotMaskWeights = lerp( float3(1.05, 0.75, 1.05), float3(0.75, 1.05, 0.75), floor(fmod(mod_factor, 2.0)) ); mul_res *= dotMaskWeights; // Convert the image gamma for display on our output device. mul_res = pow(mul_res, 1.0 / (2.0 * inputGamma - outputGamma)); return float4(mul_res, 1.0); } technique tech_main { pass P0 { PixelShader = compile ps_2_a ps_main(); } }