Closed ISSOtm closed 10 months ago
rgba.cpp credits its color curve to https://github.com/LIJI32/SameBoy/commit/65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c. SameBoy's color correction is designed to make PC colors appear like GBC hardware ones, with less contrast and brightness. RGBGFX would want to do the reverse, and exaggerate the colors so playing on real hardware would look like the designer's PC mockups.
To do: fill in the gaps in this reversed mapping:
static std::array<uint8_t, 256> reverse_curve{
0, __, 1, __, 2, __, __, 3, __, __, __, __, 4, __, __, __, // These
__, __, 5, __, __, __, __, __, __, 6, __, __, __, __, __, __, // comments
__, __, 7, __, __, __, __, __, __, __, 8, __, __, __, __, __, // prevent
__, __, __, __, 9, __, __, __, __, __, __, __, __, __, 10, __, // clang-format
__, __, __, __, __, __, __, __, __, 11, __, __, __, __, __, __, // from
__, __, __, __, __, 12, __, __, __, __, __, __, __, __, __, __, // reflowing
__, 13, __, __, __, __, __, __, __, __, __, __, __, 14, __, __, // these
__, __, __, __, __, __, __, __, __, 15, __, __, __, __, __, __, // sixteen
__, __, __, __, __, __, 16, __, __, __, __, __, __, __, __, __, // 16-item
__, __, 17, __, __, __, __, __, __, __, __, __, __, __, 18, __, // lines,
__, __, __, __, __, __, __, __, __, __, 19, __, __, __, __, __, // which,
__, __, __, __, __, __, 20, __, __, __, __, __, __, __, __, __, // in
__, 21, __, __, __, __, __, __, __, __, __, 22, __, __, __, __, // my
__, __, __, __, __, 23, __, __, __, __, __, __, __, 24, __, __, // opinion,
__, __, __, __, __, __, 25, __, __, __, __, __, __, 26, __, __, // help
__, __, __, 27, __, __, __, __, 28, __, __, 29, __, 30, __, 31, // visualization!
};
Is the curve there identical to that in SameBoy?
Is the curve there identical to that in SameBoy?
Yes.
SameBoy has a lot of colour correction settings, but I think we should stick to GB_COLOR_CORRECTION_CORRECT_CURVES
(which, I believe, corresponds to what @LIJI32 calls "Harsh Reality"). Then comes the question of which curve we want to stick to; I'd favour the "default" one over SGB or AGB.
I was slightly lazy and linearly interpolated between those points:
static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, // These
3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, // comments
5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, // prevent
6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, // clang-format
8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, // from
9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, // reflowing
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, // these
12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, // sixteen
13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, // 16-item
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, // lines,
16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, // which
17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, // in my
19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, // opinion,
21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, // improves
23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, // the
25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 29, 29, 30, 30, 31, // readability!
};
Just by looking at the data, I can't recognize the function itself, but the second derivative is small enough that a linear interpolation shouldn't be too far off.
I got a polynomial regression of -0.0134x³ + 0.5484x² + 4.2072x - 5.3585 to match with R² = 0.9998. I don't know if that helps.
I was slightly lazy and linearly interpolated between those points:
That curve is still the reverse of what we need. It should be identical to the reverse_curve
above, but with the __
s filled in.
I generated one with NumPy; here's the code:
This is the completely interpolated array:
static std::array<uint8_t, 256> reverse_curve{
0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, // These
4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, // comments
7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, // prevent
8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, // clang-format
10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, // from
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, // reflowing
13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, // these
14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, // sixteen
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, // 16-item
17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, // lines,
18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // which,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, // in
21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, // my
23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, // opinion,
25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 27, // help
27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31, 31, // visualization!
};
Hmm, that's based on an old curve; https://github.com/LIJI32/SameBoy/blob/master/Core/display.c#L257 is different.
When I use those values for xs
in the above curve.py script, I get this reverse curve:
static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, // These
2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, // comments
4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, // prevent
6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, // clang-format
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, // from
9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, // reflowing
10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, // these
12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, // sixteen
13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, // 16-item
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, // lines,
16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, // which,
17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, // in
19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, // my
21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, // opinion,
23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, // help
26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 30, 30, 31, // visualization!
};
I've updated #1241 to use this data.
If that's still desaturated, then swapping xs
and ys
gives this:
static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, // These
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, // comments
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, // prevent
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, // clang-format
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, // from
10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, // reflowing
13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, // these
16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, // sixteen
19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 22, // 16-item
22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, // lines,
24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, // which,
26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, // in
28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, // my
29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, // opinion,
31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, // help
31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, // visualization!
};
Testing all the possible colors:
Without a reverse curve:
With the first curve:
With the second curve:
Code:
On that basis I've updated the PR to use rev2
.
Hmm, so SameBoy's "Harsh reality" color correction, which is presumably the closest to real GBC hardware, does more than just map through that curve array. Here's the reduced code for CGB with GB_COLOR_CORRECTION_LOW_CONTRAST
:
static inline uint8_t scale_channel_with_curve(uint8_t x)
{
return inline_const(uint8_t[], {0,6,12,20,28,36,45,56,66,76,88,100,113,125,137,149,161,172,182,192,202,210,218,225,232,238,243,247,250,252,254,255})[x];
}
// given r,g,b in 0-31 range, update their values to be harshly realistic
r = scale_channel_with_curve(r);
g = scale_channel_with_curve(g);
b = scale_channel_with_curve(b);
if (g != b) {
g = round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
}
uint8_t new_r = r * 15 / 16 + ( g + b) / 32;
uint8_t new_g = g * 15 / 16 + (r + b) / 32;
uint8_t new_b = b * 15 / 16 + (r + g ) / 32;
r = new_r;
g = new_g;
b = new_b;
r = r * (162 - 45) / 255 + 45;
g = g * (167 - 41) / 255 + 41;
b = b * (157 - 38) / 255 + 38;
r >>= 3;
g >>= 3;
b >>= 3;
Maybe we should figure out a more complex reverse transformation than just a per-channel lookup table.
This will print out all 32,768 raw->scaled colors. Ideally we'd want to reverse-map every color on the right to its original on the left.
#include <stdio.h>
#include <math.h>
int main() {
static int scale_channel_with_curve[32] = {
0,6,12,20,28,36,45,56,66,76,88,100,113,125,137,149,161,172,
182,192,202,210,218,225,232,238,243,247,250,252,254,255
};
for (int raw_r = 0; raw_r < 32; raw_r++) {
for (int raw_g = 0; raw_g < 32; raw_g++) {
for (int raw_b = 0; raw_b < 32; raw_b++) {
int scaled_r = scale_channel_with_curve[raw_r];
int scaled_g = scale_channel_with_curve[raw_g];
int scaled_b = scale_channel_with_curve[raw_b];
if (scaled_g != scaled_b) {
scaled_g = round(pow((pow(scaled_g / 255.0, 2.2) * 3 +
pow(scaled_b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
}
int new_r = scaled_r * 15 / 16 + (scaled_g + scaled_b) / 32;
int new_g = scaled_g * 15 / 16 + (scaled_r + scaled_b) / 32;
int new_b = scaled_b * 15 / 16 + (scaled_r + scaled_g) / 32;
new_r = new_r * (162 - 45) / 255 + 45;
new_g = new_g * (167 - 41) / 255 + 41;
new_b = new_b * (157 - 38) / 255 + 38;
new_r >>= 3;
new_g >>= 3;
new_b >>= 3;
printf("%02d,%02d,%02d -> %02d,%02d,%02d\n",
raw_r, raw_g, raw_b, new_r, new_g, new_b);
}
}
}
return 0;
}
These are the ranges of the harsh-reality-scaled values:
min R: [05,05,06,06,07,07,08,08,09,09,10,10,11,12,12,13,14,14,15,15,16,16,17,17,18,18,18,18,19,19,19,19]
max R: [06,06,07,07,07,08,08,09,09,10,11,11,12,13,13,14,15,15,16,16,17,17,18,18,18,19,19,19,19,20,20,20]
min G: [05,05,05,06,06,06,07,07,08,08,09,10,10,11,12,12,13,13,14,14,15,15,16,16,16,17,17,17,17,17,18,18]
max G: [13,13,13,13,13,14,14,14,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,19,20,20,20,20,20,20,20]
min B: [04,05,05,05,06,06,07,07,08,08,09,10,10,11,12,12,13,14,14,15,15,16,16,17,17,17,18,18,18,18,18,18]
max B: [05,05,06,06,07,07,08,08,09,09,10,11,11,12,13,13,14,15,15,16,16,17,17,17,18,18,18,19,19,19,19,19]
The red and blue channels could use lookup tables and only be off by +/- 1, but green needs more complex logic.
I honestly think it's better to use "Modern – Accurate" rather than Harsh Reality in this context. Reversing the "Harsh Reality" color transformation would cause way too much color clipping due to the harshly reduced contrast.
Okay, the GB_COLOR_CORRECTION_MODERN_ACCURATE
transformation looks easier to just invert, since it doesn't have the step where each channel gets lerped with 1/16th of the others. Although there's still the way that green depends on blue in the gamma-based formula. But if we ignore that, then it really is just the inverted scale_channel_with_curve
table which I've already implemented.
uint8_t r = (color) & 0x1F;
uint8_t g = (color >> 5) & 0x1F;
uint8_t b = (color >> 10) & 0x1F;
r = scale_channel_with_curve(r);
g = scale_channel_with_curve(g);
b = scale_channel_with_curve(b);
uint8_t new_r = r;
uint8_t new_g = g != b ? round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255) : g;
uint8_t new_b = b;
r = new_r;
g = new_g;
b = new_b;
Edit: inverting the gamma step:
g = pow((pow(new_g / 31, 2.2) * 4 - pow(new_b / 31, 2.2)) / 3, 1 / 2.2) * 31;
Picking up after #1200: @coffeevalenbat shared captures (which should be reposted here) of an image being converted with and without the setting, and enabling it tended to de-saturate the image, when the opposite was expected.