Skip to content

Instantly share code, notes, and snippets.

@realmonster
Last active April 13, 2018 10:55
Show Gist options
  • Select an option

  • Save realmonster/c95aaae4d170535d1aa1d7c508908be0 to your computer and use it in GitHub Desktop.

Select an option

Save realmonster/c95aaae4d170535d1aa1d7c508908be0 to your computer and use it in GitHub Desktop.
NES NTSC composite signal simulation for RetroArch, algo by Bisqwit
// 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