Welcome back to the world of image effect shaders! In gaming today, nostalgia reigns supreme - not only are old games frequently remastered for modern hardware, but new properties pop up occasionally with an aesthetic based entirely on tech from the past. Those who grew up in the 80s and 90s probably watched films at home using VHS tapes and will surely remember the graphical artefacts those tapes suffered from.
On top of that, VHS tapes were predominantly viewed on CRT TVs. Weâve discussed CRT screens on here before - one of the characteristic features of a CRT is the presence of scanlines. Today weâre going to create an effect that brings together both ancient technologies.
Image by Joshua_Willson from Pixabay
Scanlines
CRT screens typically produce images in rows. Each row is filled in horizontally, left-to-right, and once a row is filled, the following row is filled. Each row is called a scanline. But when we talk about scanlines colloquially, youâre probably thinking of the dark lines that appear over the image - the gaps between the real scanlines. Weâre skipping over a lot of technical detail - not all CRTs had visible scanlines due to the arrangement of pixels - but as is now tradition with this series, weâre more interested in peopleâs expectations of what a scanline should look like when building the effect. Weâre going to overlay our source image with slightly darker lines. Weâll have two modes in mind - relatively thick scanlines which will look best when faded, and thinner scanlines which will look better when dark. Inside the Textures folder are our two textures - Scanlines.png and Scanlines2.png - each of which has a resolution of only 4x4.
Letâs jump into the Scanline shader, found at Resources/Shaders/Scanlines.shader. Since itâs so basic, itâs the perfect shader to warm ourselves up since the previous Ultra Effects article!
Weâre going to need four variables: _MainTex
is the base image texture captured by the camera; _ScanlineTex
is one of the two scanline textures above, or you can supply your own; _Strength
controls how dark the scanlines will appear; and _Size
controls how large the scanlines are. Put these declarations above the fragment shader.
sampler2D _MainTex;
sampler2D _ScanlineTex;
float _Strength;
int _Size;
Now weâre going to overlay _ScanlineTex
on top of _MainTex
. Itâs best to make sure any scanline texture you choose to use has its filter mode set to point filtering so that your scanlines look crisp. And youâll certainly want to set its wrap mode to repeat, because this tiny texture is going to be tiled many times across the screen.
Weâre going to multiply the UV coordinates used to sample _ScanlineTex
by using _Size
as a multiplier inside the fragment shader.
float4 frag (v2f i) : SV_Target
{
float2 scanlineUV = i.uv * (_ScreenParams.y / _Size);
...
}
Here, _ScreenParams.y
contains the y-resolution of the screen texture, _MainTex
. You could scale the UVs by _ScreenParams.xy
to include scaling along the x-axis, but since our scanlines are purely horizontal, itâs not necessary. The original UVs contained in i.uv
are normalised between 0 and 1, so multiplying by _ScreenParams.y
and dividing by _Size
will tile the texture exactly _Size
times along the y-axis.
Next, we will sample the original image texture and the scanline texture using i.uv
and scanlineUV
respectively.
float4 col = tex2D(_MainTex, i.uv);
float4 scanlines = tex2D(_ScanlineTex, scanlineUV);
Now we need to combine the two image samples. We could multiply the two images together - this would result in totally black lines between intact rows of the original image, but we want to control the _Strength
of the effect. Instead, weâll lerp
between the original image - col
- and the modified image - col * scanlines
- using _Strength
as the interpolation factor.
return lerp(col, col * scanlines, _Strength);
Now we must create a script called ScanlinesEffect
to drive the effect. Itâs super simple so we wonât go too in-depth but take the time to re-familiarise yourself with the script format if you need to. Itâs at Scripts/ScanlinesEffect.cs. Its only function is to pass variables to the shader and perform a basic Graphics.Blit
each frame.
[SerializeField]
private Texture2D scanlineTex = null;
[SerializeField]
private float strength = 0.1f;
[SerializeField]
private int size = 8;
// Find the Scanlines shader source.
public override void OnCreate()
{
if(scanlineTex == null)
{
scanlineTex = Texture2D.whiteTexture;
}
baseMaterial = new Material(Resources.Load<Shader>("Shaders/Scanlines"));
baseMaterial.SetTexture("_ScanlineTex", scanlineTex);
baseMaterial.SetFloat("_Strength", strength);
baseMaterial.SetInt("_Size", size);
}
public override void Render(RenderTexture src, RenderTexture dst)
{
Graphics.Blit(src, dst, baseMaterial);
}
When we run the scene with this effect activated (the effect can be found at Effects/Scanlines) using the thin scanline texture variant and a medium strength of around 0.4, the result looks like this:
Visual Glitches
VHS tapes were known for their visual artefacts. Depending on how old the tape is, how it has been stored and whether it was copied from another tape, all kinds of oddities, from noise and distortion to missing or fuzzy bands across the image, plagued VHS. Weâve covered noise in a previous tutorial, and today weâre going to leverage what we learned to add a fuzzy band of interference over our image. To render a fuzzy band, weâll use a thin, long gradient texture to control the âshapeâ of the band (or, if youâd like, the number of bands that appear - the example will stick to one band). Then, weâll modulate the UVs used to read the texture over time so that the band appears to scroll down the image, as it might in a real VHS tape, and add a noise offset to the UVs so the band is a bit fuzzy.
Letâs kick off with Resources/Shaders/Interference.shader. Weâre going to copy the Perlin noise code we used back in the film grain article inside a CGINCLUDE
block above the shader Pass
- remember that the perlin2D
function collapses a float2
vector into a single pseudorandom value.
Letâs think about the variables weâll need for the effect. Weâre going to need to pass in the gradient texture, which weâll call _InterferenceTex
, and a float
to control the _Speed
of the effect.
sampler2D _MainTex;
sampler2D _InterferenceTex;
float _Speed;
Next, weâll calculate the UVs for sampling the interference texture. Weâre going to modulate this by time and noise - weâll calculate the noise first.
float4 frag (v2f i) : SV_Target
{
float2 pos = i.uv * _ScreenParams.xy / 4.0f;
float2 offset = float2(perlin2D(pos), perlin2D(pos + 0.5f));
...
}
Using the _ScreenParams
like we did for the scanline effect, but this time using both the x
and y
components, weâll convert from UV coordinates to what I like to call âpixel coordinatesâ, then divide by 4. This value will be the seed for our perlin2D
random number generator. Dividing by 4 means that the fuzz will have a lower resolution - itâs a magic number that you can change if you want. Weâll then calculate the noise-based offset
using the perlin2D
function on the position. We add 0.5 to the y-component seed for a bit of variation to the offset.
float2 interferenceUV = i.uv + offset * 0.05f + _Time.yy * _Speed;
float interference = tex2D(_InterferenceTex, interferenceUV);
To add the time-based offset, weâll add an extra offset based on _Time.y
(which represents unscaled time, as opposed to _Time.x
which is slowed down), multiplied by the effectâs _Speed
. Here, we also add the interference offset
, multiplied by another magic number - 0.05 - so its effect isnât too great. Using those UVs, we sample the interference gradient texture.
float4 col = tex2D(_MainTex, i.uv);
return lerp(col, 1.0f, interference);
All thatâs left to do is sample the main texture and add fuzziness where necessary. To add the fuzziness, we lerp
between the main texture sample and full-white, where the value of the interference texture sample is the interpolation factor.
Letâs gloss over the script to control this effect. Itâs more basic than even the scanline effect script, as it has fewer variables. Open Scripts/InterferenceEffect.cs to see. As with ScanlineEffect
, InterferenceEffect
âs job is to Blit
the screen each frame and pass variables over to the shader.
[SerializeField]
private Texture2D interferenceTex = null;
[SerializeField]
private float speed = 0.5f;
// Find the Interference shader source.
public override void OnCreate()
{
if (interferenceTex == null)
{
interferenceTex = Texture2D.whiteTexture;
}
baseMaterial = new Material(Resources.Load<Shader>("Shaders/Interference"));
baseMaterial.SetTexture("_InterferenceTex", interferenceTex);
baseMaterial.SetFloat("_Speed", speed);
}
public override void Render(RenderTexture src, RenderTexture dst)
{
Graphics.Blit(src, dst, baseMaterial);
}
And with that out of the way, we can see what the effect looks like when applied to the scene - the effect can be found at Effects/Interference:
Combining the scanline and interference effects, as well as adding noise to the full image, we can create a basic CRT effect - this project contains a composite effect in Effects/CRT:
Conclusion
Weâve taken a lot of inspiration from retro effects during this series, and this article is no exception - CRT screens and VHS tapes are hugely nostalgic for multiple generations. CRT screens have many characteristic features, one of which is the presence of visible scanlines not found on modern LCD or LED screens. VHS tapes are often remembered for their poor-quality artefacts compared to DVDs, including bands of fuzzy or missing information across the screen.
Next time, weâll look at a kaleidoscope effect in which we mirror an image, or parts of it, several times at certain angles.
Acknowledgements
Assets
This tutorial series uses the following asset packs:
Forest - Low Poly Toon Battle Arena / Tower Defense Pack | AurynSky |
Supporters
Support me on Patreon or buy me a coffee on Ko-fi for PDF versions of each article and to access certain articles early!
Special thanks to my Patreon backers:
- Gemma Louise Ilett
- Jack Dixon
- John Selig
- Shaun Wall
- Chris Sims
- Christopher Pereira
- JacksonG
- Pat
And a shout-out to my top Ko-fi supporters:
- Hung Hoang
- Mysterious Anonymous Person