It's free to join Gamasutra!|Have a question? Want to know who runs this site? Here you go.|Targeting the game development market with your product or service? Get info on advertising here.||For altering your contact information or changing email subscription preferences.
Registered members can log in here.Back to the home page.

Search articles, jobs, buyers guide, and more.

By Bruce Dawson
Gamasutra
[Author's Bio]
June 22, 2001

Introduction

Illegal Colors

NTSC Composite Waveform Red

Resolution Limitations

What About PAL and HDTV

Printer Friendly Version
 
Discuss this Article

This feature originally appeared in the proceedings of the 2001 Game Developers Conference

 

Letters to the Editor:
Write a letter
View all letters


Features

What Happened to My Colors!?!
Displaying Console Computer Graphics on a TV

NTSC Composite Waveform for Pure Red

By now, I'm sure that you recognize this waveform as being a highly saturated signal with relatively low luminance. This means it must be red or blue. The phase of the waveform makes it obvious that it is red. Okay, maybe not so obvious, but the sine wave is clearly shifted to the right a bit compared to yellow, thus indicating a non-yellow hue. Saturated colors that use two primaries tend to go off the top of the graph, because they are naturally brighter. Saturated colors that have a lot of just one primary tend to go off the bottom of the graph. This signal went down to -0.333, but was clamped at -0.20.

NTSC composite waveform for red.

The problem is, the NTSC signal isn't allowed to go up 33% higher than white. That would have destroyed backwards compatibility with the millions of monochrome sets in use when color NTSC was designed. Scaling all of the signals down would have allowed pure yellow to be transmitted, but it would have reduced the precision for other colors, and would have made color TV signals dim when compared with monochrome signals. So, the designers of the NTSC color system compromised. Pure yellow and cyan are pretty rare in nature, so they specified that the signal could go 20% above white and 20% below black. In other words, they said that you are not allowed to broadcast certain highly saturated colors.

In some cases, it's not even a matter of not being allowed to; it's actually physically impossible! On broadcast TV, the voltage encoding for 20% higher-than-white is zero volts (high voltage is used for black). So, pure yellow would require negative voltage, an impossible voltage to broadcast. Clamping the signal causes all the same problems you get with clipping when recording loud sounds. The signal usually gets terribly distorted, and it's not worth doing.

We're not trying to broadcast our graphics, so our bright yellows and pure reds might actually make it to the television, but TVs were never designed to handle those signals so it is unlikely that they will handle them well. These highly saturated colors tend to throb and pulsate. If the signal is clipped to the broadcast limits, then the signal will be distorted and nearby pixels may show unpredictable problems. Highly saturated colors also increase problems with chroma and luminance cross talk by making it harder to cleanly separate chroma and luminance.


The worst-case RGB combinations, known as "hot" or "illegal" colors are:

     1, 1, 0 - yellow
     0, 1, 1 - cyan
     1, 0, 0 - red
     0, 0, 1 - blue

You can fix red by reducing the luminance, but you have to reduce it to 0.60 to get it just barely into legal range. This changes the color a lot.

Alternately you can fix red by reducing the saturation, while holding the luminance constant. This means subtracting 0.15 from the red and adding about 0.06 to blue and green. This changes the color much less.

Finally, you can fix red by just adding 0.10 to green and blue. There is no mathematical justification for this. Instead, you're actually increasing the brightness, but it looks very good. It fixes the problem because increasing the green and blue lowers the saturation more than it increases the brightness. Similar fixes work for the other problematic colors.

The saturation fix seems to be the best programmatic solution, but an artist's touch can give even better results.

The following code is a simple function for detecting illegal NTSC colors:

#include <math.h>
#include <assert.h>

const double pi = 3.14159265358979323846;
const double cos33 = cos(33 * pi / 180);
const double sin33 = sin(33 * pi / 180);

// Specify the maximum amount you are willing to go
// above 1.0 and below 0.0. This should be set no
// higher than 0.2. 0.1 may produce better results.
// This value assumes RGB values from 0.0 to 1.0.
const double k_maxExcursion = 0.2;

// Returns zero if all is well. If the signal will go too high it returns how
// high it will go. If the signal will go too low it returns how low it
// will go - a negative number.
// It is not possible for the composite signal to simultaneously
// go off the top and bottom of the chart.
// This code is non-optimized, for clarity.
double CalcOverheatAmount(double r, double g, double b)
{
    assert(r >= 0.0 && r <= 1.0);
    assert(g >= 0.0 && g <= 1.0);
    assert(b >= 0.0 && b <= 1.0);
    // Convert from RGB to YUV space.
    double y = .299 * r + .587 * g + .114 * b;
    double u = 0.492 * (b - y);
    double v = 0.877 * (r - y);

    // Convert from YUV to YIQ space. This could be combined with
    // the RGB to YIQ conversion for better performance.
    double i = cos33 * v - sin33 * u;
    double q = sin33 * v + cos33 * u;

    // Calculate the amplitude of the chroma signal.
    double c = sqrt(i * i + q * q);
    // See if the composite signal will go too high or too low.
    double maxComposite = y + c;
    double minComposite = y - c;
    if (maxComposite > 1.0 + k_maxExcursion)
    return maxComposite;
    if (minComposite < -k_maxExcursion)
    return minComposite;
    return 0;
}

Once you have identified a hot color there are two easy programmatic ways to fix it - reduce the luminance, or reduce the saturation. Reducing the luminance is easy, just multiply R', G' and B' by the desired reduction ratio. Reducing chrominance is only slightly harder. You need to convert to YIQ (although YUV actually works just as well) then scale I and Q or U and V and then convert back to RGB (Martindale 91). The code snippets below will calculate and use the appropriate reduction factors to get the colors exactly into legal range.

// This epsilon value is necessary because the standard color space conversion
// factors are only accurate to three decimal places.
const double k_epsilon = 0.001;

void FixAmplitude(double& r, double& g, double& b)
{
    // maxComposite is slightly misnamed - it may be min or max.
    double maxComposite = CalcOverheatAmount(r, g, b);
    if (maxComposite != 0.0)
    {
        double coolant;
        if (maxComposite > 0)
        {
               // Calculate the ratio between our maximum composite
               // signal level and our maximum allowed level.
               coolant = (1.0 + k_maxExcursion - k_epsilon) / maxComposite;
        }
        else
        {
               // Calculate the ratio between our minimum composite
               // signal level and our minimum allowed level.
               coolant = (k_maxExcursion - k_epsilon) / -maxComposite;
        }
        // Scale R, G and B down. This will move the composite signal
        // proportionally closer to zero.
        r *= coolant;
        g *= coolant;
        b *= coolant;
    }
}

void FixSaturation(double& r, double& g, double& b)
{
    // maxComposite is slightly misnamed - it may be min
    // composite or maxComposite.
    double maxComposite = CalcOverheatAmount(r, g, b);
    if (maxComposite != 0.0)
    {
        // Convert into YIQ space, and convert c, the amplitude
        // of the chroma component.
        DoubleYIQ YIQ = DoubleRGB(r, g, b).toYIQ();
        double c = sqrt(YIQ.i * YIQ.i + YIQ.q * YIQ.q);
        double coolant;
        // Calculate the ratio between the maximum chroma range allowed
        // and the current chroma range.
        if (maxComposite > 0)
        {
                // The maximum chroma range is the maximum composite value
                // minus the luminance.
                coolant = (1.0 + k_maxExcursion - k_epsilon - YIQ.y) / c;
        }
        else
        {
                // The maximum chroma range is the luminance minus the
                // minimum composite value.
                coolant = (YIQ.y - -k_maxExcursion - k_epsilon) / c;
        }
        // Scale I and Q down, thus scaling chroma down and reducing the
        // saturation proportionally.
        YIQ.i *= coolant;
        YIQ.q *= coolant;
        DoubleRGB RGB = YIQ.toRGB();
        r = RGB.r;
        g = RGB.g;
        b = RGB.b;
    }
}

Complete code is available on my web site. Included is a sample app that lets you interactively adjust RGB and HSV (hue, saturation, value) values while viewing the corresponding composite waveform. The program also lets you invoke the FixAmplitude() and FixSaturation() functions, to compare their results.

It's worth noting that there are also YIQ colors that represent RGB colors outside of the unit RGB cube. Video editors who do some of their work in YIQ or YUV need to deal with this, but we don't have to.

Overall, it's best to avoid using colors that will go out of range. Many image-editing programs have a tool that will find and fix 'illegal' or 'hot' NTSC colors. If you want more control, a tool that finds and fixes illegal colors is available on my web site. One advantage of a home grown tool is that you can decide, based on your target console and artistic sensibilities, how close to the maximum you want to go, and how to fix colors when you go out of range.

I mentioned at the beginning that the prime symbols that I try to remember to use on R', G' and B' indicate that these calculations are supposed to be done on gamma corrected RGB values. Therefore, it seems reasonable to ask whether our RGB values are gamma corrected. The good news is, they usually are, and when they're not, it usually doesn't matter. Alternatively, to put it more brutally, the game industry has successfully ignored gamma for this long, why change now?

The reason our images are mostly gamma corrected is that TVs and computer monitors naturally do a translation from gamma corrected RGB to linear light (Poynton 98). This is a good thing because it means that the available RGB values are more evenly spread out according to what our eyes can see. Since we create our texture maps and RGB values by making them look good on a monitor, they are naturally gamma corrected (although some adjustments are needed because monitors and televisions may have different gamma values).

However, if we do a lot of alpha blending and antialiasing then we are doing non-gamma corrected pixel math. If we do a 50% blend between a white pixel and a black pixel, presumably we want the result to be a pixel that represents half the light density, or half as many photons per second. However, the answer that we typically get is 127, which does not represent half as many photons, because the frame buffer values don't represent linear light. The correct answer that you can verify by playing around with various sized black white dither patterns and comparing them to various grays, is typically around a 186 gray. It's not a huge error, and unless graphics hardware starts supporting gamma corrected alpha blending, there is nothing we can do.

______________________________________________________

Resolution Limitations


join | contact us | advertise | write | my profile
news | features | companies | jobs | resumes | education | product guide | projects | store



Copyright © 2003 CMP Media LLC

privacy policy
| terms of service