Emulating CRT Phosphor Triads in GLSL Shaders

This article explores the procedural sub-pixel matrix algorithm—most notably popularized by Timothy Lottes' CRT shader—used to emulate cathode-ray tube (CRT) shadow masks in GLSL. You will learn the mathematical foundations of repeating phosphor triad emulation, how screen coordinates are mapped to repeating sub-pixel matrices, and how to implement this effect efficiently in a fragment shader to replicate the authentic physical glow and structure of classic display monitors.

The Shadow Mask Concept

Physical CRT monitors produce color images by firing electron beams through a perforated metal sheet called a shadow mask (or aperture grille). The mask aligns the beams with individual red, green, and blue phosphor deposits arranged in repeating triangular or striped triads.

In real-time computer graphics, replicating this physical layout requires modifying incoming pixel colors using a spatial attenuation pattern. The most efficient and standard algorithm to accomplish this in GLSL is coordinate-tiling through modulo arithmetic to sample a repeating weight matrix.

The Coordinate Mapping Algorithm

To generate a sub-pixel phosphor mask, the fragment shader maps the continuous viewport coordinates (gl_FragCoord.xy) into discrete sub-pixel bins. This is achieved using modulo operations:

  1. Horizontal Alignment: The horizontal coordinate is mapped to a repeating pattern representing the primary color channels (Red, Green, Blue). A cycle width of 3 pixels (or multiples thereof) matches the three phosphors.
  2. Vertical Staggering (Optional): To emulate a delta-gun shadow mask or slot mask rather than an aperture grille, alternate horizontal rows are shifted vertically using an offset based on mod(floor(coord.y), 2.0).

The base index calculation in GLSL typically looks like:

int x = int(mod(gl_FragCoord.x, 3.0));

Phosphor Triad Weight Formulation

Once the sub-pixel index is determined, an attenuation vector is applied to the sampled texture color. The classic Timothy Lottes shadow mask algorithm defines these weights analytically or via tiny discrete arrays.

For a standard 3-pixel repeating aperture/triad mask, the weights isolate and highlight each respective channel while dimming adjacent channels:

  • Index 0 (Red): vec3(1.0, maskDark, maskDark)
  • Index 1 (Green): vec3(maskDark, 1.0, maskDark)
  • Index 2 (Blue): vec3(maskDark, maskDark, 1.0)

Here, maskDark is a value between 0.0 and 0.5 that dictates the separation between phosphor elements.

Complete GLSL Implementation

Below is a complete, optimized GLSL function demonstrating the repeating sub-pixel matrix algorithm for a classic triad/dot shadow mask:

vec3 Mask(vec2 pos, vec3 color) {
    // Define the dark attenuation value for off-channels
    const float maskDark = 0.25;
    const float maskLight = 1.0;

    // Stagger rows for a classic dot-triad layout
    float line = maskLight;
    float odd = 0.0;
    if (fract(pos.x * 0.166666) < 0.5) odd = 1.0;
    if (fract((pos.y + odd) * 0.5) < 0.5) line = maskDark;

    // Determine the horizontal phosphor index (0 = R, 1 = G, 2 = B)
    vec3 mask = vec3(maskDark);
    float m = mod(pos.x, 3.0);

    if (m < 1.0) {
        mask.r = maskLight;
    } else if (m < 2.0) {
        mask.g = maskLight;
    } else {
        mask.b = maskLight;
    }

    // Apply the staggered line pattern and multiply by input color
    mask *= line;
    return color * mask;
}

Gamma and Brightness Compensation

Multiplying an image by a repeating matrix inherently reduces overall luminance because parts of each pixel area are attenuated. To maintain visual fidelity:

  1. Linear Color Space: The mask multiplication must occur in linear color space. Incoming sRGB textures must be converted to linear space before mask application and converted back to sRGB afterwards.
  2. Beam Overdrive: Because the mask eliminates between 30% to 60% of the light output, the input color is typically boosted by multiplying it by a brightness scalar or by applying a non-linear phosphor bloom curve prior to the mask stage.