Has there ever been written a phosphor trail/glow shader?

I’m about ready to put the finishing touches on my vintage TV .cgp which I hope to release to the board very soon but one thing that could be done to make it complete is something that simulates a phosphor trail, a good example being in Super Mario Bros. when Mario shoots a fireball and it leaves behind a faint white streak. I’ve tried to mess around in motion-blur shader but it tends to just create a mirror double image that trails behind, not quite the smeary look i’m going for.

Any help would be vastly appreciated!

I tried taking the motionblur-simple shader and applied an actual blur function (stolen from one of harlequin’s gameboy shader passes) to all of the PREV* bits and ended up with something that looks slightly softer/bloomier. It’s still not a smear (which I don’t think is really possible here, since references to previous frames use the raw frame rather than a filtered frame, and we don’t have any good way to get motion vectors, AFAIK), but it might serve your purposes or at least get you on the right track to something better:

/*
    Motion Blur Glow
    Authors: hunterk, cgwg, harlequin
 
    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by the Free
    Software Foundation; either version 2 of the License, or (at your option)
    any later version.
*/

struct previous
{
   uniform sampler2D texture;
   float2 tex_coord;
};

struct input
{
    float2 video_size;
    float2 texture_size;
    float2 output_size;
    float frame_count;
    float frame_direction;
    float frame_rotation;
    sampler2D texture : TEXUNIT0;
};

struct tex_coords
{
   float2 tex;
   float2 prev;
   float2 prev1;
   float2 prev2;
   float2 prev3;
   float2 prev4;
   float2 prev5;
   float2 prev6;
};

void main_vertex
(
   float4 position : POSITION,
   out float4 oPosition : POSITION,
   uniform float4x4 modelViewProj,
   float2 tex : TEXCOORD,

   previous PREV,
   previous PREV1,
   previous PREV2,
   previous PREV3,
   previous PREV4,
   previous PREV5,
   previous PREV6,
   out tex_coords coords
)
{
   oPosition = mul(modelViewProj, position);
   coords = tex_coords(tex, PREV.tex_coord,
      PREV1.tex_coord,
      PREV2.tex_coord,
      PREV3.tex_coord,
      PREV4.tex_coord,
      PREV5.tex_coord,
      PREV6.tex_coord);
}

struct output 
{
  float4 col    : COLOR;
};

output main_fragment( float2 texCoord : TEXCOORD0,
uniform input IN,
      previous PREV,
      previous PREV1,
      previous PREV2,
      previous PREV3,
      previous PREV4,
      previous PREV5,
      previous PREV6
)
{
    fixed2 texel = fixed2(1.0 / IN.texture_size);
    fixed2 lower_bound = float2(0.0);
    fixed2 upper_bound = texel * (IN.output_size - 1.0);

    float offsets[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);
    fixed weights[5] = fixed[](0.22702703, 0.19459459, 0.12162162, 0.05405405, 0.01621621);    //calculated from pascal triangle, normalized to 1 to prevent darkening

    fixed4 out_color = tex2D(IN.texture, clamp(texCoord, lower_bound, upper_bound)) * weights[0];

    for (int i = 1; i < 5; i++) 
    {
    out_color += tex2D(IN.texture, clamp(texCoord + fixed2(0.0, offsets[i] * texel.y), lower_bound, upper_bound)) * weights[i];
    out_color += tex2D(IN.texture, clamp(texCoord - fixed2(0.0, offsets[i] * texel.y), lower_bound, upper_bound)) * weights[i];
    out_color += tex2D(IN.texture, clamp(texCoord + fixed2(offsets[i] * texel.x, 0.0), lower_bound, upper_bound)) * weights[i];
    out_color += tex2D(IN.texture, clamp(texCoord - fixed2(offsets[i] * texel.x, 0.0), lower_bound, upper_bound)) * weights[i]; 
    out_color /= 1.15; //keeps it from getting to bloomy
 }
    
   float4 color = ((1.0 * out_color) + tex2D(PREV6.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV5.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV4.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV3.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV2.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV1.texture, texCoord)) / 2.0;
   color = (color * (1.0 * out_color) + tex2D(PREV.texture, texCoord)) / 2.0;
   color = (color + tex2D(IN.texture, texCoord)) / 2.0;
   
output OUT;
OUT.col = color;
return OUT;
}

Thanks hunter, only issue is if I run the shader in openGL mode, all I get is a black screen. It seems to work fine in direct3d but then again all my stacked shaders only seem to work in openGL! Any idea what could be causing this?

I’m using a Geforce 555M with a core i7 and latest drivers.

Hmm. Works fine here with Intel and Radeon in OpenGL.

I took another stab at making a phosphor trail shader. This time, I only used the luminance value from the previous frames, so it’s a grayscale after-image, which I think is more appropriate, and it only shows trails through very dark parts (e.g., black; there’s a threshold variable where you can set how dark it has to be to show through). It doesn’t screenshot well because the effect is usually subtle, so I cranked up the response time variable to take this exaggerated shot: Here is the code, also available in the common-shaders repo:

/* COMPATIBILITY 
   - HLSL compilers
   - Cg   compilers
*/

// TWEAKABLES //
#define response_time 0.4 // This controls how fast the luminance falls off. The default value is 4, higher values exaggerate the effect.
#define THRESHOLD 0.1     // This controls how dark a pixel needs to be before it will show trails

struct input
{
  float2 video_size;
  float2 texture_size;
  float2 output_size;
  float  frame_count;
  float  frame_direction;
  float frame_rotation;
  sampler2D texture;
};

struct previous
{
   uniform sampler2D texture;
   float2 tex_coord;
};

struct tex_coords
{
   float2 tex;
   float2 prev;
   float2 prev1;
   float2 prev2;
   float2 prev3;
   float2 prev4;
   float2 prev5;
   float2 prev6;
};

void main_vertex
(
   float4 position : POSITION,
   out float4 oPosition : POSITION,
   uniform float4x4 modelViewProj,
   float2 tex : TEXCOORD,
   previous PREV,
   previous PREV1,
   previous PREV2,
   previous PREV3,
   previous PREV4,
   previous PREV5,
   previous PREV6,
   out tex_coords coords
)
{
   oPosition = mul(modelViewProj, position);
   coords = tex_coords(tex, PREV.tex_coord,
      PREV1.tex_coord,
      PREV2.tex_coord,
      PREV3.tex_coord,
      PREV4.tex_coord,
      PREV5.tex_coord,
      PREV6.tex_coord);
}

float3x3 RGB_to_YIQ = float3x3(
         0.299,0.587,0.114, 
		 0.595716,-0.274453,-0.321263,
		 0.211456,-0.522591, 0.311135);

struct output 
{
  float4 col    : COLOR;
};

output main_fragment(in float2 texCoord : TEXCOORD0,
uniform input IN,
      previous PREV,
      previous PREV1,
      previous PREV2,
      previous PREV3,
      previous PREV4,
      previous PREV5,
      previous PREV6
)
{
// Sample our textures
float3 curr = tex2D(IN.texture, texCoord).rgb;
float3 prev0 = tex2D(PREV.texture, texCoord).rgb;
float3 prev1 = tex2D(PREV1.texture, texCoord).rgb;
float3 prev2 = tex2D(PREV2.texture, texCoord).rgb;
float3 prev3 = tex2D(PREV3.texture, texCoord).rgb;
float3 prev4 = tex2D(PREV4.texture, texCoord).rgb;
float3 prev5 = tex2D(PREV5.texture, texCoord).rgb;
float3 prev6 = tex2D(PREV6.texture, texCoord).rgb;

// Convert each previous frame to a grayscale image based on luminance value (i.e., convert from RGB to YIQ colorspace, where Y=luma)
float3 luma0 = float3(mul(prev0, RGB_to_YIQ).r);
float3 luma1 = float3(mul(prev1, RGB_to_YIQ).r);
float3 luma2 = float3(mul(prev2, RGB_to_YIQ).r);
float3 luma3 = float3(mul(prev3, RGB_to_YIQ).r);
float3 luma4 = float3(mul(prev4, RGB_to_YIQ).r);
float3 luma5 = float3(mul(prev5, RGB_to_YIQ).r);
float3 luma6 = float3(mul(prev6, RGB_to_YIQ).r);

// Add each previous frame's luma value together with an exponential decay
float3 input = float3(0.0);
input += luma0 * pow(response_time, 2.0);
input += luma1 * pow(response_time, 3.0);
input += luma2 * pow(response_time, 4.0);
input += luma3 * pow(response_time, 5.0); 
input += luma4 * pow(response_time, 6.0);
input += luma5 * pow(response_time, 7.0);
input += luma6 * pow(response_time, 8.0);

float4 luma_trails = float4(input, 1.0);
float4 screen = float4(0.0);

// Setup the threshold for when the luma trails are visible
// (I had a complicated non-conditional here based on the current
// frame's luma converted to alpha, but this looked better and was easier to tweak)
if ( curr.r + curr.g + curr.b > THRESHOLD )
 screen += float4(curr, 0.0);
else
 screen += float4(curr, 1.0);

   output OUT;
   OUT.col = lerp(screen, luma_trails, screen.a); // Any pixels darker than the threshold will allow the trails to shine through
   return OUT;
}

That looks pretty cool actually!

I’m fairly busy these days, so I can’t do much work on this, but maybe I can provide a couple tips: [ol] [li] Rather than using a threshold, try adding the previous frames in a linear colour space; that should have a similar effect of making the persistence more pronounced when the screen is darker.[/:m:3n8np704][/li][] The period of exponential decay for a CRT phosphor is quite short (much less than a frame). The longer-time persistence is more like 1/t rather than exp(-At) (see here). The long-term persistence can also last up to about a second, so you either need to access a lot of previous frames or make some kind of clever use of feedback (is that implemented somewhere?).[/:m:3n8np704][/ol]

So, take all of the prevs and current frame and wrap them in pow(, 1.0/2.2), then add pow(*, 2.2) at the end to bring it back to sRGB?

No feedback yet, but both byuu and maister have talked about implementing it, likely in the form of access to a single, previously filtered frame.

Thanks for the info :slight_smile:

No, the opposite: sRGB encoding has an approximate power of 1/2.2, and decoding has an approximate power of 2.2.

Oh, gotcha. I always get my gamma exponents backward…

That looks really nice, actually, and largely fixes the problem that led me to the threshold in the first place, namely that it was darkening bright pixels if the previous frame’s pixel was dark, which shouldn’t ever happen.

Here’s the updated code and a screenshot:

/* COMPATIBILITY 
   - HLSL compilers
   - Cg   compilers
*/

// TWEAKABLES //
#define response_time 1.0 // This controls how brightly the phosphors glow. The default value is 1, lower values reduce the effect.

struct input
{
  float2 video_size;
  float2 texture_size;
  float2 output_size;
  float  frame_count;
  float  frame_direction;
  float frame_rotation;
  sampler2D texture;
};

struct previous
{
   uniform sampler2D texture;
   float2 tex_coord;
};

struct tex_coords
{
   float2 tex;
   float2 prev;
   float2 prev1;
   float2 prev2;
   float2 prev3;
   float2 prev4;
   float2 prev5;
   float2 prev6;
};

void main_vertex
(
   float4 position : POSITION,
   out float4 oPosition : POSITION,
   uniform float4x4 modelViewProj,
   float2 tex : TEXCOORD,
   previous PREV,
   previous PREV1,
   previous PREV2,
   previous PREV3,
   previous PREV4,
   previous PREV5,
   previous PREV6,
   out tex_coords coords
)
{
   oPosition = mul(modelViewProj, position);
   coords = tex_coords(tex, PREV.tex_coord,
      PREV1.tex_coord,
      PREV2.tex_coord,
      PREV3.tex_coord,
      PREV4.tex_coord,
      PREV5.tex_coord,
      PREV6.tex_coord);
}

float3x3 RGB_to_YIQ = float3x3(
         0.299,0.587,0.114, 
		 0.595716,-0.274453,-0.321263,
		 0.211456,-0.522591, 0.311135);

struct output 
{
  float4 col    : COLOR;
};

output main_fragment(in float2 texCoord : TEXCOORD0,
uniform input IN,
      previous PREV,
      previous PREV1,
      previous PREV2,
      previous PREV3,
      previous PREV4,
      previous PREV5,
      previous PREV6
)
{

// Sample our textures, with the previous frames in linear color space
float3 curr = tex2D(IN.texture, texCoord);
float3 prev0 = pow(tex2D(PREV.texture, texCoord), 2.2);
float3 prev1 = pow(tex2D(PREV1.texture, texCoord), 2.2);
float3 prev2 = pow(tex2D(PREV2.texture, texCoord), 2.2);
float3 prev3 = pow(tex2D(PREV3.texture, texCoord), 2.2);
float3 prev4 = pow(tex2D(PREV4.texture, texCoord), 2.2);
float3 prev5 = pow(tex2D(PREV5.texture, texCoord), 2.2);
float3 prev6 = pow(tex2D(PREV6.texture, texCoord), 2.2);

// Convert each previous frame to a grayscale image based on luminance value (i.e., convert from RGB to YIQ colorspace, where Y=luma)
float3 luma0 = (float3(mul(prev0, RGB_to_YIQ).r));
float3 luma1 = (float3(mul(prev1, RGB_to_YIQ).r));
float3 luma2 = (float3(mul(prev2, RGB_to_YIQ).r));
float3 luma3 = (float3(mul(prev3, RGB_to_YIQ).r));
float3 luma4 = (float3(mul(prev4, RGB_to_YIQ).r));
float3 luma5 = (float3(mul(prev5, RGB_to_YIQ).r));
float3 luma6 = (float3(mul(prev6, RGB_to_YIQ).r));

// Add each previous frame's luma value together with a linear decay
float3 trails = float3(0.0);
trails += luma0 * (response_time / 100.0);
trails += luma1 * (response_time / 200.0) / 2.0;
trails += luma2 * (response_time / 300.0) / 2.0;
trails += luma3 * (response_time / 400.0) / 2.0; 
trails += luma4 * (response_time / 500.0) / 2.0;
trails += luma5 * (response_time / 600.0) / 2.0;
trails += luma6 * (response_time / 700.0) / 2.0;

trails = pow(trails, 1.0 / 2.2); // Bring the previous frames back to sRGB color space

   output OUT;
   OUT.col = float4((curr + trails), 1.0);
   return OUT;
}

EDIT: made a couple more tweaks

hmm, the picture seems to get very bright with it. doesn’t seem to happen on your screenshot though. maybe happened with your edit tweak? and yeah the effect is very subtle. unless we can accumulate values over past shader results the options are very limited to simulate this effect.

Yeah, you’re right. I think it was just a matter of not being very noticeable in that last shot. You can tame it by multiplying the (curr + trails) part by ~0.87 or so there at the end, but that still leaves the picture a little desaturated, just as a result of the grayscale trails being added to it.

There might be some other way of combining or compensating that fixes both problems, though. I’ll keep working on it.

I think the effect is shorter than my PVA screen ghosting, just can’t see it!

lol yeah, the effect is extremely subtle, to the point of being essentially undetectable on some games, which is why I did my testing with Aladdin on Genesis. The intro has a bright object (the genie) tracking across a flat black background, which is enough contrast to maximize the visibility. Even then, I have to pause RetroArch to really see it.

However, it’s possible the shader isn’t actually working on your machine. I’ve only tested with an AMD GPU w/ OpenGL driver, so it could be broken on some setups. If you’re on Nvidia hardware, you might try switching to the D3D driver instead.

Indeed I’m on Nvidia. Just tried d3d but it’s freezing the display, emulation and sound can still be heard but you just see the frozen shader menu. That’s when I have to revert the retroarch.cgp to make the emu works again. That happens quite a lot when you play with shaders, an auto-revert feature would be nice to see. (just an idea, not asking anything! thanks for your amazing work btw, retroarch is getting better all the time)

I was trying to get a “glow” effect but using bloom.cg didn’t give me what I hoped for. The “ghost pictures” are so well detailed… the opposite of a halo. The “gaussian blur” with bloom option of sweetFX seemed a bit nicer (still not perfect though).

I’ll have to figure out what’s up with Nvidia cards not liking any of my motion blur-derived shaders :confused: I remember maister mentioning something about needing to eliminate unused structs or somesuch…

You can try this shader I made, which may be more what you were looking for: http://www.mediafire.com/download/5vbfp … lation.zip

It applies a gaussian blur and then merges the blurred image with the original, unblurred image, so you maintain detail but still get a bloomy effect. You can put pretty much any single-pass shader in the first slot (i.e., replacing stock.cg) and it will bloom the result (cgwg’s CRT and Hyllian’s xBR both look good with it).

Thanks for sharing. :slight_smile:

Yes that’s the kind of effect I’m trying to have but it gets a bit blurry!

Damn, I was using bilinear filtering 2 weeks ago, but now that I’m used to cgwg crt-geom-flat it’s hard to look at a slightly blurred picture…

Really blurry glow in the back + perfectly crisp crt-geom, is that even possible?

You can try playing around with the BLURFACTOR variables at the top of the gaussian passes to see if something stronger looks better to you. Counterintuitively, as those passes get blurrier, they have less of an impact on the unblurred image, despite making a larger halo (like this).