Vault-Tec Labs
Advertisement

Introduction[]

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".

The whole article is still WIP until this remark is removed in the future. It is based on this article from TeamX: http://members.fifengine.de/docs/pal_processing.html

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 correspoding 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[]

Palette Fallout 2

https://shahovkit.github.io/Frame-Animator-Online/palette.html

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

Name Number of elements Palette index Value of color components Change time
Slime 4 229..232 0 Red 0 200 ms
Green 108
Blue 0
1 Red 11
Green 115
Blue 7
2 Red 27
Green 123
Blue 15
3 Red 43
Green 131
Blue 27
Shoreline 6 248..253 0 Red 83 200 ms
Green 63
Blue 43
1 Red 75
Green 59
Blue 43
2 Red 67
Green 55
Blue 39
3 Red 63
Green 51
Blue 39
4 Red 55
Green 47
Blue 35
5 Red 51
Green 43
Blue 35
Slow fire 5 238..242 0 Red 255 200 ms
Green 0
Blue 0
1 Red 215
Green 0
Blue 0
2 Red 147
Green 43
Blue 11
3 Red 255
Green 119
Blue 0
4 Red 255
Green 59
Blue 0
Fast fire 5 243..247 0 Red 71 142 ms
Green 0
Blue 0
1 Red 123
Green 0
Blue 0
2 Red 179
Green 0
Blue 0
3 Red 123
Green 0
Blue 0
4 Red 71
Green 0
Blue 0
Monitors 5 233..237 0 Red 107 100 ms
Green 107
Blue 111
1 Red 99
Green 103
Blue 127
2 Red 87
Green 107
Blue 143
3 Red 0
Green 147
Blue 163
4 Red 107
Green 187
Blue 255
Alarm 1 254 0 Red 252 33 ms
Green 0
Blue 0

Example function[]

The algorithm of color changing is explained by this function:

C:

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)

Credits[]

  • Author: Anchorite
  • E-mail: anchorite2001@yandex.ru
Advertisement