NTSC video: Difference between revisions
(+luma to chroma leaking) |
(+color tuning information) |
||
Line 176: | Line 176: | ||
Because also the frame length is uneven (neither 262*341*8 nor (262*341-1)*8 is an even multiple of 12), the color mixing also changes a little every frame. When rendering is normally enabled, the screen is alternatingly 89342 and 89341 cycles long. The combination of these (89342+89341)*8 <i>is</i> an even multiple of 12, which means that the artifact pattern cycles every 2 frames. The pattern of cycling can be changed by disabling rendering during the end of the pre-render scanline; it forces the screen length to 89342 cycles, even if would be 89341 otherwise. | Because also the frame length is uneven (neither 262*341*8 nor (262*341-1)*8 is an even multiple of 12), the color mixing also changes a little every frame. When rendering is normally enabled, the screen is alternatingly 89342 and 89341 cycles long. The combination of these (89342+89341)*8 <i>is</i> an even multiple of 12, which means that the artifact pattern cycles every 2 frames. The pattern of cycling can be changed by disabling rendering during the end of the pre-render scanline; it forces the screen length to 89342 cycles, even if would be 89341 otherwise. | ||
The process of decoding NTSC signal (convert it into RGB) is subject to a lot of study, and there are many patents and different techniques for it. A simple method suitable for emulation is covered below. It is not accurate, because in reality the chroma is blurred much more than is done here (the region of signal sampled for I and Q is wider than 12 samples), but it already produces a quite authentic looking picture. In addition, the border region (total of 26 pixels of background color around the 256-pixel scanline) is not sampled. | The process of decoding NTSC signal (convert it into RGB) is subject to a lot of study, and there are many patents and different techniques for it. A simple method suitable for emulation is covered below. It is not accurate, because in reality the chroma is blurred much more than is done here (the region of signal sampled for I and Q is wider than 12 samples), and the filter used here is a simple box FIR filter rather than an IIR filter, but it already produces a quite authentic looking picture. In addition, the border region (total of 26 pixels of background color around the 256-pixel scanline) is not sampled. | ||
<nowiki> | <nowiki> | ||
float signal_levels[256*8] = {...}; // Eight signal levels for each pixel, normalized to 0..1 range. Calculated as above. | float signal_levels[256*8] = {...}; // Eight signal levels for each pixel, normalized to 0..1 range. Calculated as above. | ||
unsigned Width; // Input: Screen width. Can be not only 256, but anything up to 2048. | unsigned Width; // Input: Screen width. Can be not only 256, but anything up to 2048. | ||
float phase; // Input: This should the value that was PPU_cycle_counter * 8 | |||
// at the BEGINNING of this scanline. | // at the BEGINNING of this scanline. | ||
// It can additionally include a floating-point hue offset. | // It can additionally include a floating-point hue offset. | ||
Line 201: | Line 201: | ||
}</nowiki> | }</nowiki> | ||
The NTSC decoder here produces pixels in YIQ color space. The YIQ colors can be converted into sRGB colors with the following formula, using the FCC-sanctioned YIQ-to-RGB conversion matrix: | The NTSC decoder here produces pixels in YIQ color space. | ||
If you want more saturated colors, just multiply <code>i</code> and <code>q</code> with a factor of your choosing, such as 1.7. If you want brighter colors, just multiply <code>y</code>, <code>i</code> and <code>q</code> with a factor of your choosing, such as 1.1. If you want to adjust the hue, just add or subtract a value from/to <code>phase</code>. | |||
The YIQ colors can be converted into sRGB colors with the following formula, using the FCC-sanctioned YIQ-to-RGB conversion matrix. This produces a value that can be saved to e.g. framebuffer: | |||
<nowiki> | <nowiki> | ||
float gamma = | float gamma = 2.0f; // Assumed display gamma | ||
auto gammafix = [=](float f) { return f <= 0.f ? 0.f : pow(f, 2.2f / gamma); }; | auto gammafix = [=](float f) { return f <= 0.f ? 0.f : pow(f, 2.2f / gamma); }; | ||
auto clamp = [](int v) { return v>255 ? 255 : v; }; | auto clamp = [](int v) { return v>255 ? 255 : v; }; | ||
Line 211: | Line 215: | ||
+ 0x00100*clamp(255.95 * gammafix(y + -0.274788f*i + -0.635691f*q)) | + 0x00100*clamp(255.95 * gammafix(y + -0.274788f*i + -0.635691f*q)) | ||
+ 0x00001*clamp(255.95 * gammafix(y + -1.108545f*i + 1.709007f*q));</nowiki> | + 0x00001*clamp(255.95 * gammafix(y + -1.108545f*i + 1.709007f*q));</nowiki> | ||
==Interactive Demo== | ==Interactive Demo== |
Revision as of 14:25, 8 November 2011
Note: This data is preliminary and still being reviewed.
Basics
Master clock is 21.47727273 MHz. Each PPU pixel lasts four clocks. $xy refers to a palette color in the range $00 to $3F.
Scanline Timing
Values in PPU pixels (341 total per scanline).
sync | 25 |
black | 4 |
colorburst | 15 |
black | 5 |
pulse | 1 |
left border (background color) | 15 |
active | 256 |
right border (background color) | 11 |
black | 9 |
Brightness Levels
Voltage levels used by the PPU are as follows - absolute, relative to synch, and normalized between black level and white:
Type | Absolute | Relative | Normalized |
Synch | 0.781 | 0.000 | -0.359 |
Colorburst L | 1.000 | 0.218 | -0.208 |
Colorburst H | 1.712 | 0.931 | 0.286 |
Color 0D | 1.131 | 0.350 | -0.117 |
Color 1D (black) | 1.300 | 0.518 | 0.000 |
Color 2D | 1.743 | 0.962 | 0.308 |
Color 3D | 2.331 | 1.550 | 0.715 |
Color 00 | 1.875 | 1.090 | 0.397 |
Color 10 | 2.287 | 1.500 | 0.681 |
Color 20 | 2.743 | 1.960 | 1.000 |
Color 30 | 2.743 | 1.960 | 1.000 |
$xE/$xF output the same voltage as $1D. $x1-$xC output a square wave alternating between levels for $xD and $x0. Colors $20 and $30 are exactly the same.
Color Phases
111111------ 22222------2 3333------33 444------444 55------5555 6------66666 ------777777 -----888888- ----999999-- ---AAAAAA--- --BBBBBB---- -CCCCCC-----
The color generator is clocked by the rising and falling edges of the ~21.48 MHz clock, resulting in an effective ~42.95 MHz clock rate. There are 12 color square waves, spaced at regular phases. Each runs at the ~3.58 MHz colorburst rate. Color $xY uses the wave shown in row Y from the table. Color burst uses color phase 8 (with voltages listed above).
Color Tint Bits
There are three color modulation channels controlled by the top three bits of $2001. Each channel uses one of the color square waves (see above diagram) and enables attenuation of the video signal when the color square wave is high. A single attenuator is shared by all channels.
$2001 | Active phase | Complement |
---|---|---|
Bit 7 | Color 8 | Color 2 (blue) |
Bit 6 | Color 4 | Color A (green) |
Bit 5 | Color C | Color 6 (red) |
When signal attenuation is enabled by one or more of the channels and the current pixel is a color other than $xE/$xF (black), the signal is attenuated as follows (calculations given for both relative and absolute values as shown in the voltage table above):
relative = relative * 0.746
normalized = normalized * 0.746 - 0.0912
For example, when $2001 bit 6 is true, the attenuator will be active during the phases of color 4. This means the attenuator is not active during its complement (color A), and the screen appears to have a tint of color A, which is green.
Example Waveform
This waveform steps through various grays and then stops on a color.
1.0 +--+ 0.9 | | 0.8 | | 0.7 +--+ | +-+ +-+ 0.6 | | | | | | 0.5 | | | | | | 0.4 +--+ | | | | | 0.3 +--+ | | | | | 0.2 | | | | | | 0.1 | | | | | | 0.0 . +--+ . . . . . +-+ +-+ + . . -0.1 --+ 0D 0F 2D 00 10 30 11
The PPU's shortcut method of NTSC modulation often produces artifacts in which vertical lines appear slightly ragged, as the chroma spills over into luma.
Emulating in C++ code
Calculating the momentary NTSC signal level can be done as follows in C++:
// Voltage levels, relative to synch voltage static const float black=.518f, white=1.962f, attenuation=.746f, levels[8] = {.350f, .518f, .962f,1.550f, // Signal low 1.094f,1.506f,1.962f,1.962f}; // Signal high // Input variables: int pixel; // Pixel color (9-bit) given as input. Bitmask format: "eeellcccc". int phase; // Signal phase (0..11). It is a variable that increases by 8 each pixel. // Decode the NES color. int color = (pixel & 0x0F); // 0..15 "cccc" int level = (pixel >> 4) & 3; // 0..3 "ll" int emphasis = (pixel >> 6); // 0..7 "eee" if(color > 13) { level = 1; } // For colors 14..15, level 1 is forced. // The square wave for this color alternates between these two colors: float low = levels[0 + level]; float high = levels[4 + level]; if(color == 0) { low = high; } // For color 0, only high level is emitted if(color > 12) { high = low; } // For colors 13..15, only low level is emitted // Generate the square wave auto InColorPhase = [=](int color) { return (color + phase) % 12 < 6; }; // Inline function float level = InColorPhase(color) ? high : low; // When de-emphasis bits are set, some parts of the signal are attenuated: if( (emphasis & 1) && InColorPhase(0) || (emphasis & 2) && InColorPhase(4) || (emphasis & 4) && InColorPhase(8) ) level = level * attenuation;
The process of generating NTSC signal for a single pixel can be simulated with the following C++ code:
int phase = PPU_cycle_counter * 8; for(int p=0; p<8; ++p) // Each pixel produces distinct 8 samples of NTSC signal. { float level = ...; // Calculated as above // Optionally apply some lowpass-filtering to the signal here. // Optionally normalize the signal to 0..1 range: level = (level-black) / (white-black); save_signal_level(level); // Send the signal to the decoder (which produces visual). phase = phase + 1; }
It is important to note that while the NES only generates eight (8) samples of NTSC signal per pixel, the wavelength for chroma is 12 samples long. This means that the colors of adjacent pixels get mandatorily mixed up to some degree. For the same reason, narrow black&white details can be interpreted as colors.
Because the scanline length is uneven (neither 341*8 nor 340*8 is an even multiple of 12), the color mixing shifts a little each scanline. This appears visually as a sawtooth effect at the edges of colors at high resolution. The sawtooth cycles every 3 scanlines.
Because also the frame length is uneven (neither 262*341*8 nor (262*341-1)*8 is an even multiple of 12), the color mixing also changes a little every frame. When rendering is normally enabled, the screen is alternatingly 89342 and 89341 cycles long. The combination of these (89342+89341)*8 is an even multiple of 12, which means that the artifact pattern cycles every 2 frames. The pattern of cycling can be changed by disabling rendering during the end of the pre-render scanline; it forces the screen length to 89342 cycles, even if would be 89341 otherwise.
The process of decoding NTSC signal (convert it into RGB) is subject to a lot of study, and there are many patents and different techniques for it. A simple method suitable for emulation is covered below. It is not accurate, because in reality the chroma is blurred much more than is done here (the region of signal sampled for I and Q is wider than 12 samples), and the filter used here is a simple box FIR filter rather than an IIR filter, but it already produces a quite authentic looking picture. In addition, the border region (total of 26 pixels of background color around the 256-pixel scanline) is not sampled.
float signal_levels[256*8] = {...}; // Eight signal levels for each pixel, normalized to 0..1 range. Calculated as above. unsigned Width; // Input: Screen width. Can be not only 256, but anything up to 2048. float phase; // Input: This should the value that was PPU_cycle_counter * 8 // at the BEGINNING of this scanline. // It can additionally include a floating-point hue offset. for(unsigned x = 0; x < Width; ++x) { // Determine the region of scanline signal to sample. Take 12 samples. int center = x * (256*8) / Width + 4; int begin = center - 6; if(begin < 0) begin = 0; int end = center + 6; if(end > 256*8) end = 256*8; float y = 0.f, i = 0.f, q = 0.f; // Calculate the color in YIQ. for(int p = begin; p < end; ++p) // Collect and accumulate samples { float level = signal_levels[p] / 12.f; y = y + level; i = i + level * cos( M_PI * (phase+p) / 6 ); q = q + level * sin( M_PI * (phase+p) / 6 ); } render_pixel(y,i,q); // Send the YIQ color for rendering. }
The NTSC decoder here produces pixels in YIQ color space.
If you want more saturated colors, just multiply i
and q
with a factor of your choosing, such as 1.7. If you want brighter colors, just multiply y
, i
and q
with a factor of your choosing, such as 1.1. If you want to adjust the hue, just add or subtract a value from/to phase
.
The YIQ colors can be converted into sRGB colors with the following formula, using the FCC-sanctioned YIQ-to-RGB conversion matrix. This produces a value that can be saved to e.g. framebuffer:
float gamma = 2.0f; // Assumed display gamma auto gammafix = [=](float f) { return f <= 0.f ? 0.f : pow(f, 2.2f / gamma); }; auto clamp = [](int v) { return v>255 ? 255 : v; }; unsigned rgb = 0x10000*clamp(255.95 * gammafix(y + 0.946882f*i + 0.623557f*q)) + 0x00100*clamp(255.95 * gammafix(y + -0.274788f*i + -0.635691f*q)) + 0x00001*clamp(255.95 * gammafix(y + -1.108545f*i + 1.709007f*q));
Interactive Demo
The following C source code implements the above described algorithm and displays it on screen with interactive mouse control of phase using SDL.
- nes_ntsc_waveform.c
- Windows executable (requires SDL.dll)
- nes_ntsc_palette.c - a sample NTSC palette generator program