Retro CRT Shader — A post processing effect study
Tags: Game Development, Babylonjs, 3d, Shaders, Babylon

Think back to those halcyon days when video games came on cartridges, controllers were square, and televisions were big, rounded, and weighed nearly as much as a fully loaded pickup truck. The days when computational speed was measured in megahertz, and when television was fuzzy pixels dancing across curved screens illuminated by cathode rays that may or may not have permanently damaged our eyes. Ahhh the memories…

We can’t bring back the past, but through the magic of shaders and math, we can try to emulate some of the artifacts of this bygone era to give your games that warm, fuzzy feeling you know you’ve missed:

Don’t mind the banding, that’s just the aliasing of our scan lines when compressed. Though you sometimes see this effect when real photographs of CRTs are manipulated as well! — https://drigax.github.io/RooftopRampage/

Starting off, TV screens were originally rounded and convex, driven by the limits of display technology at the time. Put briefly, our modern LED televisions are a densly packed matrix of individually controllable lights that we can use to create a moving picture:

The precursor to this type of display technology were cathode rays, sequenced beams of electrons that would react with a phosphor coating on the inside of the television glass to create an image:

image borrowed from https://computer.howstuffworks.com/monitor7.htm

Magnetic fields were used to bend these electron beams to position them at different locations on the screen, which was curved to minimize the distortion throughout the picture.

For some more info, I highly recommend checking out Technology Connection’s series of videos on YouTube for more detailed explanation of how CRT televisions work, and a supplement for an in-depth explanation of early color implementations.

Lets start with simulating the low hanging fruit and working our way up. We can simulate the screen curvature by doing a similar process via a pixel, or post processing shader. We can define a mathematical curve, and modify how we sample from our rendered scene texture to the resulting render texture as we apply our process.

With the help of hiulit’s port of knarkowicz’s retro styled shader, we can see that the curvature of a screen can be represented by distorting the UVs using a cubic function expressed as:

outUV = inUV(abs(inUV * 2 — 1) / curvature)² + inUV

Graphed, it may be a bit more intuitive:

1 dimensional view of remapped UVs after applying screen curvature filter.

Our “curvature” parameter can be used to control how extreme we want to deform our texture near the edges, to create the appearance that the image is projected on a rounded surface. We can represent a flatter display with a larger curvature factor. We also choose to add some bounds to this function to represent the “frame” around the television where the computed UV exceeds [0,1].

Implemented in GLSL, we get:

#ifdef GL_ES

precision highp float;

#endif // Samplers

varying vec2 vUV;

uniform sampler2D textureSampler; // Parameters

uniform vec2 curvature;



vec2 curveRemapUV(vec2 uv)

{

// as we near the edge of our screen apply greater distortion using a cubic function uv = uv * 2.0–1.0;

vec2 offset = abs(uv.yx) / vec2(curvature.x, curvature.y);

uv = uv + uv * offset * offset;

uv = uv * 0.5 + 0.5;

return uv;

} void main(void)

{

vec2 remappedUV = curveRemapUV(vec2(vUV.x, vUV.y));

vec4 baseColor = texture2D(textureSampler, remappedUV); if (remappedUV.x < 0.0 || remappedUV.y < 0.0 || remappedUV.x > 1.0 || remappedUV.y > 1.0){

gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);

} else {

gl_FragColor = baseColor;

}

}

We can modify the default Babylon.js playground scene to allow us to add a post process to the end of the rendering pipeline without much hassle: