Last active
April 13, 2018 10:55
-
-
Save realmonster/c95aaae4d170535d1aa1d7c508908be0 to your computer and use it in GitHub Desktop.
NES NTSC composite signal simulation for RetroArch, algo by Bisqwit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // NES NTSC composite signal CRT simulation for RetroArch | |
| // implementation of algorithm by Bisqwit | |
| // shader by r57shell | |
| // thanks to feos & HardWareMan | |
| // also TV subpixels and scanlines | |
| void main_vertex | |
| ( | |
| float4 position : POSITION, | |
| out float4 oPosition : POSITION, | |
| uniform float4x4 modelViewProj, | |
| float2 tex : TEXCOORD, | |
| out float2 oTex : TEXCOORD | |
| ) | |
| { | |
| oPosition = mul(modelViewProj, position); | |
| oTex = tex; | |
| } | |
| struct input | |
| { | |
| float2 video_size; | |
| float2 texture_size; | |
| float2 output_size; | |
| float frame_count; | |
| float frame_direction; | |
| float frame_rotation; | |
| }; | |
| // TWEAKS start | |
| // phase shift from frame to frame as NES does. | |
| #define ANIMATE_PHASE | |
| // this is not how actual NES works | |
| // it does not alter fields. | |
| //#define ANIMATE_SCANLINE | |
| // rough simulation of scanlines | |
| //#define USE_SCANLINES | |
| // simulate CRT TV subpixels | |
| //#define USE_SUBPIXELS | |
| // to change gamma of virtual TV from 2.2 to something else | |
| //#define USE_GAMMA | |
| // use core size. for NES use this, for other cores turn off | |
| // for other cores use "size" tweak. | |
| //#define USE_CORE_SIZE | |
| // use raw palette, turn it on if you | |
| // have nestopia and having using raw palette | |
| //#define USE_RAW | |
| // compensate filter width | |
| #define COMPENSATE_WIDTH | |
| // use sampled version. it's much more slower version of shader. | |
| // because it is computing x4 more values | |
| //#define USE_SAMPLED | |
| //default NTSC gamma = 2.2 | |
| const float gamma = 2.0; // gamma of virtual TV | |
| const int Ywidth = 12, Iwidth = 23, Qwidth = 23; | |
| const int Mwidth = 23; | |
| const int Contrast = 167941, Saturation = 144044; | |
| const float pi = 3.1415926535897932384626433832795; | |
| const float phase_y = 4.; | |
| const float phase_one = 0.; // alternating phases. | |
| const float phase_two = 8.; | |
| const int tv_pixels = 400; | |
| #ifdef USE_CORE_SIZE | |
| // just use core output size. | |
| #define size (IN.video_size.xy) | |
| #else | |
| // screen size, scanlines = y*2; y one field, and y other field. | |
| vec2 size = vec2(256,240); | |
| #endif | |
| const float dark_scanline = 0.5; // half | |
| // TWEAKS end | |
| bool InColorPhase(int color, int phase) | |
| { | |
| return mod((color + phase),12) < 6; | |
| } | |
| // from nesdev wiki page NTSC_video | |
| float NTSCsignal(vec3 pixel, int phase) | |
| { | |
| // Voltage levels, relative to synch voltage | |
| static const float black=.518f, white=1.962f, attenuation=.746f, | |
| levels[8] = {.350f, .518f, .962f,1.550f, // Signal low | |
| 1.094f,1.506f,1.962f,1.962f}; // Signal high | |
| // Decode the NES color. | |
| int color = int(pixel.r*15); // 0..15 "cccc" | |
| int level = int(pixel.g*3); // 0..3 "ll" | |
| int emphasis = int(pixel.b*7+0.1); // 0..7 "eee" | |
| if (color > 13) { level = 1; } // For colors 14..15, level 1 is forced. | |
| // The square wave for this color alternates between these two voltages: | |
| float low = levels[0]; | |
| float high = levels[4]; | |
| if (level == 1) | |
| { | |
| low = levels[1]; | |
| high = levels[5]; | |
| } | |
| if (level == 2) | |
| { | |
| low = levels[2]; | |
| high = levels[6]; | |
| } | |
| if (level == 3) | |
| { | |
| low = levels[3]; | |
| high = levels[7]; | |
| } | |
| if(color == 0) { low = high; } // For color 0, only high level is emitted | |
| if(color > 12) { high = low; } // For colors 13..15, only low level is emitted | |
| // Generate the square wave | |
| float signal = InColorPhase(color,phase) ? high : low; | |
| // When de-emphasis bits are set, some parts of the signal are attenuated: | |
| int e1 = mod(emphasis, 2); | |
| int e2 = mod(emphasis-e1, 4); | |
| int e3 = emphasis-e1-e2; | |
| if( ((e1 == 1) && InColorPhase(0,phase)) | |
| || ((e2 == 2) && InColorPhase(4,phase)) | |
| || ((e3 == 4) && InColorPhase(8,phase)) ) signal = signal * attenuation; | |
| return signal; | |
| } | |
| float sinn(float x) | |
| { | |
| return 8*sin(mod(x,12)*(pi*2./12.)); | |
| } | |
| float coss(float x) | |
| { | |
| return 8*cos(mod(x,12)*(pi*2./12)); | |
| } | |
| vec3 monitor(uniform sampler2D tex : TEXUNIT0, vec2 p, uniform input IN) | |
| { | |
| vec2 size2 = IN.texture_size; | |
| #ifdef COMPENSATE_WIDTH | |
| p.x += p.x*(Ywidth/8.)/size.x; | |
| #endif | |
| // align vertical coord to center of texel | |
| p.y = (floor((p.y*IN.texture_size.y))+0.5)/IN.texture_size.y; | |
| vec2 uv = p; | |
| float alpha = 4.10001+floor(p.x*IN.texture_size.x/IN.video_size.x*size.x*8 | |
| +floor(p.y*IN.texture_size.y/IN.video_size.y*size.y)*phase_y); | |
| #ifdef ANIMATE_PHASE | |
| if (mod(IN.frame_count,2) < 1.) | |
| alpha += phase_one; | |
| else | |
| alpha += phase_two; | |
| #endif | |
| // 1/size.x of screen in uv coords = IN.video_size.x/IN.texture_size.x/size.x; | |
| // then 1/8*size.x of screen: | |
| float ustep = IN.video_size.x/IN.texture_size.x/size.x/8; | |
| float sig = 0.; | |
| float border = IN.video_size.x/IN.texture_size.x; | |
| double ysum = 0., isum = 0., qsum = 0.; | |
| for (int i=0; i<Mwidth; ++i) | |
| { | |
| vec4 res = tex2D(tex, uv); | |
| #ifdef USE_RAW | |
| // outside of texture is 0,0,0 which is white instead of black | |
| if (uv.x > 0.0 && uv.x < border) | |
| { | |
| sig = NTSCsignal(res.xyz,alpha-4.1); | |
| sig = ((sig-0.518)*1000/12)-15; | |
| } | |
| else | |
| sig = 0; | |
| #else | |
| vec3 yuv = mul(float3x3( | |
| 0.299,0.587,0.114, //Y | |
| 0.5959,-0.2746,-0.3213, //I | |
| 0.2115,-0.5227,0.3112 //Q | |
| ), res.xyz); | |
| sig = (yuv.x+dot(yuv.yz,sign(vec2(coss(alpha),sinn(alpha))))); | |
| sig = ((sig*(1.960-0.518)+0.518-0.518)*1000/12)-15; | |
| #endif | |
| if (i < Ywidth) | |
| { | |
| ysum += sig; | |
| } | |
| if (i < Iwidth) | |
| { | |
| isum += sig*coss(alpha); | |
| } | |
| if (i < Qwidth) | |
| { | |
| qsum += sig*sinn(alpha); | |
| } | |
| alpha -= 1; | |
| uv.x -= ustep; | |
| } | |
| #define sa Saturation | |
| #define br Contrast | |
| vec3 rgb = | |
| #ifdef USE_GAMMA | |
| pow( | |
| #endif | |
| mul(vec3(ysum,isum,qsum),float3x3( | |
| float(br)/Ywidth, float(br)/Ywidth, float(br)/Ywidth, | |
| br*1.994681e-6*sa/Iwidth, br*9.151351e-8*sa/Iwidth, br*(-1.012984e-6)*sa/Iwidth, | |
| br*9.915742e-7*sa/Qwidth, br*(-6.334805e-7)*sa/Qwidth, br*1.667217e-6*sa/Qwidth) | |
| )/65536./255. | |
| #ifdef USE_GAMMA | |
| //(a^x)^2.2 = a^gamma; | |
| ,gamma/2.2) | |
| #endif | |
| ; | |
| #if (defined(USE_SUBPIXELS) || defined(USE_SCANLINES)) | |
| #ifdef COMPENSATE_WIDTH | |
| p.x /= (1.+(Ywidth/8.)/size.x); | |
| #endif | |
| vec2 q = (p*IN.texture_size/IN.video_size)*vec2(tv_pixels*3,size.y*2); | |
| #endif | |
| #ifdef USE_SCANLINES | |
| float z = | |
| #ifdef ANIMATE_SCANLINE | |
| mod(IN.frame_count,2.0)+ | |
| #endif | |
| 0.5; | |
| if (abs(mod(q.y,2)-z)<0.5) | |
| rgb *= dark_scanline; | |
| #endif | |
| // size of pixel screen in texture coords: | |
| //float output_pixel_size = IN.video_size.x/(IN.output_size.x*IN.texture_size.x); | |
| // correctness check | |
| //if (mod(p.x*output_pixel_size,2.0) < 1.0) | |
| // rgb = vec3(0); | |
| #ifdef USE_SUBPIXELS | |
| float left = mod(q.x-0.5*tv_pixels*3./IN.output_size.x,3); | |
| float right = left+tv_pixels*3./IN.output_size.x; | |
| vec3 w = min(max(vec3(0.,1.,2.),vec3(left)),vec3(1.,2.,3.)) | |
| -max(min(vec3(1.,2.,3.),vec3(right)),vec3(0.,1.,2.)) | |
| +min(max(vec3(3.,4.,5.),vec3(left)),vec3(4.,5.,6.)) | |
| -max(min(vec3(4.,5.,6.),vec3(right)),vec3(3.,4.,5.)); | |
| rgb = rgb*3.*w/(w.x+w.y+w.z); | |
| #endif | |
| return rgb; | |
| } | |
| // pos (left corner, sample size) | |
| vec4 monitor_sample(uniform sampler2D tex : TEXUNIT0, vec2 p, vec2 sample, uniform input IN) | |
| { | |
| // linear interpolation was... | |
| // now other thing. | |
| // http://imgur.com/m8Z8trV | |
| // AT LAST IT WORKS!!!! | |
| // going to check in retroarch... | |
| float2 size = IN.texture_size; | |
| vec2 next = vec2(.25,1.)/size; | |
| vec2 f = fract(vec2(4.,1.)*size*p); | |
| sample *= vec2(4.,1.)*size; | |
| vec2 l; | |
| vec2 r; | |
| if (f.x+sample.x < 1.) | |
| { | |
| l.x = f.x+sample.x; | |
| r.x = 0.; | |
| } | |
| else | |
| { | |
| l.x = 1.-f.x; | |
| r.x = min(1.,f.x+sample.x-1.); | |
| } | |
| if (f.y+sample.y < 1.) | |
| { | |
| l.y = f.y+sample.y; | |
| r.y = 0.; | |
| } | |
| else | |
| { | |
| l.y = 1.-f.y; | |
| r.y = min(1.,f.y+sample.y-1.); | |
| } | |
| vec3 top = mix(monitor(tex, p, IN), monitor(tex, p+vec2(next.x,0.), IN), r.x/(l.x+r.x)); | |
| vec3 bottom = mix(monitor(tex, p+vec2(0.,next.y), IN), monitor(tex, p+next, IN), r.x/(l.x+r.x)); | |
| return vec4(mix(top,bottom, r.y/(l.y+r.y)),1.0); | |
| } | |
| float4 main_fragment(uniform sampler2D tex : TEXUNIT0, float2 coords : TEXCOORD0, uniform input IN) : COLOR | |
| { | |
| #ifdef USE_SAMPLED | |
| return monitor_sample(tex, coords, 1./IN.output_size, IN); | |
| #else | |
| return vec4(monitor(tex, coords, IN), 1.); | |
| #endif | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment