Search Unity

  1. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Old-school CRT/NTSC Effect

Discussion in 'Works In Progress' started by PhobicGunner, Nov 6, 2013.

  1. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    EDIT: My new efforts have been moved into a different thread. Please comment there instead :)

    I don't know why, but I suddenly got it into my head that I wanted to make an NTSC/CRT effect in Unity.
    At first I tried to hack it. Every other "scanline" I offset texture by a pixel, and I also slightly separated the red and green channels. It looked OK, but not great.
    My next attempt is another beast altogether. This time it's a multipass effect, ported over from an emulator. The idea is that the shader converts a digital RGB image into an NTSC composite signal (chroma+luma), then converts it back to RGB values. There's some other stuff in there such as offsetting some values by a frame counter, but the basic idea is converting to and from an NTSC signal (there's some inherent data loss in there which results in that classic color bleeding we all know and love)

    Here's the result. It's some footage of Super Mario World on the SNES.

    https://dl.dropboxusercontent.com/u/99106620/CRT Effect/CRTDemo.html

    Click "Toggle NTSC" to switch the NTSC effect on and off (it's off by default). I also hacked some scanlines into the shader, just cause they look cool.
    Apologies if the video gets really repetitive (it's only a minute long, and just loops). I suggest turning down your sound (or mute it altogether)

    Thoughts?
     
    Last edited: Aug 1, 2016
  2. Hogge

    Hogge

    Joined:
    Aug 7, 2012
    Posts:
    12
    A CRT-effect is something I've been looking for for some time, as I want my game to look like something from the 32/64 bit era.
    Could you upload it somewhere?
    I was also wondering, are you aware of an effect that turns off perspective-correction?
     
  3. HolBol

    HolBol

    Joined:
    Feb 9, 2010
    Posts:
    2,884
    The dropbox link is a 404.
     
  4. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    Yeah I know, I took it down because I was worried about space and because I thought the thread died lol
    I'll put up the source code when I get the chance.
     
  5. Hogge

    Hogge

    Joined:
    Aug 7, 2012
    Posts:
    12
    I'm looking forward to this a lot. As I've asked before, I'm a bit curious regarding if you have any idea how to acheive the "shaky" effect seen in Sega Saturn and PSOne games, as they didn't have proper perspective correction?
     
  6. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    I don't, unfortunately. It was really a consequence of hardware design that caused that, which I'm not sure can be easily replicated on modern hardware short of writing your own software renderer.

    Anyway, here's the shader.

    Code (csharp):
    1.  
    2. Shader "Custom/NTSC"
    3. {
    4.     Properties
    5.     {
    6.         _MainTex ("Base (RGB)", 2D) = "white" {}
    7.         _Res ("Input Res XY / Display Res XY", Vector) = (640, 480, 640, 480)
    8.         _FrameCount ("Frame Count", Float) = 0
    9.     }
    10.     SubShader
    11.     {
    12.         Tags { "RenderType"="Opaque" }
    13.         LOD 200
    14.        
    15.         // base pass - creates Luma signal
    16.         Pass
    17.         {
    18.             CGPROGRAM
    19.             #pragma target 3.0
    20.             #pragma vertex vert
    21.             #pragma fragment frag
    22.             #include "UnityCG.cginc"
    23.  
    24.  
    25.             #define TEX2D(c) tex2D( _MainTex, (c) )
    26.             #define PI 3.14159265
    27.             #define PI_OVER_THREE PI / 3
    28.  
    29.  
    30.             sampler2D _MainTex;
    31.             float4 _Res;
    32.             float _FrameCount;
    33.  
    34.  
    35.             struct v2f
    36.             {
    37.                 float4 pos : SV_POSITION;
    38.                 float4 uv : TEXCOORD0;
    39.             };
    40.  
    41.  
    42.             v2f vert (appdata_base v)
    43.             {
    44.                 v2f o;
    45.                 o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
    46.                 o.uv = v.texcoord;
    47.                 return o;
    48.             }
    49.  
    50.  
    51.             half4 frag (v2f i) : COLOR
    52.             {
    53.                 float2 xy = i.uv.xy;
    54.                
    55.                 float2 xyp = xy * _Res.zw * 4.0 * PI_OVER_THREE;
    56.                 xyp.y = xyp.y / 2.0 + 2.0 * PI_OVER_THREE * fmod( _FrameCount, 2 );
    57.  
    58.  
    59.                 float4 rgb = TEX2D( xy );
    60.  
    61.  
    62.                 float3x3 rgb2yuv = float3x3(0.299,-0.14713, 0.615,
    63.                                             0.587,-0.28886,-0.51499,
    64.                                             0.114, 0.436  ,-0.10001);
    65.  
    66.  
    67.                 float3 yuv;
    68.                 yuv = mul( rgb2yuv, rgb.rgb );
    69.  
    70.  
    71.                 float dx = PI_OVER_THREE;
    72.                 float c0 = yuv.x + yuv.y * sin(xyp.x+xyp.y) + yuv.z*cos(xyp.x+xyp.y);
    73.                 float c1 = yuv.x + yuv.y * sin(xyp.x+xyp.y+dx) + yuv.z * cos(xyp.x+xyp.y+dx);
    74.                 float c2 = yuv.x + yuv.y * sin(xyp.x+xyp.y+2.0*dx) + yuv.z * cos(xyp.x+xyp.y+2.0*dx);
    75.                 float c3 = yuv.x + yuv.y * sin(xyp.x+xyp.y+3.0*dx) + yuv.z * cos(xyp.x+xyp.y+3.0*dx);
    76.  
    77.  
    78.                 return (float4(c0,c1,c2,c3)+0.65)/2.3;
    79.                 //return float4(1,1,1,1);
    80.             }
    81.  
    82.  
    83.             ENDCG
    84.         }
    85.  
    86.  
    87.         // second pass - adds chroma signal and scanlines
    88.         Pass
    89.         {
    90.             CGPROGRAM
    91.             #pragma target 3.0
    92.             #pragma vertex vert
    93.             #pragma fragment frag
    94.             #include "UnityCG.cginc"
    95.  
    96.  
    97.             #define TEX2D(c) tex2D( _MainTex, (c) )
    98.             #define PI 3.14159265
    99.             #define PI_OVER_THREE PI / 3
    100.  
    101.  
    102.             sampler2D _MainTex;
    103.             float4 _Res;
    104.             float _FrameCount;
    105.  
    106.  
    107.             struct v2f
    108.             {
    109.                 float4 pos : SV_POSITION;
    110.                 float4 uv : TEXCOORD0;
    111.             };
    112.  
    113.  
    114.             v2f vert (appdata_base v)
    115.             {
    116.                 v2f o;
    117.                 o.pos = mul( UNITY_MATRIX_MVP, v.vertex );
    118.                 o.uv = v.texcoord;
    119.                 return o;
    120.             }
    121.  
    122.  
    123.             half4 frag (v2f i) : COLOR
    124.             {
    125.                 float scanlineIntensity = ( (int)( i.uv.y * _Res.y ) % 2 );// * ( (int)( i.uv.x * _Res.z ) % 2 );
    126.                 scanlineIntensity = clamp( scanlineIntensity, 0.75, 1 );
    127.  
    128.  
    129.                 float2 xy = i.uv.xy;
    130.                 float2 xyf = frac( xy * _Res.zw );
    131.                 float2 xyp = floor( xy * _Res.zw ) + float2( 0.5, 0.5 );
    132.                 xy = xyp / _Res.zw;
    133.                 float offs = fmod( _FrameCount, 2 )/2.0;
    134.                 float val1 = xyp.x + xyp.y / 2.0 + offs;
    135.                 float val2 = -1.0 + val1;
    136.                 float val3 = 1.0 + val1;
    137.                 float4 phases = (float4(0.0,0.25,0.5,0.75) + float4(val1,val1,val1,val1)) *4.0*PI/3.0;
    138.                 float4 phasesl = (float4(0.0,0.25,0.5,0.75) + float4(val2,val2,val2,val2)) *4.0*PI/3.0;
    139.                 float4 phasesr = (float4(0.0,0.25,0.5,0.75) + float4( val3,val3,val3,val3)) *4.0*PI/3.0;
    140.                 float4 phsin = sin(phases);
    141.                 float4 phcos = cos(phases);
    142.                 float4 phsinl= sin(phasesl);
    143.                 float4 phcosl= cos(phasesl);
    144.                 float4 phsinr= sin(phasesr);
    145.                 float4 phcosr= cos(phasesr);
    146.                 float4 phone = float4(1.0,1.0,1.0,1.0);
    147.  
    148.  
    149.                 float2 one = 1.0/_Res.zw;
    150.  
    151.                 float4 c = TEX2D(xy)*2.3-0.65;
    152.                 float4 cl= TEX2D(xy + float2(-one.x,0.0))*2.3-0.65;
    153.                 float4 cr= TEX2D(xy + float2( one.x,0.0))*2.3-0.65;
    154.  
    155.  
    156.                 float3 yuva = float3((dot(cl.zw,phone.zw)+dot(c.xyz,phone.xyz)+0.5*(cl.y+c.w))/6.0, (dot(cl.zw,phsinl.zw)+dot(c.xyz,phsin.xyz)+0.5*(cl.y*phsinl.y+c.w*phsin.w))/3.0, (dot(cl.zw,phcosl.zw)+dot(c.xyz,phcos.xyz)+0.5*(cl.y*phcosl.y+c.w*phcos.w))/3.0);
    157.                 float3 yuvb = float3((cl.w*phone.w+dot(c.xyzw,phone.xyzw)+0.5*(cl.z+cr.x))/6.0, (cl.w*phsinl.w+dot(c.xyzw,phsin.xyzw)+0.5*(cl.z*phsinl.z+cr.x*phsinr.x))/3.0, (cl.w*phcosl.w+dot(c.xyzw,phcos.xyzw)+0.5*(cl.z*phcosl.z+cr.x*phcosr.x))/3.0);
    158.                 float3 yuvc = float3((cr.x*phone.x+dot(c.xyzw,phone.xyzw)+0.5*(cl.w+cr.y))/6.0, (cr.x*phsinr.x+dot(c.xyzw,phsin.xyzw)+0.5*(cl.w*phsinl.w+cr.y*phsinr.y))/3.0, (cr.x*phcosr.x+dot(c.xyzw,phcos.xyzw)+0.5*(cl.w*phcosl.w+cr.y*phcosr.y))/3.0);
    159.                 float3 yuvd = float3((dot(cr.xy,phone.xy)+dot(c.yzw,phone.yzw)+0.5*(c.x+cr.z))/6.0, (dot(cr.xy,phsinr.xy)+dot(c.yzw,phsin.yzw)+0.5*(c.x*phsin.x+cr.z*phsinr.z))/3.0, (dot(cr.xy,phcosr.xy)+dot(c.yzw,phcos.yzw)+0.5*(c.x*phcos.x+cr.z*phcosr.z))/3.0);
    160.  
    161.  
    162.                 float3x3 yuv2rgb = float3x3(1.0, 1.0, 1.0,
    163.                                             0.0,-0.39465,2.03211,
    164.                                             1.13983,-0.58060,0.0);
    165.  
    166.  
    167.                 if (xyf.x < 0.25)
    168.                     return float4( mul( yuv2rgb, yuva ) * scanlineIntensity, 0.0);
    169.                 else if (xyf.x < 0.5)
    170.                     return float4( mul( yuv2rgb, yuvb ) * scanlineIntensity, 0.0);
    171.                 else if (xyf.x < 0.75)
    172.                     return float4( mul( yuv2rgb, yuvc ) * scanlineIntensity, 0.0);
    173.                 else
    174.                     return float4( mul( yuv2rgb, yuvd ) * scanlineIntensity, 0.0);
    175.             }
    176.  
    177.  
    178.             ENDCG
    179.         }
    180.        
    181.     }
    182.     FallBack "Diffuse"
    183. }
    184.  
    It's a two-pass image effect, and you need to pass the frame count for the animated scanline effect (actually it works best if framecount is incremented 30 times per second instead of 60, looks more retro-ish)
     
  7. Hogge

    Hogge

    Joined:
    Aug 7, 2012
    Posts:
    12
    Cool.
    How do I apply it? I can't drag it to the camera.
     
  8. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    It's a shader, it has to be used from an image effect script (via OnRenderImage callback)
    Something like this:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5.  
    6. public class CRTEffect : MonoBehaviour
    7. {
    8.     public Shader CRTShader;
    9.    
    10.     private Material mat;
    11.    
    12.     void Awake()
    13.     {
    14.         mat = new Material( CRTShader );
    15.     }
    16.    
    17.     void OnDestroy()
    18.     {
    19.         Destroy( mat );
    20.     }
    21.    
    22.     void OnRenderImage( RenderTexture src, RenderTexture dest )
    23.     {
    24.         RenderTexture pass1 = RenderTexture.GetTemporary( src.width, src.height );
    25.         Graphics.Blit( src, pass1, mat, 0 ); // render first pass
    26.         Graphics.Blit( pass1, dest, mat, 1 ); // render second pass
    27.         RenderTexture.ReleaseTemporary( pass1 );
    28.     }
    29. }
    30.  
     
    Last edited: Jan 21, 2014
  9. Hogge

    Hogge

    Joined:
    Aug 7, 2012
    Posts:
    12
    Parsing error :(
     
  10. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    Oops, typed "void" instead of "class". Try it now.
     
  11. ZJP

    ZJP

    Joined:
    Jan 22, 2010
    Posts:
    2,646
    Code (csharp):
    1.  
    2. RenderTexture pass1 = RenderTexture.GetTemporary( src.width, src.height );
    3.  
     
  12. Undertaker-Infinity

    Undertaker-Infinity

    Joined:
    May 2, 2014
    Posts:
    103
    Sorry for necroing, but I was snooping for NTSC shaders and found this.
    I believe the *shake* error was a projection based on look up tables, with errors in them.
    Either that or it was rounding errors during the calculation due to integer math.
    In any case, you'd have to get into the camera. The projection nowadays is done in hardware, so this would be tricky to pull off :-/
    Perhaps with a Vertex shader and an estimation of the error based on distance from the camera it could be achieved.
     
  13. HeyItsLollie

    HeyItsLollie

    Joined:
    May 16, 2015
    Posts:
    54
    Another year, another necro. Digging around for NTSC shaders as well, and this is one of the very few real results available. Like, at all.

    I'm hoping someone can help figure out what's gone wrong here. I'm testing a mock-up image (left) on a quad, and I have an orthographic camera with the CS script and shader applied. The only change I've made to the shader is the Input/Display resolution (256, 240, 256, 240), to match the input image.



    I've had very little luck trying to troubleshoot it. The best I can get out of it is a red channel image with very faint blue/green channels showing through the scanlines — Fine for the Virtual Boy, but not what I'm aiming for! If anyone knows how to correct this, it'd be greatly appreciated.
     
    HerrDarko likes this.
  14. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    I'd sorta abandoned this, but I'll give another shot at fixing it up.

    EDIT: Interestingly, I have one project in which this works and one in which it doesn't. No idea what the difference is yet, but that should be a clue as to how to get this fixed up and working properly :)
    I also have some ideas on how to make it more efficient and compact, by potentially using lookup tables to do the RGB->YIQ->RGB conversion instead of doing all of the math in the shader.

    EDIT: OK, so today I actually decided to implement the effect from scratch using LUTs instead of a per-pixel matrix multiply. The good news is that it's working (again). The even better news is that the new shader is much more concise. The even more better(er?) news is that it actually looks a little bit better IMHO, as I'm porting over some code from a different NTSC shader. This one is three passes instead of just two, but actually looks a bit more retro IMHO because it basically:

    1.) converts RGB to YIQ color space
    2.) converts the YIQ color into a composite signal
    3.) low-passes and smears the composite signal a bit, and then decodes the composite signal back into RGB.

    This has the effect of adding some of the image fuzzing and color smearing that you'd see on older TVs that the previous method didn't have as much.
    Also, since this shader is a lot more of my own code than the previous one was, I have a much better understanding of how it works and so it should be easier to maintain (thankfully!)
     
    Last edited: Jul 29, 2016
  15. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    Some results of the reimplementation, using a screenshot from Sonic 2 for testing. Added a simple pixel mask effect which sort of simulates the shadow mask used in some TVs, plus fisheye distortion to give it the curved CRT look
    Have yet to find a situation which makes everything go magenta in this version, so that's a plus I suppose :)

     
    HeyItsLollie likes this.
  16. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    Today I decided to write a screen fading image effect inspired by the Sonic The Hedgehog titles on the Sega Genesis. Those of us who've played it will remember that hue shift as the screen faded -when fading in you could see the whole palette shift through a blue-ish color. After doing some research, it turns out that the R, G, and B fades are independently staggered from each other. Since they don't fade at the same time, it produces a hue shift. Whether or not this was intentional or a bug isn't known, but I decided to replicate it anyway.
    So the screen fade effect lets you specify an RGB separation value, which is how much each component is staggered. It goes from 0 (off) to 0.66 (components are faded in sequence, first R then G then B, with no overlap). You can see the results here (shows both fading to black, and fading to white):



    EDIT: Oh, and by the way, I'm thinking I'll be turning this into a whole suite of effects and releasing it on the asset store. In addition to effects intended for pixel art 2D games, I'll also be throwing in some effects for Saturn/PS1 style games as well, such as the good old wobbly vertex effect, affine texture mapping, and a few dithering effects I came up with (as an example, check out the car shadows in Virtua Racer for the Sega Saturn).

    It will also include a custom input manager which correctly handles mouse position even with the fisheye effect enabled (it can apply an optional fisheye distortion to the mouse position for mouse events).

    EDIT2: I'll probably also throw in an interesting tool I made... basically, a while back I spent a lot of time researching making digital-to-analog circuitry that could interface with the VGA standard. It turns out that, for each color component, you make a voltage divider which contains a series of resistors of doubling resistance values, one resistor per bit, such that when all bits are on the voltage sums to 0.7v (which is max voltage in VGA).
    So, based on that research, I made a tool for Unity. You specify how many bits per color component, and voltage per bit, and it tries to spit out resistance values that would match this. You can tweak the resistance values from there (for instance, picking closest values you could actually order from manufacturers), and then based on that it can spit out a color lookup table which is made by calculating voltage values, which can be plugged into the Color Grading effect as-is. So in the end you can produce a color LUT which would be the result of using theoretical custom-designed VGA circuitry. Overkill? Probably. But I think it's neat anyway, and you can always just plug in number of bits, hit Generate, and just save it without messing with any of the values.
     
    Last edited: Jul 30, 2016
    HeyItsLollie likes this.
  17. HeyItsLollie

    HeyItsLollie

    Joined:
    May 16, 2015
    Posts:
    54
    Wickeddd. There's so many "retro" assets and tutorials out there that merely throw chromatic aberration over the top (or even more simply, shift color channels left and right) and call it a day, so it's refreshing to see an effect that goes the full mile. Especially since older titles were designed around the limits of composite video, and even took advantage of them in some cases.

    Integer vertex snapping and affine texture mapping are always fun too. Mercilessly unforgiving, but fun!

    The VGA LUT generator sounds interesting though. I'm assuming the end result is fairly subtle but noticeable. How far can the generator's values be pushed?

    Edit: I saw the Video Mode Demo video on your channel, and it looks great right now. How customizable are the video mode presets? If I were to dig into the shader itself, could the amount of color smearing (for example) be fine-tuned? Can the display video be locked to a specific resolution or aspect ratio? (eg: Input resolution is 320x240, while output is set to display at 4:3 with pillar/letterboxing, regardless of the player's actual screen res)
     
    Last edited: Aug 11, 2016
  18. PhobicGunner

    PhobicGunner

    Joined:
    Jun 28, 2011
    Posts:
    1,813
    HeyItsLollie likes this.
unityunity