Some ideas:
int lightness = r + g + b;
int exp1 = lightness / 8;
int man1 = lightness % 8;
This sets lightness
to a value in the range 0..765,
exp1
to a value in the range 0..95,
and man1
to a value in the range 0..7.
int mantissa_bits = (1 << 23) + (man1 << 20) + (r << 8) + g;
This sets mantissa
to a non-negative integer less than 2^24,
but not less than 2^23.
float multiplier = pow(2, exp1 - 119);
This sets multiplier
to a power of 2 no less than 2^-119
and no greater than 2^-24.
float rgb_encoding = mantissa * multiplier;
This sets rgb_encoding
to a number less than 2^24 * 2^-24 = 1, but no less than 2^23 * 2^-119 = 2^-96.
The leading bit of mantissa_bits
, which is always 1 << 23
,
becomes the implicit most significant bit of the floating-point value
(which is not actually stored in the bits of rgb_encoding
)
while the next 23 bits of mantissa_bits
become the actual stored
mantissa of rgb_encoding
with no loss of information.
Now if you increase or decrease any of the values of r
, g
, or b
by 1, you change the value of rgb_encoding
by at least 6.6%.
The change in rgb_encoding
is very slightly larger if you change r
than if you change b
.
For example, if r
= 127, g
= 15, b
= 95, then
mantissa_bits
= 13664015 (0x00d07f0f hexadecimal).
Increasing b
by 1 changes mantissa_bits
to 14712591
(an increase of 1048576),
but increasing r
by 1 changes mantissa_bits
to 14712847
(an increase of 1048832).
To decode the RGB data from rgb_encoding
,
get its exponent using double frexp(double value, int *exp)
(as in this answer),
use the value of exp
to multiply rgb_encoding
by a power of 2 that
produces an integer value between 2^23 and 2^24,
cast it to an int
, extract bits 20 through 22 and add these to
8 * (exp + 119)
.
This gives the value of r + g + b
.
Get the values of r
and g
from the least significant 16 bits of the
integer, and subtract r + g
from r + g + b
to get b
.
The trick here is that we've stored just 23 bits of information in the
mantissa of rgb_encoding
, but almost 7 bits of information in
the 8-bit exponent of rgb_encoding
.
As a result, the encoded value represents the lightness of the original
RGB color in a non-linear way,
and darker colors map to very small values of rgb_encoding
, although still in the range of normal positive float
values.
We've made use of those 30 bits by storing the
completely symmetric information of r + g + b
in the 10 bits that have the greatest impact on the encoded value,
and the asymmetric information of r
and g
in the 16 bits that have the
least impact on the encoded value. We've also left 4 bits in the middle
at constant zeros in order to amplify the significance of the
symmetric information over the asymmetric information.