colour: add luminance scaling and XYZ/xyY/sRGB conversions

This commit is contained in:
Danny Robson 2021-05-12 13:26:52 +10:00
parent 7d3ae8bd8f
commit 15ee2f5f58
3 changed files with 170 additions and 1 deletions

View File

@ -3,13 +3,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2010-2017 Danny Robson <danny@nerdcruft.net>
* Copyright 2010-2021 Danny Robson <danny@nerdcruft.net>
*/
#include "colour.hpp"
#include "ascii.hpp"
#include "parse/value.hpp"
#include "debug/assert.hpp"
using cruft::colour;
using cruft::srgba;
///////////////////////////////////////////////////////////////////////////////
@ -48,4 +52,156 @@ cruft::srgba4f
cruft::parse::value<cruft::srgba4f> (cruft::view<const char*> &str)
{
return parse_hex (str);
}
///////////////////////////////////////////////////////////////////////////////
/// Invert the sRGB gamma function
static constexpr float
linearise (float const u)
{
if (u <= 0.04045f)
return u / 12.92f;
else
return std::pow ((u + 0.055f) / 1.055f, 2.4f);
}
///----------------------------------------------------------------------------
/// Apply the sRGB gamma function
static constexpr float
exponentiate (float const u)
{
if (u <= 0.0031308f)
return 12.92f * u;
else
return 1.055f * std::pow (u, 1/2.4f) - 0.055f;
}
///----------------------------------------------------------------------------
/// Convert an sRGB colour to XYZ.
///
/// Gamma will be removed during the conversion. Do not use with linear RGB.
static cruft::vector3f
sRGB_to_XYZ (cruft::vector3f sRGB)
{
auto const [r_, g_, b_] = sRGB;
float const r = linearise (r_);
float const g = linearise (g_);
float const b = linearise (b_);
// D65 whitepoint, 2° observer.
//
// We use higher precision values than the specification dictates, from
// Bruce Lindbloom's reference, so as to minimise numerical errors when
// chaining conversions.
float const X = r * 0.4124564f + g * 0.3575761f + b * 0.1804375f;
float const Y = r * 0.2126729f + g * 0.7151522f + b * 0.0721750f;
float const Z = r * 0.0193339f + g * 0.1191920f + b * 0.9503041f;
return {X, Y, Z};
}
///----------------------------------------------------------------------------
/// Convert an XYZ colour to sRGB.
///
/// Gamma will be applied during the conversion.
///
/// Out of range colours will be returned as is. It is the caller's
/// responsibility to process these as desired.
static cruft::vector3f
XYZ_to_sRGB (cruft::vector3f XYZ)
{
auto const [X, Y, Z] = XYZ;
float const r = +3.2404542f * X -1.5371385f * Y -0.4985314f * Z;
float const g = -0.9692660f * X +1.8760108f * Y +0.0415560f * Z;
float const b = +0.0556434f * X -0.2040259f * Y +1.0572252f * Z;
return {
exponentiate (r),
exponentiate (g),
exponentiate (b),
};
}
///----------------------------------------------------------------------------
/// Convert an XYZ colour to xyY
static
cruft::vector3f
XYZ_to_xyY (cruft::vector3f XYZ)
{
auto const [X, Y, Z] = XYZ;
if (cruft::abs (X + Y + Z) < 1e-5f) {
// Use the D65 xy if we're at black.
return {
0.31271f,
0.32902f,
Y
};
} else {
return {
X / (X + Y + Z),
Y / (X + Y + Z),
Y,
};
}
}
///----------------------------------------------------------------------------
/// Convert an xyY colour to XYZ
static cruft::vector3f
xyY_to_XYZ (cruft::vector3f xyY)
{
auto const [x, y, Y] = xyY;
if (cruft::abs (y) < 1e-5f) {
return {0, 0, 0};
} else {
return {
x * Y / y,
Y,
(1 - x - y) * Y / y,
};
}
}
//-----------------------------------------------------------------------------
cruft::srgba3f
cruft::scale_luminance (cruft::srgba3f sRGB, float f)
{
// We can't scale sRGB directly. Given we probably have to convert to other
// spaces via XYZ anyway we may as well scale the luminance value in xyY.
//
// The conversion chain is thus:
// * sRBB -> XYZ -> xyY -> scaling -> XYZ -> sRGB
auto const XYZ0= sRGB_to_XYZ (sRGB.as<cruft::vector3f> ());
auto const xyY0= XYZ_to_xyY (XYZ0);
auto const xyY1 = xyY0 * cruft::vector3f{1,1,f};
auto const XYZ1 = xyY_to_XYZ (xyY1);
return XYZ_to_sRGB (XYZ1).as<cruft::srgba3f> ();
}
//-----------------------------------------------------------------------------
cruft::srgba4f
cruft::scale_luminance (cruft::srgba4f sRGBA, float f)
{
auto const sRGB0 = sRGBA.indices<0,1,2> ();
auto const sRGB1 = scale_luminance (sRGB0, f);
return {
sRGB1.r,
sRGB1.g,
sRGB1.b,
sRGBA.a,
};
}

View File

@ -84,6 +84,9 @@ namespace cruft {
using srgba3u = srgba<3,uint8_t>;
using srgba4u = srgba<4,uint8_t>;
srgba3f scale_luminance (srgba3f, float factor);
srgba4f scale_luminance (srgba4f, float factor);
template <size_t S, typename T> struct hsva : colour<S,T,hsva<S,T>> {};
template <size_t S, typename T>

View File

@ -16,5 +16,15 @@ main (int, char**)
tap.expect_eq (u.cast<float> (), f, "cast u8 to float");
}
{
cruft::srgba3f white {1, 1, 1};
cruft::srgba3f black {0, 0, 0};
cruft::srgba3f percent_50 { 0.735359f, 0.735356f, 0.735357f };
tap.expect (max (scale_luminance (white, 1.f) - white) < 1e-5f, "unit luminance scaling for white");
tap.expect (max (scale_luminance (white, 0.f) - black) < 1e-5f, "zero luminance scaling for white");
tap.expect (max (scale_luminance (white, .5f) - percent_50) < 1e-5f, "half luminance scaling for white");
}
return tap.status ();
}