From ea0fab21d4f1e61b873afbd355cd244deaf4af75 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sun, 31 Aug 2025 02:56:21 +0200 Subject: [PATCH] Color Management: Builtin support for ACEScg and Rec.2020 linear spaces With new functions to convert to/from scene linear in the Python API. ACEScg in particular is common in USD and MaterialX files. Pull Request: https://projects.blender.org/blender/blender/pulls/145755 --- source/blender/imbuf/IMB_colormanagement.hh | 8 ++ .../intern/IMB_colormanagement_intern.hh | 4 + .../blender/imbuf/intern/colormanagement.cc | 45 +++++++---- .../imbuf/intern/colormanagement_inline.h | 20 +++++ .../blender/imbuf/opencolorio/OCIO_matrix.hh | 13 +++ .../python/mathutils/mathutils_Color.cc | 80 +++++++++++++++++++ 6 files changed, 155 insertions(+), 15 deletions(-) diff --git a/source/blender/imbuf/IMB_colormanagement.hh b/source/blender/imbuf/IMB_colormanagement.hh index 67266d6d8ae..d61f55d8b97 100644 --- a/source/blender/imbuf/IMB_colormanagement.hh +++ b/source/blender/imbuf/IMB_colormanagement.hh @@ -110,6 +110,14 @@ BLI_INLINE void IMB_colormanagement_aces_to_scene_linear(float scene_linear[3], const float aces[3]); BLI_INLINE void IMB_colormanagement_scene_linear_to_aces(float aces[3], const float scene_linear[3]); +BLI_INLINE void IMB_colormanagement_acescg_to_scene_linear(float scene_linear[3], + const float acescg[3]); +BLI_INLINE void IMB_colormanagement_scene_linear_to_acescg(float acescg[3], + const float scene_linear[3]); +BLI_INLINE void IMB_colormanagement_rec2020_to_scene_linear(float scene_linear[3], + const float rec2020[3]); +BLI_INLINE void IMB_colormanagement_scene_linear_to_rec2020(float rec2020[3], + const float scene_linear[3]); blender::float3x3 IMB_colormanagement_get_xyz_to_scene_linear(); blender::float3x3 IMB_colormanagement_get_scene_linear_to_xyz(); diff --git a/source/blender/imbuf/intern/IMB_colormanagement_intern.hh b/source/blender/imbuf/intern/IMB_colormanagement_intern.hh index c80a3710f80..0f0f005840f 100644 --- a/source/blender/imbuf/intern/IMB_colormanagement_intern.hh +++ b/source/blender/imbuf/intern/IMB_colormanagement_intern.hh @@ -26,8 +26,12 @@ extern blender::float3x3 imbuf_scene_linear_to_xyz; extern blender::float3x3 imbuf_xyz_to_scene_linear; extern blender::float3x3 imbuf_scene_linear_to_aces; extern blender::float3x3 imbuf_aces_to_scene_linear; +extern blender::float3x3 imbuf_scene_linear_to_acescg; +extern blender::float3x3 imbuf_acescg_to_scene_linear; extern blender::float3x3 imbuf_scene_linear_to_rec709; extern blender::float3x3 imbuf_rec709_to_scene_linear; +extern blender::float3x3 imbuf_scene_linear_to_rec2020; +extern blender::float3x3 imbuf_rec2020_to_scene_linear; extern bool imbuf_scene_linear_is_rec709; #define MAX_COLORSPACE_NAME 64 diff --git a/source/blender/imbuf/intern/colormanagement.cc b/source/blender/imbuf/intern/colormanagement.cc index a6b4083cb29..695a4948d6d 100644 --- a/source/blender/imbuf/intern/colormanagement.cc +++ b/source/blender/imbuf/intern/colormanagement.cc @@ -98,8 +98,12 @@ float3x3 imbuf_scene_linear_to_xyz = float3x3::zero(); float3x3 imbuf_xyz_to_scene_linear = float3x3::zero(); float3x3 imbuf_scene_linear_to_rec709 = float3x3::zero(); float3x3 imbuf_rec709_to_scene_linear = float3x3::zero(); +float3x3 imbuf_scene_linear_to_rec2020 = float3x3::zero(); +float3x3 imbuf_rec2020_to_scene_linear = float3x3::zero(); float3x3 imbuf_scene_linear_to_aces = float3x3::zero(); float3x3 imbuf_aces_to_scene_linear = float3x3::zero(); +float3x3 imbuf_scene_linear_to_acescg = float3x3::zero(); +float3x3 imbuf_acescg_to_scene_linear = float3x3::zero(); bool imbuf_scene_linear_is_rec709 = false; /* lock used by pre-cached processors getters, so processor wouldn't @@ -514,6 +518,31 @@ static bool colormanage_role_color_space_name_get(ocio::Config &config, return true; } +static void colormanage_update_matrices() +{ + /* Load luminance coefficients. */ + imbuf_luma_coefficients = g_config->get_default_luma_coefs(); + + /* Load standard color spaces. */ + imbuf_xyz_to_scene_linear = g_config->get_xyz_to_scene_linear_matrix(); + imbuf_scene_linear_to_xyz = math::invert(imbuf_xyz_to_scene_linear); + + imbuf_scene_linear_to_rec709 = ocio::XYZ_TO_REC709 * imbuf_scene_linear_to_xyz; + imbuf_rec709_to_scene_linear = math::invert(imbuf_scene_linear_to_rec709); + + imbuf_scene_linear_to_rec2020 = ocio::XYZ_TO_REC2020 * imbuf_scene_linear_to_xyz; + imbuf_rec2020_to_scene_linear = math::invert(imbuf_scene_linear_to_rec2020); + + imbuf_aces_to_scene_linear = imbuf_xyz_to_scene_linear * ocio::ACES_TO_XYZ; + imbuf_scene_linear_to_aces = math::invert(imbuf_aces_to_scene_linear); + + imbuf_acescg_to_scene_linear = imbuf_xyz_to_scene_linear * ocio::ACESCG_TO_XYZ; + imbuf_scene_linear_to_acescg = math::invert(imbuf_acescg_to_scene_linear); + + imbuf_scene_linear_is_rec709 = math::is_equal( + imbuf_scene_linear_to_rec709, float3x3::identity(), 0.0001f); +} + static bool colormanage_load_config(ocio::Config &config) { bool ok = true; @@ -561,21 +590,7 @@ static bool colormanage_load_config(ocio::Config &config) } } - /* Load luminance coefficients. */ - imbuf_luma_coefficients = config.get_default_luma_coefs(); - - /* Load standard color spaces. */ - imbuf_xyz_to_scene_linear = config.get_xyz_to_scene_linear_matrix(); - imbuf_scene_linear_to_xyz = math::invert(imbuf_xyz_to_scene_linear); - - imbuf_scene_linear_to_rec709 = ocio::XYZ_TO_REC709 * imbuf_scene_linear_to_xyz; - imbuf_rec709_to_scene_linear = math::invert(imbuf_scene_linear_to_rec709); - - imbuf_aces_to_scene_linear = imbuf_xyz_to_scene_linear * ocio::ACES_TO_XYZ; - imbuf_scene_linear_to_aces = math::invert(imbuf_aces_to_scene_linear); - - imbuf_scene_linear_is_rec709 = math::is_equal( - imbuf_scene_linear_to_rec709, float3x3::identity(), 0.0001f); + colormanage_update_matrices(); return ok; } diff --git a/source/blender/imbuf/intern/colormanagement_inline.h b/source/blender/imbuf/intern/colormanagement_inline.h index ceac97a6811..0fc83e965e6 100644 --- a/source/blender/imbuf/intern/colormanagement_inline.h +++ b/source/blender/imbuf/intern/colormanagement_inline.h @@ -94,4 +94,24 @@ void IMB_colormanagement_scene_linear_to_aces(float aces[3], const float scene_l mul_v3_m3v3(aces, imbuf_scene_linear_to_aces.ptr(), scene_linear); } +void IMB_colormanagement_acescg_to_scene_linear(float scene_linear[3], const float acescg[3]) +{ + mul_v3_m3v3(scene_linear, imbuf_acescg_to_scene_linear.ptr(), acescg); +} + +void IMB_colormanagement_scene_linear_to_acescg(float acescg[3], const float scene_linear[3]) +{ + mul_v3_m3v3(acescg, imbuf_scene_linear_to_acescg.ptr(), scene_linear); +} + +void IMB_colormanagement_rec2020_to_scene_linear(float scene_linear[3], const float rec2020[3]) +{ + mul_v3_m3v3(scene_linear, imbuf_rec2020_to_scene_linear.ptr(), rec2020); +} + +void IMB_colormanagement_scene_linear_to_rec2020(float rec2020[3], const float scene_linear[3]) +{ + mul_v3_m3v3(rec2020, imbuf_scene_linear_to_rec2020.ptr(), scene_linear); +} + #endif /* __IMB_COLORMANAGEMENT_INLINE_H__ */ diff --git a/source/blender/imbuf/opencolorio/OCIO_matrix.hh b/source/blender/imbuf/opencolorio/OCIO_matrix.hh index aa4c298a870..34900a89061 100644 --- a/source/blender/imbuf/opencolorio/OCIO_matrix.hh +++ b/source/blender/imbuf/opencolorio/OCIO_matrix.hh @@ -15,10 +15,23 @@ static const float3x3 XYZ_TO_REC709{{3.2409699f, -0.9692436f, 0.0556301f}, {-1.5373832f, 1.8759675f, -0.2039770f}, {-0.4986108f, 0.0415551f, 1.0569715f}}; +/* Standard XYZ (D65) to linear Rec.2020 transform. */ +/* Use four-digit constants instead of higher precisions to match sRGB and Rec.2020 standards. + * See PR #141027 for details. */ +static const float3x3 XYZ_TO_REC2020{{1.7166512f, -0.6666844, 0.0176399f}, + {-0.3556708f, 1.6164812f, -0.0427706f}, + {-0.2533663f, 0.0157685f, 0.9421031f}}; + /* Standard ACES to XYZ (D65) transform. * Matches OpenColorIO builtin transform: UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD. */ static const float3x3 ACES_TO_XYZ = {{0.938280f, 0.337369f, 0.001174f}, {-0.004451f, 0.729522f, -0.003711f}, {0.016628f, -0.066890f, 1.091595f}}; +/* Standard ACEScg to XYZ (D65) transform. + * Matches OpenColorIO builtin transform: UTILITY - ACES-AP1_to_CIE-XYZ-D65_BFD. */ +static const float3x3 ACESCG_TO_XYZ = {{0.652238f, 0.267672f, -0.005382f}, + {0.128237f, 0.674340f, 0.001369f}, + {0.169983f, 0.057988f, 1.093071f}}; + } // namespace blender::ocio diff --git a/source/blender/python/mathutils/mathutils_Color.cc b/source/blender/python/mathutils/mathutils_Color.cc index 935f3495b5a..dde38d58f81 100644 --- a/source/blender/python/mathutils/mathutils_Color.cc +++ b/source/blender/python/mathutils/mathutils_Color.cc @@ -195,6 +195,38 @@ static PyObject *Color_from_aces_to_scene_linear(ColorObject *self) return Color_CreatePyObject(col, Py_TYPE(self)); } +PyDoc_STRVAR( + /* Wrap. */ + Color_from_scene_linear_to_acescg_doc, + ".. function:: from_scene_linear_to_acescg()\n" + "\n" + " Convert from scene linear to ACEScg linear color space.\n" + "\n" + " :return: A color in ACEScg linear color space.\n" + " :rtype: :class:`Color`\n"); +static PyObject *Color_from_scene_linear_to_acescg(ColorObject *self) +{ + float col[3]; + IMB_colormanagement_scene_linear_to_acescg(col, self->col); + return Color_CreatePyObject(col, Py_TYPE(self)); +} + +PyDoc_STRVAR( + /* Wrap. */ + Color_from_acescg_to_scene_linear_doc, + ".. function:: from_acescg_to_scene_linear()\n" + "\n" + " Convert from ACEScg linear to scene linear color space.\n" + "\n" + " :return: A color in scene linear color space.\n" + " :rtype: :class:`Color`\n"); +static PyObject *Color_from_acescg_to_scene_linear(ColorObject *self) +{ + float col[3]; + IMB_colormanagement_acescg_to_scene_linear(col, self->col); + return Color_CreatePyObject(col, Py_TYPE(self)); +} + PyDoc_STRVAR( /* Wrap. */ Color_from_scene_linear_to_rec709_linear_doc, @@ -227,6 +259,38 @@ static PyObject *Color_from_rec709_linear_to_scene_linear(ColorObject *self) return Color_CreatePyObject(col, Py_TYPE(self)); } +PyDoc_STRVAR( + /* Wrap. */ + Color_from_scene_linear_to_rec2020_linear_doc, + ".. function:: from_scene_linear_to_rec2020_linear()\n" + "\n" + " Convert from scene linear to Rec.2020 linear color space.\n" + "\n" + " :return: A color in Rec.2020 linear color space.\n" + " :rtype: :class:`Color`\n"); +static PyObject *Color_from_scene_linear_to_rec2020_linear(ColorObject *self) +{ + float col[3]; + IMB_colormanagement_scene_linear_to_rec2020(col, self->col); + return Color_CreatePyObject(col, Py_TYPE(self)); +} + +PyDoc_STRVAR( + /* Wrap. */ + Color_from_rec2020_linear_to_scene_linear_doc, + ".. function:: from_rec2020_linear_to_scene_linear()\n" + "\n" + " Convert from Rec.2020 linear color space to scene linear color space.\n" + "\n" + " :return: A color in scene linear color space.\n" + " :rtype: :class:`Color`\n"); +static PyObject *Color_from_rec2020_linear_to_scene_linear(ColorObject *self) +{ + float col[3]; + IMB_colormanagement_rec2020_to_scene_linear(col, self->col); + return Color_CreatePyObject(col, Py_TYPE(self)); +} + #endif /* !MATH_STANDALONE */ /** \} */ @@ -1220,6 +1284,14 @@ static PyMethodDef Color_methods[] = { (PyCFunction)Color_from_aces_to_scene_linear, METH_NOARGS, Color_from_aces_to_scene_linear_doc}, + {"from_scene_linear_to_acescg", + (PyCFunction)Color_from_scene_linear_to_acescg, + METH_NOARGS, + Color_from_scene_linear_to_acescg_doc}, + {"from_acescg_to_scene_linear", + (PyCFunction)Color_from_acescg_to_scene_linear, + METH_NOARGS, + Color_from_acescg_to_scene_linear_doc}, {"from_scene_linear_to_rec709_linear", (PyCFunction)Color_from_scene_linear_to_rec709_linear, METH_NOARGS, @@ -1228,6 +1300,14 @@ static PyMethodDef Color_methods[] = { (PyCFunction)Color_from_rec709_linear_to_scene_linear, METH_NOARGS, Color_from_rec709_linear_to_scene_linear_doc}, + {"from_scene_linear_to_rec2020_linear", + (PyCFunction)Color_from_scene_linear_to_rec2020_linear, + METH_NOARGS, + Color_from_scene_linear_to_rec2020_linear_doc}, + {"from_rec2020_linear_to_scene_linear", + (PyCFunction)Color_from_rec2020_linear_to_scene_linear, + METH_NOARGS, + Color_from_rec2020_linear_to_scene_linear_doc}, #endif /* !MATH_STANDALONE */ {nullptr, nullptr, 0, nullptr},