Pal animations

This article tries to explain how palette animations worked in the Fallout games. These special animations are based on PAL files in spite of the FRM-based "frame animations".

Conversion: palette -> RGB
+-+                                  | Brightness(currentGamma) (****) | +-+                                                   |                +--+                                   V                |  Palette     |                     ++         +-+                 +-+ |  from       |                     |    Color transformation    |         |   System    |                 | DirectDraw  | | PAL-file (*) |                    |          table (**)        |         |   palette   |                 |   palette   | +-++                    ++---+         +-+---+                 +-+---+ |     | Red    |>|  0 |  pow(0, currentGamma) |         |     | Red   |                 |     | Red   | |    ++                     ++---+         |     +---+                 |     +---+ |  0  | Green  |                     |  1 |  pow(1, currentGamma) |>|  0  | Green |                 |  0  | Green | |    ++                     ++---+         |     +---+                 |     +---+ |     | Blue   |                     |            ... |        |     | Blue  |                 |     | Blue  | +-++                    ++---+         +-+---+   Red   << 2    +-+---+ |    | Red    |               +>| 63 |  pow(63, currentGamma)|         |     | Red   |-- Green << 2 -->|     | Red   | |    ++               |     ++---+         |     +---+   Blue  << 2    |     +---+ | 1  | Green  |               |                                            |  1  | Green |                 |  1  | Green | |    ++               |                                            |     +---+                 |     +---+ |     | Blue   |               |                                            |     | Blue  |                 |     | Blue  | +-++              |                                            +-+---+                 +-+---+ |     ...      |               |                                            |     ...     |                 |     ...     | +-++                                                            +-+---+                 +-+---+ |     | Red    |           Red   >> 2                                       |     | Red   |                 |     | Red   | |    ++           Green >> 2                                       |     +---+                 |     +---+ | 255 | Green |           Blue  >> 2                                       | 255 | Green |                 | 255 | Green | |    ++                                                            |     +---+                 |     +---+ |     | Blue   |               |                                            |     | Blue  |                 |     | Blue  | +-++              |                                            +-+---+                 +-+---+                                |                                |                                | +---+  | |   Animated colors (***)   |--+ +---+


 * * - If at the stage of PAL-file loading the value of one of color's components is not in the range between 0 .. 63, the value of all components of this color are to 0.
 * ** - If the result of the function turns out to be smaller than 0, the value of the corresponding element of the table is set to 0. If the result of the function turns out bigger than 63, the value of the corresponding element of the table set to 63. (pow - power function)
 * *** - Details in the animated colors section
 * **** - In Fallout the variable currentGamma has the double type and can have a range from 1.000000 up to 1.179993.

It's important to know that the used algorithm of brightness adjusting isn't fully working: the 0th and 1st element of the color transformation table never change.

Conversion: RGB -> palette index
The conversion is based on the following scheme:

Red               Green                 Blue +-|-+   +-|-+    +-|-+ |7|6|5|4|3|2|1|0|    |7|6|5|4|3|2|1|0|    |7|6|5|4|3|2|1|0| +-|-+    +-|-+    +-|-+      |                    |                    |      |                    |                    |      +-+              |              +-+            |              |              |            V              V              V  +-|--|--|--+ |0|R7|R6|R5|R4|R3|G7|G6|G5|G4|G3|B7|B6|B5|B4|B3| +-|--|--|--+

The resulting value is used as a transformation table index that is read from the corresponding PAL-file. Value of the table on corresponding index is a required index into palette. (I've absolutely got no clue what the last sentence means.)

Animated colors
In the Fallout games "animated colors" have been used to reduce the filesize of graphics. It allows you to use one frame with changing colors, instead of the several frames with static colors.

The user can control the "animated colors" with two parameters in fallout2.cfg and mapper2.cfg: [system] color_cycling=1 cycle_speed_factor=1


 * color_cycling - (1) / (0) enables / disables palette animations.
 * cycle_speed_factor - Adjusts the animation speed; the larger the value the slower the animation speed.

Animated color groups


There are six groups of "animated colors" with different initial parameters: (change time for cycle_speed_factor=1)

Example function
The algorithm of color changing is explained by this function: BYTE g_Palette[768];

// ÃÂÃÂ°Ã‘Â‡ÃÂ°ÃÂ»Ã‘ÂŒÃÂ½ÃÂ¾ÃÂµ ÃÂ·ÃÂ½ÃÂ°Ã‘Â‡ÃÂµÃÂ½ÃÂ¸ÃÂµ Ã‘Â†ÃÂ²ÃÂµÃ‘Â‚ÃÂ° BYTE g_nSlime[] = { 0, 108, 0, 11, 115, 7, 27, 123, 15, 43, 131, 27 };                            // Slime BYTE g_nMonitors[] = { 107, 107, 111, 99, 103, 127, 87, 107, 143, 0, 147, 163, 107, 187, 255 };   // Monitors BYTE g_nFireSlow[] = { 255, 0, 0, 215, 0, 0, 147, 43, 11, 255, 119, 0, 255, 59, 0 };             // Slow fire BYTE g_nFireFast[] = { 71, 0, 0, 123, 0, 0, 179, 0, 0, 123, 0, 0, 71, 0, 0 };                     // Fast fire BYTE g_nShoreline[] = { 83, 63, 43, 75, 59, 43, 67, 55, 39, 63, 51, 39, 55, 47, 35, 51, 43, 35 }; // Shoreline int g_nBlinkingRed = -4*4;                                                                       // Alarm

// Current parameters of cycle DWORD g_dwSlimeCurrent = 0; DWORD g_dwMonitorsCurrent = 0; DWORD g_dwFireSlowCurrent = 0; DWORD g_dwFireFastCurrent = 0; DWORD g_dwShorelineCurrent = 0; BYTE g_nBlinkingRedCurrent = 0;

// Time of Last changinge DWORD g_dwLastCycleSlow = 0; DWORD g_dwLastCycleMedium = 0; DWORD g_dwLastCycleFast = 0; DWORD g_dwLastCycleVeryFast = 0;

// Current speed factor DWORD g_dwCycleSpeedFactor = 1;

void AnimatePalette {   BOOL bPaletteChanged = FALSE; DWORD dwCurrentTime = GetTickCount;

if (dwCurrentTime - g_dwLastCycleSlow >= 200 * g_dwCycleSpeedFactor) { // Slime DWORD dwSlimeCurrentWork = g_dwSlimeCurrent;

for(int i = 3; i >= 0; i--) { g_Palette[687 + i * 3] = g_nSlime[dwSlimeCurrentWork * 3] >> 2;                      // Red g_Palette[687 + i * 3 + 1] = g_nSlime[dwSlimeCurrentWork * 3 + 1] >> 2;              // Green g_Palette[687 + i * 3 + 2] = g_nSlime[dwSlimeCurrentWork * 3 + 2] >> 2;              // Blue

if (dwSlimeCurrentWork == 3) dwSlimeCurrentWork = 0; else dwSlimeCurrentWork++; }

if (g_dwSlimeCurrent == 3) g_dwSlimeCurrent = 0; else g_dwSlimeCurrent++;

// Shoreline DWORD dwShorelineCurrentWork = g_dwShorelineCurrent;

for(int i = 5; i >= 0; i--) { g_Palette[744 + i * 3] = g_nShoreline[dwShorelineCurrentWork * 3] >> 2;              // Red g_Palette[744 + i * 3 + 1] = g_nShoreline[dwShorelineCurrentWork * 3 + 1] >> 2;      // Green g_Palette[744 + i * 3 + 2] = g_nShoreline[dwShorelineCurrentWork * 3 + 2] >> 2;      // Blue

if (dwShorelineCurrentWork == 5) dwShorelineCurrentWork = 0; else dwShorelineCurrentWork++; }

if (g_dwShorelineCurrent == 5) g_dwShorelineCurrent = 0; else g_dwShorelineCurrent++;

// Fire_slow DWORD dwFireSlowCurrentWork = g_dwFireSlowCurrent;

for(int i = 4; i >= 0; i--) { g_Palette[714 + i * 3] = g_nFireSlow[dwFireSlowCurrentWork * 3] >> 2;                // Red g_Palette[714 + i * 3 + 1] = g_nFireSlow[dwFireSlowCurrentWork * 3 + 1] >> 2;        // Green g_Palette[714 + i * 3 + 2] = g_nFireSlow[dwFireSlowCurrentWork * 3 + 2] >> 2;        // Blue

if (dwFireSlowCurrentWork == 4) dwFireSlowCurrentWork = 0; else dwFireSlowCurrentWork++; }

if (g_dwFireSlowCurrent == 4) g_dwFireSlowCurrent = 0; else g_dwFireSlowCurrent++;

g_dwLastCycleSlow = dwCurrentTime; bPaletteChanged = TRUE; }

dwCurrentTime = GetTickCount;

if (dwCurrentTime - g_dwLastCycleMedium >= 142 * g_dwCycleSpeedFactor) { // Fire_fast DWORD dwFireFastCurrentWork = g_dwFireFastCurrent;

for(int i = 4; i >= 0; i--) { g_Palette[729 + i * 3] = g_nFireFast[dwFireFastCurrentWork * 3] >> 2;                // Red g_Palette[729 + i * 3 + 1] = g_nFireFast[dwFireFastCurrentWork * 3 + 1] >> 2;        // Green g_Palette[729 + i * 3 + 2] = g_nFireFast[dwFireFastCurrentWork * 3 + 2] >> 2;        // Blue

if (dwFireFastCurrentWork == 4) dwFireFastCurrentWork = 0; else dwFireFastCurrentWork++; }

if (g_dwFireFastCurrent == 4) g_dwFireFastCurrent = 0; else g_dwFireFastCurrent++;

g_dwLastCycleMedium = dwCurrentTime; bPaletteChanged = TRUE; }

dwCurrentTime = GetTickCount;

if (dwCurrentTime - g_dwLastCycleFast >= 100 * g_dwCycleSpeedFactor) { // Monitors DWORD dwMonitorsCurrentWork = g_dwMonitorsCurrent;

for(int i = 4; i >= 0; i--) { g_Palette[699 + i * 3] = g_nMonitors[dwMonitorsCurrentWork * 3] >> 2;                // Red g_Palette[699 + i * 3 + 1] = g_nMonitors[dwMonitorsCurrentWork * 3 + 1] >> 2;        // Green g_Palette[699 + i * 3 + 2] = g_nMonitors[dwMonitorsCurrentWork * 3 + 2] >> 2;        // Blue

if (dwMonitorsCurrentWork == 4) dwMonitorsCurrentWork = 0; else dwMonitorsCurrentWork++; }

if (g_dwMonitorsCurrent == 4) g_dwMonitorsCurrent = 0; else g_dwMonitorsCurrent++;

g_dwLastCycleFast = dwCurrentTime; bPaletteChanged = TRUE; }

dwCurrentTime = GetTickCount;

if (dwCurrentTime - g_dwLastCycleVeryFast >= 33 * g_dwCycleSpeedFactor) { // Blinking red if ((g_nBlinkingRedCurrent == 0) ||(g_nBlinkingRedCurrent == 60*4)) g_nBlinkingRed = -g_nBlinkingRed;

g_Palette[762] = g_nBlinkingRedCurrent;                                                  // Red g_Palette[763] = 0;                                                                      // Green g_Palette[764] = 0;                                                                      // Blue

g_nBlinkingRedCurrent = g_nBlinkingRed + g_nBlinkingRedCurrent;

g_dwLastCycleVeryFast = dwCurrentTime; bPaletteChanged = TRUE; }

if (bPaletteChanged) UpdatePalette; } Javascript: function animatepalette(paletteOffset, colors, timeInterval) { setInterval( => {       colors.push(colors.shift)        palette.splice(paletteOffset, colors.length, ...colors);        updatepalette;    }, timeInterval); }

function animatepaletteRed(paletteOffset, timeInterval) { let red = 0 let increment = -4;

setInterval( => {       if (red === 0 || red === 60) {            increment = -increment;        }

red += increment;

palette[paletteOffset] = [ red, 0,           0        ];

updatepalette; }, timeInterval); }

let slimeColors = [ [0, 108, 0],   [11, 115, 7],    [27, 123, 15],    [43, 131, 27] ]; // Slime 229 let monitors = [ [107, 107, 111],   [99, 103, 127],    [87, 107, 143],    [0, 147, 163],    [107, 187, 255] ]; // Monitors 233 let fireSlow = [ [255, 0, 0],   [215, 0, 0],    [147, 43, 11],    [255, 119, 0],    [255, 59, 0] ]; // Slow fire 238 let fireFast = [ [71, 0, 0],   [123, 0, 0],    [179, 0, 0],    [123, 0, 0],    [71, 0, 0] ]; // Fast fire 243 let shoreline = [ [83, 63, 43],   [75, 59, 43],    [67, 55, 39],    [63, 51, 39],    [55, 47, 35],    [51, 43, 35] ]; // Shoreline 248

animatepalette(229, slimeColors, 200) animatepalette(238, fireSlow, 200) animatepalette(248, shoreline, 200) animatepalette(243, fireFast, 142) animatepalette(233, monitors, 100)

animatepaletteRed(254, 33)