Created
June 16, 2026 10:28
-
-
Save garyo/4a6228b35bf8448600ee6b1011b5730e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #ifndef _ofxColourConvert_h_ | |
| #define _ofxColourConvert_h_ | |
| // Copyright OpenFX and contributors to the OpenFX project. | |
| // SPDX-License-Identifier: BSD-3-Clause | |
| /** @file ofxColourConvert.h | |
| A small, dependency-free, header-only C++17 utility for converting RGB | |
| triplets between the OFX "native" colourspaces (see ofxColour.h and the | |
| config header ofx-native-v1.5_aces-v1.3_ocio-v2.3.h) and the ACES2065-1 | |
| (AP0) reference colourspace. | |
| This is a convenience for plug-in and host authors who want simple | |
| colour conversions without a full OCIO dependency. It is NOT a | |
| replacement for OCIO: it does not implement display rendering (the ACES | |
| Output Transform / RRT+ODT), so the display-referred colourspaces | |
| (the @c *_display spaces), the ADX film-density encodings, and the | |
| abstract "basic" @c ofx_* spaces are intentionally out of scope. What is | |
| implemented are the scene-referred working spaces and the camera/texture | |
| encodings, each of which reduces exactly to: | |
| code value --(transfer function)--> linear RGB --(3x3 matrix)--> AP0 | |
| and back. Every conversion here is colorimetrically exact and round-trips | |
| to ACES2065-1. | |
| @par Derivation | |
| The gamut matrices are derived at compile time from the published xy | |
| chromaticities of each colourspace using the Normalized Primaries Matrix | |
| plus a von Kries chromatic adaptation to the ACES white point, exactly as | |
| done by OpenColorIO's built-in transforms. The adaptation method (Bradford | |
| or CAT02) and the transfer-function constants match the OCIO ACES config | |
| that the OFX native colourspaces are based on, so results agree with OCIO | |
| to within floating-point precision. | |
| @par Usage | |
| @code | |
| using namespace ofx::colour; | |
| RGB c = { 0.2, 0.5, 0.8 }; // an S-Log3/S-Gamut3 pixel | |
| RGB aces = toACES2065_1(c, Colourspace::slog3_sgamut3); | |
| RGB out = fromACES2065_1(aces, Colourspace::lin_rec709_srgb); | |
| // or directly: | |
| RGB out2 = convert(c, Colourspace::slog3_sgamut3, Colourspace::lin_rec709_srgb); | |
| @endcode | |
| */ | |
| #if !defined(__cplusplus) || __cplusplus < 201703L | |
| # error "ofxColourConvert.h requires C++17 or later" | |
| #endif | |
| #include <array> | |
| #include <cmath> | |
| #include <cstring> | |
| namespace ofx { | |
| namespace colour { | |
| // --------------------------------------------------------------------------- | |
| // Small linear-algebra core (constexpr: matrices are built at compile time). | |
| // Vectors are column vectors; a Matrix33 M acts as v' = M * v. | |
| // --------------------------------------------------------------------------- | |
| /** @brief An RGB (or XYZ) triplet. */ | |
| struct RGB { double r, g, b; }; | |
| /** @brief A 3x3 matrix in row-major order. */ | |
| struct Matrix33 { double m[3][3]; }; | |
| /** @brief Matrix * column-vector. */ | |
| constexpr RGB operator*(const Matrix33 &a, const RGB &v) | |
| { | |
| return { | |
| a.m[0][0] * v.r + a.m[0][1] * v.g + a.m[0][2] * v.b, | |
| a.m[1][0] * v.r + a.m[1][1] * v.g + a.m[1][2] * v.b, | |
| a.m[2][0] * v.r + a.m[2][1] * v.g + a.m[2][2] * v.b | |
| }; | |
| } | |
| /** @brief Matrix * matrix. */ | |
| constexpr Matrix33 operator*(const Matrix33 &a, const Matrix33 &b) | |
| { | |
| Matrix33 r{}; | |
| for (int i = 0; i < 3; ++i) | |
| for (int j = 0; j < 3; ++j) | |
| r.m[i][j] = a.m[i][0] * b.m[0][j] | |
| + a.m[i][1] * b.m[1][j] | |
| + a.m[i][2] * b.m[2][j]; | |
| return r; | |
| } | |
| /** @brief Inverse of a 3x3 matrix (assumes the matrix is non-singular). */ | |
| constexpr Matrix33 inverse(const Matrix33 &a) | |
| { | |
| const double det = | |
| a.m[0][0] * (a.m[1][1] * a.m[2][2] - a.m[1][2] * a.m[2][1]) | |
| - a.m[0][1] * (a.m[1][0] * a.m[2][2] - a.m[1][2] * a.m[2][0]) | |
| + a.m[0][2] * (a.m[1][0] * a.m[2][1] - a.m[1][1] * a.m[2][0]); | |
| const double inv = 1.0 / det; | |
| Matrix33 r{}; | |
| r.m[0][0] = (a.m[1][1] * a.m[2][2] - a.m[1][2] * a.m[2][1]) * inv; | |
| r.m[0][1] = (a.m[0][2] * a.m[2][1] - a.m[0][1] * a.m[2][2]) * inv; | |
| r.m[0][2] = (a.m[0][1] * a.m[1][2] - a.m[0][2] * a.m[1][1]) * inv; | |
| r.m[1][0] = (a.m[1][2] * a.m[2][0] - a.m[1][0] * a.m[2][2]) * inv; | |
| r.m[1][1] = (a.m[0][0] * a.m[2][2] - a.m[0][2] * a.m[2][0]) * inv; | |
| r.m[1][2] = (a.m[0][2] * a.m[1][0] - a.m[0][0] * a.m[1][2]) * inv; | |
| r.m[2][0] = (a.m[1][0] * a.m[2][1] - a.m[1][1] * a.m[2][0]) * inv; | |
| r.m[2][1] = (a.m[0][1] * a.m[2][0] - a.m[0][0] * a.m[2][1]) * inv; | |
| r.m[2][2] = (a.m[0][0] * a.m[1][1] - a.m[0][1] * a.m[1][0]) * inv; | |
| return r; | |
| } | |
| constexpr Matrix33 kIdentity33 = { { {1,0,0}, {0,1,0}, {0,0,1} } }; | |
| // --------------------------------------------------------------------------- | |
| // Chromaticities and matrix derivation (NPM + von Kries adaptation). | |
| // --------------------------------------------------------------------------- | |
| /** @brief CIE xy chromaticities of three primaries and a white point. */ | |
| struct Primaries { | |
| double rx, ry; | |
| double gx, gy; | |
| double bx, by; | |
| double wx, wy; | |
| }; | |
| /** @brief Chromatic adaptation transform used when white points differ. */ | |
| enum class Adaptation { None, Bradford, CAT02 }; | |
| /** @brief XYZ tristimulus of a white point (Y normalised to 1). */ | |
| constexpr RGB whiteXYZ(double wx, double wy) | |
| { | |
| return { wx / wy, 1.0, (1.0 - wx - wy) / wy }; | |
| } | |
| /** @brief Normalized Primaries Matrix: linear RGB (these primaries) -> CIE XYZ. | |
| Follows the standard construction (SMPTE RP 177): build the matrix of | |
| primary chromaticities, scale its columns so that RGB=(1,1,1) maps to the | |
| white point's XYZ. */ | |
| constexpr Matrix33 npm(const Primaries &p) | |
| { | |
| // Columns are the primaries' (x, y, z=1-x-y). | |
| const Matrix33 P = { { | |
| { p.rx, p.gx, p.bx }, | |
| { p.ry, p.gy, p.by }, | |
| { 1 - p.rx - p.ry, 1 - p.gx - p.gy, 1 - p.bx - p.by } | |
| } }; | |
| const Matrix33 Pinv = inverse(P); | |
| const RGB w = whiteXYZ(p.wx, p.wy); | |
| // gains = Pinv * whiteXYZ | |
| const RGB gains = Pinv * w; | |
| Matrix33 r = P; | |
| for (int row = 0; row < 3; ++row) { | |
| r.m[row][0] *= gains.r; | |
| r.m[row][1] *= gains.g; | |
| r.m[row][2] *= gains.b; | |
| } | |
| return r; | |
| } | |
| constexpr Matrix33 coneResponse(Adaptation a) | |
| { | |
| if (a == Adaptation::CAT02) | |
| return { { { 0.7328, 0.4296, -0.1624 }, | |
| { -0.7036, 1.6975, 0.0061 }, | |
| { 0.0030, 0.0136, 0.9834 } } }; | |
| // Bradford | |
| return { { { 0.8951, 0.2664, -0.1614 }, | |
| { -0.7502, 1.7135, 0.0367 }, | |
| { 0.0389, -0.0685, 1.0296 } } }; | |
| } | |
| /** @brief von Kries chromatic adaptation matrix (XYZ src white -> dst white). */ | |
| constexpr Matrix33 vonKries(const RGB &srcW, const RGB &dstW, Adaptation method) | |
| { | |
| const Matrix33 M = coneResponse(method); | |
| const Matrix33 Minv = inverse(M); | |
| const RGB s = M * srcW; | |
| const RGB d = M * dstW; | |
| const Matrix33 scale = { { { d.r / s.r, 0, 0 }, | |
| { 0, d.g / s.g, 0 }, | |
| { 0, 0, d.b / s.b } } }; | |
| return Minv * (scale * M); | |
| } | |
| /** @brief ACES AP0 (ACES2065-1) primaries, from SMPTE ST 2065-1. */ | |
| constexpr Primaries kAP0 = { 0.7347, 0.2653, 0.0000, 1.0000, 0.0001, -0.0770, 0.32168, 0.33767 }; | |
| /** @brief ACES AP1 (ACEScg) primaries. */ | |
| constexpr Primaries kAP1 = { 0.713, 0.293, 0.165, 0.830, 0.128, 0.044, 0.32168, 0.33767 }; | |
| /** @brief Build the linear-RGB -> ACES2065-1(AP0) matrix for a set of primaries. */ | |
| constexpr Matrix33 toAP0Matrix(const Primaries &src, Adaptation method) | |
| { | |
| const Matrix33 srcToXYZ = npm(src); | |
| const Matrix33 ap0ToXYZ = npm(kAP0); | |
| const Matrix33 xyzToAP0 = inverse(ap0ToXYZ); | |
| const bool sameWhite = (src.wx == kAP0.wx && src.wy == kAP0.wy); | |
| if (method == Adaptation::None || sameWhite) | |
| return xyzToAP0 * srcToXYZ; | |
| const RGB srcW = srcToXYZ * RGB{1, 1, 1}; | |
| const RGB ap0W = ap0ToXYZ * RGB{1, 1, 1}; | |
| const Matrix33 vk = vonKries(srcW, ap0W, method); | |
| return xyzToAP0 * (vk * srcToXYZ); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Transfer functions. Each encoding provides a decode (code -> scene-linear) | |
| // and an encode (scene-linear -> code). All are sign-preserving where the | |
| // formula would otherwise be undefined for negatives. | |
| // --------------------------------------------------------------------------- | |
| namespace detail { | |
| inline double signedPow(double x, double e) | |
| { | |
| return x < 0.0 ? -std::pow(-x, e) : std::pow(x, e); | |
| } | |
| // --- Generic OCIO-style "camera log" with an optional linear toe segment. --- | |
| struct CameraLog { | |
| double base; | |
| double logSlope, logOffset; // log-side slope / offset | |
| double linSlope, linOffset; // lin-side slope / offset | |
| double linBreak; // scene-linear value where the segments meet | |
| double linearSlope; // slope of the toe; <=0 means "derive it" | |
| }; | |
| inline double logBreak(const CameraLog &p) | |
| { | |
| return p.logSlope * std::log(p.linSlope * p.linBreak + p.linOffset) / std::log(p.base) | |
| + p.logOffset; | |
| } | |
| inline double linearSlopeOf(const CameraLog &p) | |
| { | |
| return p.linearSlope > 0.0 | |
| ? p.linearSlope | |
| : p.logSlope * p.linSlope | |
| / ((p.linSlope * p.linBreak + p.linOffset) * std::log(p.base)); | |
| } | |
| inline double cameraLogToLinear(double v, const CameraLog &p) | |
| { | |
| const double ls = linearSlopeOf(p); | |
| const double lb = logBreak(p); | |
| const double lo = lb - ls * p.linBreak; // linear-segment offset | |
| if (v <= lb) | |
| return (v - lo) / ls; | |
| return (std::pow(p.base, (v - p.logOffset) / p.logSlope) - p.linOffset) / p.linSlope; | |
| } | |
| inline double cameraLogFromLinear(double x, const CameraLog &p) | |
| { | |
| const double ls = linearSlopeOf(p); | |
| const double lb = logBreak(p); | |
| const double lo = lb - ls * p.linBreak; | |
| if (x <= p.linBreak) | |
| return ls * x + lo; | |
| return p.logSlope * std::log(p.linSlope * x + p.linOffset) / std::log(p.base) + p.logOffset; | |
| } | |
| // Parameters sourced from the OCIO ACES built-in transforms. | |
| inline const CameraLog kACEScct { 2.0, 1.0/17.52, 9.72/17.52, 1.0, 0.0, 0.0078125, 0.0 }; | |
| inline const CameraLog kARRILogC3{ 10.0, 0.2471896383, 0.3855369987, | |
| 1.0/0.18, 0.0522722750, | |
| ((1.0/9.0) - 0.0522722750) / (1.0/0.18), 0.0 }; | |
| inline const CameraLog kARRILogC4{ 2.0, 0.0647954196341293, -0.295908392682586, | |
| 2231.82630906769, 64.0, -0.0180569961199113, 0.0 }; | |
| inline const CameraLog kSLog3 { 10.0, 261.5/1023.0, 420.0/1023.0, | |
| 1.0/(0.18+0.01), 0.01/(0.18+0.01), 0.01125, | |
| ((171.2102946929 - 95.0) / 0.01125) / 1023.0 }; | |
| inline const CameraLog kVLog { 10.0, 0.241514, 0.598206, 1.0, 0.00873, 0.01, 0.0 }; | |
| inline const CameraLog kLog3G10 { 10.0, 0.224282, 0.0, 155.975327, | |
| 0.01 * 155.975327 + 1.0, -0.01, 0.0 }; | |
| // --- sRGB piecewise (IEC 61966-2-1). --- | |
| inline double srgbToLinear(double c) | |
| { | |
| const double a = std::fabs(c); | |
| const double r = a <= 0.04045 ? a / 12.92 : std::pow((a + 0.055) / 1.055, 2.4); | |
| return c < 0.0 ? -r : r; | |
| } | |
| inline double srgbFromLinear(double c) | |
| { | |
| const double a = std::fabs(c); | |
| const double r = a <= 0.0031308 ? a * 12.92 : 1.055 * std::pow(a, 1.0 / 2.4) - 0.055; | |
| return c < 0.0 ? -r : r; | |
| } | |
| // --- Pure display gamma (texture encodings g1.8 / g2.2 / g2.4). --- | |
| template <int Num, int Den> | |
| inline double gammaToLinear(double c) { return signedPow(c, double(Num) / double(Den)); } | |
| template <int Num, int Den> | |
| inline double gammaFromLinear(double c) { return signedPow(c, double(Den) / double(Num)); } | |
| // --- Rec.709 camera OETF (ITU-R BT.709). --- | |
| inline double rec709ToLinear(double v) | |
| { | |
| const double a = std::fabs(v); | |
| const double r = a < 0.081 ? a / 4.5 : std::pow((a + 0.099) / 1.099, 1.0 / 0.45); | |
| return v < 0.0 ? -r : r; | |
| } | |
| inline double rec709FromLinear(double l) | |
| { | |
| const double a = std::fabs(l); | |
| const double r = a < 0.018 ? 4.5 * a : 1.099 * std::pow(a, 0.45) - 0.099; | |
| return l < 0.0 ? -r : r; | |
| } | |
| // --- ACEScc (pure log, no linear toe; SMPTE has an explicit dark branch). --- | |
| inline double acesccToLinear(double v) | |
| { | |
| if (v < (9.72 - 15.0) / 17.52) | |
| return (std::pow(2.0, v * 17.52 - 9.72) - std::pow(2.0, -16.0)) * 2.0; | |
| return std::pow(2.0, v * 17.52 - 9.72); | |
| } | |
| inline double acesccFromLinear(double lin) | |
| { | |
| if (lin <= 0.0) | |
| return (std::log2(std::pow(2.0, -16.0)) + 9.72) / 17.52; | |
| if (lin < std::pow(2.0, -15.0)) | |
| return (std::log2(std::pow(2.0, -16.0) + lin * 0.5) + 9.72) / 17.52; | |
| return (std::log2(lin) + 9.72) / 17.52; | |
| } | |
| // --- Canon Log 2 (explicit piecewise, includes the *0.9 scene scaling). --- | |
| inline double canonLog2ToLinear(double in) | |
| { | |
| double out = in < 0.092864125 | |
| ? -(std::pow(10.0, (0.092864125 - in) / 0.24136077) - 1.0) / 87.099375 | |
| : (std::pow(10.0, (in - 0.092864125) / 0.24136077) - 1.0) / 87.099375; | |
| return out * 0.9; | |
| } | |
| inline double canonLog2FromLinear(double lin) | |
| { | |
| const double x = lin / 0.9; | |
| return x < 0.0 | |
| ? 0.092864125 - std::log10(-x * 87.099375 + 1.0) * 0.24136077 | |
| : 0.092864125 + std::log10( x * 87.099375 + 1.0) * 0.24136077; | |
| } | |
| // --- Canon Log 3 (explicit piecewise with a central linear segment). --- | |
| inline double canonLog3ToLinear(double in) | |
| { | |
| double out; | |
| if (in < 0.097465473) | |
| out = -(std::pow(10.0, (0.12783901 - in) / 0.36726845) - 1.0) / 14.98325; | |
| else if (in <= 0.15277891) | |
| out = (in - 0.12512219) / 1.9754798; | |
| else | |
| out = (std::pow(10.0, (in - 0.12240537) / 0.36726845) - 1.0) / 14.98325; | |
| return out * 0.9; | |
| } | |
| inline double canonLog3FromLinear(double lin) | |
| { | |
| const double x = lin / 0.9; | |
| if (x < -0.014) | |
| return 0.12783901 - std::log10(-x * 14.98325 + 1.0) * 0.36726845; | |
| if (x <= 0.014) | |
| return x * 1.9754798 + 0.12512219; | |
| return 0.12240537 + std::log10(x * 14.98325 + 1.0) * 0.36726845; | |
| } | |
| // --- Blackmagic Film Generation 5. --- | |
| inline double bmdFilmToLinear(double y) | |
| { | |
| constexpr double A = 0.08692876065491224, B = 0.005494072432257808, | |
| C = 0.5300133392291939, D = 8.283605932402494, | |
| E = 0.09246575342465753, LIN_CUT = 0.005; | |
| constexpr double LOG_CUT = D * LIN_CUT + E; | |
| return y < LOG_CUT ? (y - E) / D : std::exp((y - C) / A) - B; | |
| } | |
| inline double bmdFilmFromLinear(double x) | |
| { | |
| constexpr double A = 0.08692876065491224, B = 0.005494072432257808, | |
| C = 0.5300133392291939, D = 8.283605932402494, | |
| E = 0.09246575342465753, LIN_CUT = 0.005; | |
| return x < LIN_CUT ? D * x + E : A * std::log(x + B) + C; | |
| } | |
| // --- DaVinci Intermediate. --- | |
| inline double davinciIntToLinear(double v) | |
| { | |
| constexpr double A = 0.0075, B = 7.0, C = 0.07329248, | |
| M = 10.44426855, LOG_CUT = 0.02740668; | |
| return v <= LOG_CUT ? v / M : std::pow(2.0, v / C - B) - A; | |
| } | |
| inline double davinciIntFromLinear(double l) | |
| { | |
| constexpr double A = 0.0075, B = 7.0, C = 0.07329248, | |
| M = 10.44426855, LIN_CUT = 0.00262409; | |
| return l <= LIN_CUT ? l * M : (std::log2(l + A) + B) * C; | |
| } | |
| inline double linearToLinear(double x) { return x; } | |
| // Wrappers giving every camera-log encoding a uniform double(*)(double) shape. | |
| inline double acescctTo(double v) { return cameraLogToLinear(v, kACEScct); } | |
| inline double acescctFrom(double v) { return cameraLogFromLinear(v, kACEScct); } | |
| inline double arriLogC3To(double v) { return cameraLogToLinear(v, kARRILogC3); } | |
| inline double arriLogC3From(double v) { return cameraLogFromLinear(v, kARRILogC3); } | |
| inline double arriLogC4To(double v) { return cameraLogToLinear(v, kARRILogC4); } | |
| inline double arriLogC4From(double v) { return cameraLogFromLinear(v, kARRILogC4); } | |
| inline double slog3To(double v) { return cameraLogToLinear(v, kSLog3); } | |
| inline double slog3From(double v) { return cameraLogFromLinear(v, kSLog3); } | |
| inline double vlogTo(double v) { return cameraLogToLinear(v, kVLog); } | |
| inline double vlogFrom(double v) { return cameraLogFromLinear(v, kVLog); } | |
| inline double log3g10To(double v) { return cameraLogToLinear(v, kLog3G10); } | |
| inline double log3g10From(double v) { return cameraLogFromLinear(v, kLog3G10); } | |
| } // namespace detail | |
| /** @brief A pair of transfer functions: code<->scene-linear. */ | |
| struct Transfer { | |
| double (*toLinear)(double); | |
| double (*fromLinear)(double); | |
| }; | |
| // --------------------------------------------------------------------------- | |
| // Camera gamut primaries (xy), from the OCIO ACES built-in transforms. | |
| // --------------------------------------------------------------------------- | |
| constexpr Primaries kRec709 = { 0.64, 0.33, 0.30, 0.60, 0.15, 0.06, 0.3127, 0.3290 }; | |
| constexpr Primaries kRec2020 = { 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290 }; | |
| constexpr Primaries kP3D65 = { 0.680, 0.320, 0.265, 0.690, 0.150, 0.060, 0.3127, 0.3290 }; | |
| constexpr Primaries kAWG3 = { 0.684, 0.313, 0.221, 0.848, 0.0861,-0.102, 0.3127, 0.3290 }; | |
| constexpr Primaries kAWG4 = { 0.7347, 0.2653, 0.1424, 0.8576, 0.0991,-0.0308, 0.3127, 0.3290 }; | |
| constexpr Primaries kSGamut3 = { 0.730, 0.280, 0.140, 0.855, 0.100, -0.050, 0.3127, 0.3290 }; | |
| constexpr Primaries kSGamut3Cine = { 0.766, 0.275, 0.225, 0.800, 0.089, -0.087, 0.3127, 0.3290 }; | |
| constexpr Primaries kCanonCGamut = { 0.740, 0.270, 0.170, 1.140, 0.080, -0.100, 0.3127, 0.3290 }; | |
| constexpr Primaries kVGamut = { 0.730, 0.280, 0.165, 0.840, 0.100, -0.030, 0.3127, 0.3290 }; | |
| constexpr Primaries kREDWideGamut = { 0.780308, 0.304253, 0.121595, 1.493994, 0.095612, -0.084589, 0.3127, 0.3290 }; | |
| constexpr Primaries kBMDWideGamut = { 0.7177215, 0.3171181, 0.2280410, 0.8615690, 0.1005841, -0.0820452, 0.3127, 0.3290 }; | |
| constexpr Primaries kDaVinciWideGamut = { 0.8000, 0.3130, 0.1682, 0.9877, 0.0790, -0.1155, 0.3127, 0.3290 }; | |
| // Sony Venice gamuts: OCIO ships precomputed RGB->AP0 matrices (no primaries). | |
| constexpr Matrix33 kVeniceSGamut3_to_AP0 = { { | |
| { 0.7933297411, 0.0890786256, 0.1175916333 }, | |
| { 0.0155810585, 1.0327123069, -0.0482933654 }, | |
| { -0.0188647478, 0.0127694121, 1.0060953358 } | |
| } }; | |
| constexpr Matrix33 kVeniceSGamut3Cine_to_AP0 = { { | |
| { 0.6742570921, 0.2205717359, 0.1051711720 }, | |
| { -0.0093136061, 1.1059588614, -0.0966452553 }, | |
| { -0.0382090673, -0.0179383766, 1.0561474439 } | |
| } }; | |
| // --------------------------------------------------------------------------- | |
| // The supported colourspaces (scene-referred working + camera/texture spaces). | |
| // --------------------------------------------------------------------------- | |
| enum class Colourspace { | |
| ACES2065_1, // AP0, the reference | |
| ACEScg, | |
| ACEScc, | |
| ACEScct, | |
| lin_p3d65, | |
| lin_rec2020, | |
| lin_rec709_srgb, | |
| g18_rec709_tx, | |
| g22_rec709_tx, | |
| g24_rec709_tx, | |
| g22_ap1_tx, | |
| srgb_tx, | |
| srgb_encoded_ap1_tx, | |
| srgb_encoded_p3d65_tx, | |
| camera_rec709, | |
| lin_arri_wide_gamut_3, | |
| arri_logc3_ei800, | |
| lin_arri_wide_gamut_4, | |
| arri_logc4, | |
| lin_bmd_widegamut_gen5, | |
| bmdfilm_widegamut_gen5, | |
| davinci_intermediate_widegamut, | |
| lin_davinci_widegamut, | |
| lin_cinemagamut_d55, | |
| canonlog2_cinemagamut_d55, | |
| canonlog3_cinemagamut_d55, | |
| lin_vgamut, | |
| vlog_vgamut, | |
| lin_redwidegamutrgb, | |
| log3g10_redwidegamutrgb, | |
| lin_sgamut3, | |
| slog3_sgamut3, | |
| lin_sgamut3cine, | |
| slog3_sgamut3cine, | |
| lin_venice_sgamut3, | |
| slog3_venice_sgamut3, | |
| lin_venice_sgamut3cine, | |
| slog3_venice_sgamut3cine, | |
| Count | |
| }; | |
| /** @brief Everything needed to convert a colourspace to/from ACES2065-1. */ | |
| struct ColourspaceInfo { | |
| Matrix33 toAP0; // linear RGB (this space's gamut) -> AP0 | |
| Matrix33 fromAP0; // AP0 -> linear RGB (this space's gamut) | |
| Transfer tf; // code <-> scene-linear | |
| }; | |
| namespace detail { | |
| inline Transfer linearTF() { return { linearToLinear, linearToLinear }; } | |
| inline ColourspaceInfo make(const Matrix33 &toAP0, const Transfer &tf) | |
| { | |
| return { toAP0, inverse(toAP0), tf }; | |
| } | |
| // Build the table once, on first use (no static-init-order concerns; the | |
| // gamut matrices are compile-time constants). | |
| inline const std::array<ColourspaceInfo, std::size_t(Colourspace::Count)> &table() | |
| { | |
| using A = Adaptation; | |
| static const std::array<ColourspaceInfo, std::size_t(Colourspace::Count)> t = [] { | |
| std::array<ColourspaceInfo, std::size_t(Colourspace::Count)> a{}; | |
| auto set = [&](Colourspace cs, const ColourspaceInfo &info) { | |
| a[std::size_t(cs)] = info; | |
| }; | |
| const Transfer lin = linearTF(); | |
| const Transfer srgb = { srgbToLinear, srgbFromLinear }; | |
| const Transfer g18 = { gammaToLinear<9,5>, gammaFromLinear<9,5> }; | |
| const Transfer g22 = { gammaToLinear<11,5>, gammaFromLinear<11,5> }; | |
| const Transfer g24 = { gammaToLinear<12,5>, gammaFromLinear<12,5> }; | |
| const Transfer rec709 = { rec709ToLinear, rec709FromLinear }; | |
| const Transfer cc = { acesccToLinear, acesccFromLinear }; | |
| const Transfer cct = { acescctTo, acescctFrom }; | |
| const Transfer logc3 = { arriLogC3To, arriLogC3From }; | |
| const Transfer logc4 = { arriLogC4To, arriLogC4From }; | |
| const Transfer slog3 = { slog3To, slog3From }; | |
| const Transfer clog2 = { canonLog2ToLinear, canonLog2FromLinear }; | |
| const Transfer clog3 = { canonLog3ToLinear, canonLog3FromLinear }; | |
| const Transfer vlog = { vlogTo, vlogFrom }; | |
| const Transfer log3g10 = { log3g10To, log3g10From }; | |
| const Transfer bmd = { bmdFilmToLinear, bmdFilmFromLinear }; | |
| const Transfer dvi = { davinciIntToLinear, davinciIntFromLinear }; | |
| // Gamut matrices to AP0. AP1 needs no white adaptation; Rec.709/2020/ | |
| // P3 and Panasonic/RED use Bradford; the other camera gamuts use CAT02 | |
| // (matching the OCIO ACES built-in transforms). | |
| const Matrix33 ap0 = kIdentity33; | |
| const Matrix33 ap1 = toAP0Matrix(kAP1, A::None); | |
| const Matrix33 p3 = toAP0Matrix(kP3D65, A::Bradford); | |
| const Matrix33 r2020 = toAP0Matrix(kRec2020, A::Bradford); | |
| const Matrix33 r709 = toAP0Matrix(kRec709, A::Bradford); | |
| const Matrix33 awg3 = toAP0Matrix(kAWG3, A::CAT02); | |
| const Matrix33 awg4 = toAP0Matrix(kAWG4, A::CAT02); | |
| const Matrix33 bmdg = toAP0Matrix(kBMDWideGamut, A::CAT02); | |
| const Matrix33 dvwg = toAP0Matrix(kDaVinciWideGamut, A::CAT02); | |
| const Matrix33 cgamut= toAP0Matrix(kCanonCGamut, A::CAT02); | |
| const Matrix33 vgamut= toAP0Matrix(kVGamut, A::Bradford); | |
| const Matrix33 rwg = toAP0Matrix(kREDWideGamut, A::Bradford); | |
| const Matrix33 sg3 = toAP0Matrix(kSGamut3, A::CAT02); | |
| const Matrix33 sg3c = toAP0Matrix(kSGamut3Cine, A::CAT02); | |
| set(Colourspace::ACES2065_1, make(ap0, lin)); | |
| set(Colourspace::ACEScg, make(ap1, lin)); | |
| set(Colourspace::ACEScc, make(ap1, cc)); | |
| set(Colourspace::ACEScct, make(ap1, cct)); | |
| set(Colourspace::lin_p3d65, make(p3, lin)); | |
| set(Colourspace::lin_rec2020, make(r2020, lin)); | |
| set(Colourspace::lin_rec709_srgb, make(r709, lin)); | |
| set(Colourspace::g18_rec709_tx, make(r709, g18)); | |
| set(Colourspace::g22_rec709_tx, make(r709, g22)); | |
| set(Colourspace::g24_rec709_tx, make(r709, g24)); | |
| set(Colourspace::g22_ap1_tx, make(ap1, g22)); | |
| set(Colourspace::srgb_tx, make(r709, srgb)); | |
| set(Colourspace::srgb_encoded_ap1_tx, make(ap1, srgb)); | |
| set(Colourspace::srgb_encoded_p3d65_tx, make(p3, srgb)); | |
| set(Colourspace::camera_rec709, make(r709, rec709)); | |
| set(Colourspace::lin_arri_wide_gamut_3, make(awg3, lin)); | |
| set(Colourspace::arri_logc3_ei800, make(awg3, logc3)); | |
| set(Colourspace::lin_arri_wide_gamut_4, make(awg4, lin)); | |
| set(Colourspace::arri_logc4, make(awg4, logc4)); | |
| set(Colourspace::lin_bmd_widegamut_gen5, make(bmdg, lin)); | |
| set(Colourspace::bmdfilm_widegamut_gen5, make(bmdg, bmd)); | |
| set(Colourspace::davinci_intermediate_widegamut, make(dvwg, dvi)); | |
| set(Colourspace::lin_davinci_widegamut, make(dvwg, lin)); | |
| set(Colourspace::lin_cinemagamut_d55, make(cgamut, lin)); | |
| set(Colourspace::canonlog2_cinemagamut_d55, make(cgamut, clog2)); | |
| set(Colourspace::canonlog3_cinemagamut_d55, make(cgamut, clog3)); | |
| set(Colourspace::lin_vgamut, make(vgamut, lin)); | |
| set(Colourspace::vlog_vgamut, make(vgamut, vlog)); | |
| set(Colourspace::lin_redwidegamutrgb, make(rwg, lin)); | |
| set(Colourspace::log3g10_redwidegamutrgb, make(rwg, log3g10)); | |
| set(Colourspace::lin_sgamut3, make(sg3, lin)); | |
| set(Colourspace::slog3_sgamut3, make(sg3, slog3)); | |
| set(Colourspace::lin_sgamut3cine, make(sg3c, lin)); | |
| set(Colourspace::slog3_sgamut3cine,make(sg3c, slog3)); | |
| set(Colourspace::lin_venice_sgamut3, make(kVeniceSGamut3_to_AP0, lin)); | |
| set(Colourspace::slog3_venice_sgamut3, make(kVeniceSGamut3_to_AP0, slog3)); | |
| set(Colourspace::lin_venice_sgamut3cine, make(kVeniceSGamut3Cine_to_AP0, lin)); | |
| set(Colourspace::slog3_venice_sgamut3cine, make(kVeniceSGamut3Cine_to_AP0, slog3)); | |
| return a; | |
| }(); | |
| return t; | |
| } | |
| } // namespace detail | |
| /** @brief Look up the conversion data for a colourspace. */ | |
| inline const ColourspaceInfo &info(Colourspace cs) | |
| { | |
| return detail::table()[std::size_t(cs)]; | |
| } | |
| /** @brief Convert an RGB triplet in @p from to ACES2065-1 (AP0). */ | |
| inline RGB toACES2065_1(RGB c, Colourspace from) | |
| { | |
| const ColourspaceInfo &i = info(from); | |
| const RGB lin = { i.tf.toLinear(c.r), i.tf.toLinear(c.g), i.tf.toLinear(c.b) }; | |
| return i.toAP0 * lin; | |
| } | |
| /** @brief Convert an ACES2065-1 (AP0) triplet to the colourspace @p to. */ | |
| inline RGB fromACES2065_1(RGB aces, Colourspace to) | |
| { | |
| const ColourspaceInfo &i = info(to); | |
| const RGB lin = i.fromAP0 * aces; | |
| return { i.tf.fromLinear(lin.r), i.tf.fromLinear(lin.g), i.tf.fromLinear(lin.b) }; | |
| } | |
| /** @brief Convert an RGB triplet directly between two colourspaces. */ | |
| inline RGB convert(RGB c, Colourspace from, Colourspace to) | |
| { | |
| if (from == to) return c; | |
| return fromACES2065_1(toACES2065_1(c, from), to); | |
| } | |
| /** @brief Map an OFX colourspace identifier string (e.g. the kOfxColourspace* | |
| values from the config header, such as "slog3_sgamut3") to a Colourspace. | |
| @returns true and sets @p out on success; false if the name is not one of the | |
| spaces handled here. */ | |
| inline bool colourspaceFromName(const char *name, Colourspace &out) | |
| { | |
| struct Entry { const char *name; Colourspace cs; }; | |
| static const Entry entries[] = { | |
| { "ACES2065-1", Colourspace::ACES2065_1 }, | |
| { "ACEScg", Colourspace::ACEScg }, | |
| { "ACEScc", Colourspace::ACEScc }, | |
| { "ACEScct", Colourspace::ACEScct }, | |
| { "lin_p3d65", Colourspace::lin_p3d65 }, | |
| { "lin_rec2020", Colourspace::lin_rec2020 }, | |
| { "lin_rec709_srgb", Colourspace::lin_rec709_srgb }, | |
| { "g18_rec709_tx", Colourspace::g18_rec709_tx }, | |
| { "g22_rec709_tx", Colourspace::g22_rec709_tx }, | |
| { "g24_rec709_tx", Colourspace::g24_rec709_tx }, | |
| { "g22_ap1_tx", Colourspace::g22_ap1_tx }, | |
| { "srgb_tx", Colourspace::srgb_tx }, | |
| { "srgb_encoded_ap1_tx", Colourspace::srgb_encoded_ap1_tx }, | |
| { "srgb_encoded_p3d65_tx", Colourspace::srgb_encoded_p3d65_tx }, | |
| { "camera_rec709", Colourspace::camera_rec709 }, | |
| { "lin_arri_wide_gamut_3", Colourspace::lin_arri_wide_gamut_3 }, | |
| { "arri_logc3_ei800", Colourspace::arri_logc3_ei800 }, | |
| { "lin_arri_wide_gamut_4", Colourspace::lin_arri_wide_gamut_4 }, | |
| { "arri_logc4", Colourspace::arri_logc4 }, | |
| { "lin_bmd_widegamut_gen5", Colourspace::lin_bmd_widegamut_gen5 }, | |
| { "bmdfilm_widegamut_gen5", Colourspace::bmdfilm_widegamut_gen5 }, | |
| { "davinci_intermediate_widegamut", Colourspace::davinci_intermediate_widegamut }, | |
| { "lin_davinci_widegamut", Colourspace::lin_davinci_widegamut }, | |
| { "lin_cinemagamut_d55", Colourspace::lin_cinemagamut_d55 }, | |
| { "canonlog2_cinemagamut_d55", Colourspace::canonlog2_cinemagamut_d55 }, | |
| { "canonlog3_cinemagamut_d55", Colourspace::canonlog3_cinemagamut_d55 }, | |
| { "lin_vgamut", Colourspace::lin_vgamut }, | |
| { "vlog_vgamut", Colourspace::vlog_vgamut }, | |
| { "lin_redwidegamutrgb", Colourspace::lin_redwidegamutrgb }, | |
| { "log3g10_redwidegamutrgb", Colourspace::log3g10_redwidegamutrgb }, | |
| { "lin_sgamut3", Colourspace::lin_sgamut3 }, | |
| { "slog3_sgamut3", Colourspace::slog3_sgamut3 }, | |
| { "lin_sgamut3cine", Colourspace::lin_sgamut3cine }, | |
| { "slog3_sgamut3cine", Colourspace::slog3_sgamut3cine }, | |
| { "lin_venice_sgamut3", Colourspace::lin_venice_sgamut3 }, | |
| { "slog3_venice_sgamut3", Colourspace::slog3_venice_sgamut3 }, | |
| { "lin_venice_sgamut3cine", Colourspace::lin_venice_sgamut3cine }, | |
| { "slog3_venice_sgamut3cine", Colourspace::slog3_venice_sgamut3cine }, | |
| }; | |
| for (const Entry &e : entries) { | |
| if (std::strcmp(e.name, name) == 0) { out = e.cs; return true; } | |
| } | |
| return false; | |
| } | |
| } // namespace colour | |
| } // namespace ofx | |
| #endif // _ofxColourConvert_h_ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment