I want to recreate an accurate version of a 15-bit RGB color palette (PC Highcolor, SNES, Playstation 1), storing each color from the palette as a standard 6 digit hex code.
With each color component using 5 bits, this gives us available values of 0-31. Mapping that to a standard 0-255 color range needed for the hex code seems trivial, however when I do my palette seems to be subtly off from the examples I've found online.
When mapping the values I've tried both rounding and flooring to the closest match but neither quite match. My output when looping through the first few values of the red component gives me:
Rounding:
#000000, #080000, #100000, #190000, #210000, #290000, #310000, #3a0000, #420000...
Flooring:
#000000, #080000, #100000, #180000, #200000, #290000, #310000, #390000, #410000...
Whereas in the example palettes I've found I get:
#000000, #080000, #100000, #180000, #210000, #290000, #310000, #390000, #420000...
At first I considered that the example palette may have been wrong, or that the image compression used on the image may have effected the values, but it seems to be the same for all of the examples I've found.
While this might seem a trivial detail and the colors would likely be indistinguishable, I'm keen to understand if there was a hardware or software limitation on the original devices which causes these discrepancies when computing the original palette. I'm also wondering if it's actually a fence post problem in my code.
Update:
Here is the Javascript code I wrote to generate a color cube based on bit depth. rgbToHex
is just a util that does what it says on the tin :)
The differences posted above about round
and floor
refer to the int conversion done on the r, g, b values before passing to rgbToHex
. From the article cited in my answer below, I now believe the issue is to do with data loss during bit depth conversions and that while the code below produces a "perfect" linear interpolated color cube, this wasn't possible on the original hardware.
const MAX_RGB = 255;
const BITS_PER_CHANNEL = 5;
const numPosts = Math.pow(2, BITS_PER_CHANNEL);
const numSpaces = numPosts - 1;
let rIndex, gIndex, bIndex;
const colors = [];
for (bIndex = 0; bIndex < numPosts; bIndex++) {
for (gIndex = 0; gIndex < numPosts; gIndex++) {
for (rIndex = 0; rIndex < numPosts; rIndex++) {
const r = (rIndex / numSpaces) * MAX_RGB;
const g = (gIndex / numSpaces) * MAX_RGB;
const b = (bIndex / numSpaces) * MAX_RGB;
const color = rgbToHex(Math.round(r), Math.round(g), Math.round(b));
colors.push(color);
}
}
}
```