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