Friday, July 25, 2014

Cathode Ray Tube Visuals

Old video game consoles, played today, do not look the same as they did during their heyday.

The cause of this is the rise of Liquid Crystal Display (LCD) monitors and televisions. In an LCD display, the visible surface is made up of tiny rectangles, covered by special crystals. The screen is backlit, but by running a current through the crystals, you can change them from transparent to opaque. The net effect of this discretization is that you can have very sharp visuals with very crisp lines, as is common on tablets and phones today.

In contrast, when the NES and SNES were released, televisions were built using Cathode Ray Tube (CRT) technology. A cathode ray tube is literally a cathode ray in a vacuum tube. The front of the display is coated in florescent material, and the cathode ray (essentially an electron gun) fires electrons at the screen, causing them to light up. The electron beam is directed by electromagnets in the tube, and they trace out a distinctive pattern on the screen: starting at the top, and going left-to-right in a straight line. Once it has drawn a line, the beam moves back to the left, just below the drawn line, and draws another line. Imagine an old-timey typewriter and you have the general idea.

CRT displays fell out of favour for two main reasons: they were bulkier and the displays were 'fuzzier'. Once LCD technology became as cheap as CRTs, the CRT display faded into history.

As is becoming clear on this blog, I have a great affinity for retro-style graphics. When I play emulated games on my PC or on a console, such as recently going through Castlevania: Symphony of the Night, I find the pixels very sharp, almost distractingly so. SotN has an option to blur out the graphics a little bit, but the blur is a uniform one, and does not accurately mimic my beloved CRT aesthetic.

That is why I wrote myself a quick helper routine that will take an image and produce a CRT-ized output. The original image is blown up to three times its original size. This is acceptable, because older consoles had resolutions of only roughly 256x256, and you have to upscale them on a modern computer display or the screen will only be about two and a half inches square. The pixels are blended evenly in the horizontal dimension, but have a discrete step between each scanline.

using System.Drawing;
using System.Linq;

namespace CRT
{
 public static class CrtImage
 {
  public static Image Produce(string filename)
  {
   using (var bitmap = (Bitmap)Bitmap.FromFile(filename))
    return Produce(bitmap);
  }

  public static Image Produce(Bitmap source)
  {
   var result = new Bitmap(source.Width * 3, source.Height * 3);

   foreach (var destX in Enumerable.Range(0, result.Width))
    foreach (var destY in Enumerable.Range(0, result.Height))
    {
     var srcX = destX / 3;
     var srcY = destY / 3;
     var srcColor = source.GetPixel(srcX, srcY);

     Color destColor;
     if (IsCenter(destX))
     {
      destColor = srcColor;
     }
     else
     {
      var blendColor = Color.Black;

      if (srcX > 0 && IsLeftOfCenter(destX))
       blendColor = source.GetPixel(srcX - 1, srcY);
      if (srcX < source.Width - 1 && IsRightOfCenter(destX))
       blendColor = source.GetPixel(srcX + 1, srcY);

      destColor = TwoToOneBlend(srcColor, blendColor);
     }

     if (false == IsMiddleRow(destY))
      destColor = Darken(destColor);

     result.SetPixel(destX, destY, destColor);
    }

   return result;
  }

  static Color Darken(Color color)
  {
   return Color.FromArgb(255,
    color.R / 2,
    color.G / 2,
    color.B / 2);
  }

  static Color TwoToOneBlend(Color primaryColor, Color secondaryColor)
  {
   return Color.FromArgb(255,
    (primaryColor.R * 2 + secondaryColor.R) / 3,
    (primaryColor.G * 2 + secondaryColor.G) / 3,
    (primaryColor.B * 2 + secondaryColor.B) / 3);
  }

  static bool IsLeftOfCenter(int destX) { return destX % 3 == 0; }
  static bool IsCenter(int destX) { return destX % 3 == 1; }
  static bool IsRightOfCenter(int destX) { return destX % 3 == 2; }
  static bool IsMiddleRow(int destY) { return destY % 3 == 1; }
 }
}

For an example of the results, let's take a sample of ocean and sand. If we just scale the sample up to be three times its original size, we get the following:

If, on the other hand, we run it through CrtImage.Produce(), we get the following:

Note that the CRT'd version is a bit darker (only two thirds as bright as the unfiltered image), and that you can see the individual lines if you look closely.

I recommend, however, that you don't look closely at it for long periods of time, as it can cause eye strain. Your parent's advice to not sit so close to the TV when you were a kid actually has a purpose.

No comments:

Post a Comment