Refactor: Cycles: Colorspace changes to prepare for texture cache

* Move code into util/ module to be reused by tx generator.
* Move logic from ShaderManager into colorspace.cpp.
* Improve naming distinction between scene linear and data, and sRGB and
  scene linear compressed as sRGB.
* Use interop ID instead of enum for known scene linear color spaces.
* Support colorspace conversion on image buffers with arbitrary y stride.

Pull Request: https://projects.blender.org/blender/blender/pulls/152665
This commit is contained in:
Brecht Van Lommel 2026-01-10 17:42:29 +01:00
parent 78efebec1e
commit ca4458f6ea
16 changed files with 946 additions and 744 deletions

View file

@ -47,12 +47,14 @@ bool BlenderImageLoader::load_metadata(const ImageDeviceFeatures & /*features*/,
ImageMetaData &metadata)
{
bool is_float = false;
bool is_data = false;
{
void *lock;
blender::ImBuf *ibuf = BKE_image_acquire_ibuf(b_image, &b_iuser, &lock);
if (ibuf) {
is_float = ibuf->float_buffer.data != nullptr;
is_data = ibuf->colormanage_flag & blender::IMB_COLORMANAGE_IS_DATA;
metadata.width = ibuf->x;
metadata.height = ibuf->y;
metadata.channels = (is_float) ? ibuf->channels : 4;
@ -76,12 +78,13 @@ bool BlenderImageLoader::load_metadata(const ImageDeviceFeatures & /*features*/,
/* Float images are already converted on the Blender side,
* no need to do anything in Cycles. */
metadata.colorspace = u_colorspace_raw;
metadata.colorspace = (is_data) ? u_colorspace_data : u_colorspace_scene_linear;
}
else {
/* In some cases (e.g. #94135), the colorspace setting in Blender gets updated as part of the
* metadata queries in this function, so update the colorspace setting here. */
metadata.colorspace = b_image->colorspace_settings.name;
metadata.colorspace = (is_data) ? u_colorspace_data :
ustring(b_image->colorspace_settings.name);
metadata.type = IMAGE_DATA_TYPE_BYTE4;
}

View file

@ -13,9 +13,9 @@
#include <cstring>
#include "scene/colorspace.h"
#include "scene/object.h"
#include "util/colorspace.h"
#include "util/log.h"
#include "util/string.h"

View file

@ -13,7 +13,6 @@ set(SRC
background.cpp
bake.cpp
camera.cpp
colorspace.cpp
constant_fold.cpp
devicescene.cpp
film.cpp
@ -56,7 +55,6 @@ set(SRC_HEADERS
bake.h
background.h
camera.h
colorspace.h
constant_fold.h
devicescene.h
film.h
@ -110,16 +108,6 @@ if(WITH_CYCLES_OSL)
)
endif()
if(WITH_OPENCOLORIO)
add_definitions(-DWITH_OCIO)
list(APPEND LIB
bf::dependencies::optional::opencolorio
)
if(WIN32 AND NOT USD_OVERRIDE_OPENCOLORIO)
add_definitions(-DOpenColorIO_SKIP_IMPORTS)
endif()
endif()
if(WITH_OPENVDB)
list(APPEND LIB
bf::dependencies::optional::openvdb

View file

@ -900,7 +900,7 @@ void Camera::set_osl_camera(Scene *scene,
{
#ifdef WITH_OSL
/* Ensure shading system exists before we try to load a shader. */
scene->osl_manager->shading_system_init(scene->shader_manager->get_scene_linear_space());
scene->osl_manager->shading_system_init(scene->shader_manager->get_scene_linear_interop_id());
/* Load the shader. */
const char *hash;

View file

@ -1,528 +0,0 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "scene/colorspace.h"
#include "util/color.h"
#include "util/image.h"
#include "util/log.h"
#include "util/map.h"
#include "util/math.h"
#include "util/thread.h"
#include "util/vector.h"
#ifdef WITH_OCIO
# include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;
#endif
CCL_NAMESPACE_BEGIN
/* Builtin colorspaces. */
ustring u_colorspace_auto;
ustring u_colorspace_raw("__builtin_raw");
ustring u_colorspace_srgb("__builtin_srgb");
/* Cached data. */
#ifdef WITH_OCIO
static thread_mutex cache_colorspaces_mutex;
static unordered_map<ustring, ustring> cached_colorspaces;
static thread_mutex cache_processors_mutex;
static unordered_map<ustring, OCIO::ConstProcessorRcPtr> cached_processors;
static thread_mutex cache_scene_linear_mutex;
static string cache_scene_linear_name;
static void check_invalidate_caches()
{
/* Invalidate cached processors and colorspace, in case Blender changed it.
* Note this should not happen during rendering, all render should be stopped
* before it is changed. */
const thread_scoped_lock cache_scene_linear_lock(cache_scene_linear_mutex);
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_ERROR << "OCIO config error: " << exception.what();
return;
}
const OCIO::ConstColorSpaceRcPtr scene_linear_colorspace = config->getColorSpace("scene_linear");
if (scene_linear_colorspace && cache_scene_linear_name != scene_linear_colorspace->getName()) {
cache_scene_linear_name = scene_linear_colorspace->getName();
{
const thread_scoped_lock cache_processors_lock(cache_processors_mutex);
cached_processors.clear();
}
{
const thread_scoped_lock cache_lock(cache_colorspaces_mutex);
cached_colorspaces.clear();
}
}
}
#endif
ColorSpaceProcessor *ColorSpaceManager::get_processor(ustring colorspace)
{
#ifdef WITH_OCIO
/* Only use this for OpenColorIO color spaces, not the builtin ones. */
assert(colorspace != u_colorspace_srgb && colorspace != u_colorspace_auto);
if (colorspace == u_colorspace_raw) {
return nullptr;
}
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_ERROR << "OCIO config error: " << exception.what();
return nullptr;
}
if (!config) {
return nullptr;
}
check_invalidate_caches();
/* Cache processor until free_memory(), memory overhead is expected to be
* small and the processor is likely to be reused. */
const thread_scoped_lock cache_processors_lock(cache_processors_mutex);
if (cached_processors.find(colorspace) == cached_processors.end()) {
try {
cached_processors[colorspace] = config->getProcessor(colorspace.c_str(), "scene_linear");
}
catch (const OCIO::Exception &exception) {
cached_processors[colorspace] = OCIO::ConstProcessorRcPtr();
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " cannot be converted to scene_linear: " << exception.what();
}
}
const OCIO::Processor *processor = cached_processors[colorspace].get();
return (ColorSpaceProcessor *)processor;
#else
/* No OpenColorIO. */
(void)colorspace;
return nullptr;
#endif
}
bool ColorSpaceManager::colorspace_is_data(ustring colorspace)
{
if (colorspace == u_colorspace_auto || colorspace == u_colorspace_raw ||
colorspace == u_colorspace_srgb)
{
return false;
}
#ifdef WITH_OCIO
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_ERROR << "OCIO config error: " << exception.what();
return false;
}
if (!config) {
return false;
}
try {
const OCIO::ConstColorSpaceRcPtr space = config->getColorSpace(colorspace.c_str());
return space && space->isData();
}
catch (const OCIO::Exception &) {
return false;
}
#else
return false;
#endif
}
ustring ColorSpaceManager::detect_known_colorspace(ustring colorspace,
const char *file_colorspace,
const char *file_format,
bool is_float)
{
if (colorspace == u_colorspace_auto) {
/* Auto detect sRGB or raw if none specified. */
if (is_float) {
const bool srgb = (strcmp(file_colorspace, "sRGB") == 0 ||
strcmp(file_colorspace, "GammaCorrected") == 0 ||
(file_colorspace[0] == '\0' &&
(strcmp(file_format, "png") == 0 || strcmp(file_format, "jpeg") == 0 ||
strcmp(file_format, "tiff") == 0 || strcmp(file_format, "dpx") == 0 ||
strcmp(file_format, "jpeg2000") == 0)));
return srgb ? u_colorspace_srgb : u_colorspace_raw;
}
return u_colorspace_srgb;
}
/* Builtin colorspaces. */
if (colorspace == u_colorspace_srgb || colorspace == u_colorspace_raw) {
return colorspace;
}
/* Use OpenColorIO. */
#ifdef WITH_OCIO
check_invalidate_caches();
{
const thread_scoped_lock cache_lock(cache_colorspaces_mutex);
/* Cached lookup. */
if (cached_colorspaces.find(colorspace) != cached_colorspaces.end()) {
return cached_colorspaces[colorspace];
}
}
/* Detect if it matches a simple builtin colorspace. */
bool is_scene_linear;
bool is_srgb;
is_builtin_colorspace(colorspace, is_scene_linear, is_srgb);
const thread_scoped_lock cache_lock(cache_colorspaces_mutex);
if (is_scene_linear) {
LOG_INFO << "Colorspace " << colorspace.string() << " is no-op";
cached_colorspaces[colorspace] = u_colorspace_raw;
return u_colorspace_raw;
}
if (is_srgb) {
LOG_INFO << "Colorspace " << colorspace.string() << " is sRGB";
cached_colorspaces[colorspace] = u_colorspace_srgb;
return u_colorspace_srgb;
}
/* Verify if we can convert from the requested color space. */
if (!get_processor(colorspace)) {
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_ERROR << "OCIO config error: " << exception.what();
return u_colorspace_raw;
}
if (!config || !config->getColorSpace(colorspace.c_str())) {
LOG_WARNING << "Colorspace " << colorspace.c_str() << " not found, using raw instead";
}
else {
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " can't be converted to scene_linear, using raw instead";
}
cached_colorspaces[colorspace] = u_colorspace_raw;
return u_colorspace_raw;
}
/* Convert to/from colorspace with OpenColorIO. */
LOG_INFO << "Colorspace " << colorspace.string() << " handled through OpenColorIO";
cached_colorspaces[colorspace] = colorspace;
return colorspace;
#else
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " not available, built without OpenColorIO";
return u_colorspace_raw;
#endif
}
void ColorSpaceManager::is_builtin_colorspace(ustring colorspace,
bool &is_scene_linear,
bool &is_srgb)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)get_processor(colorspace);
if (!processor) {
is_scene_linear = false;
is_srgb = false;
return;
}
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
is_scene_linear = true;
is_srgb = true;
for (int i = 0; i < 256; i++) {
const float v = i / 255.0f;
float cR[3] = {v, 0, 0};
float cG[3] = {0, v, 0};
float cB[3] = {0, 0, v};
float cW[3] = {v, v, v};
device_processor->applyRGB(cR);
device_processor->applyRGB(cG);
device_processor->applyRGB(cB);
device_processor->applyRGB(cW);
/* Make sure that there is no channel crosstalk. */
if (fabsf(cR[1]) > 1e-5f || fabsf(cR[2]) > 1e-5f || fabsf(cG[0]) > 1e-5f ||
fabsf(cG[2]) > 1e-5f || fabsf(cB[0]) > 1e-5f || fabsf(cB[1]) > 1e-5f)
{
is_scene_linear = false;
is_srgb = false;
break;
}
/* Make sure that the three primaries combine linearly. */
if (!compare_floats(cR[0], cW[0], 1e-6f, 64) || !compare_floats(cG[1], cW[1], 1e-6f, 64) ||
!compare_floats(cB[2], cW[2], 1e-6f, 64))
{
is_scene_linear = false;
is_srgb = false;
break;
}
/* Make sure that the three channels behave identically. */
if (!compare_floats(cW[0], cW[1], 1e-6f, 64) || !compare_floats(cW[1], cW[2], 1e-6f, 64)) {
is_scene_linear = false;
is_srgb = false;
break;
}
const float out_v = average(make_float3(cW[0], cW[1], cW[2]));
if (!compare_floats(v, out_v, 1e-6f, 64)) {
is_scene_linear = false;
}
if (!compare_floats(color_srgb_to_linear(v), out_v, 1e-4f, 64)) {
is_srgb = false;
}
}
#else
(void)colorspace;
is_scene_linear = false;
is_srgb = false;
#endif
}
#ifdef WITH_OCIO
template<typename T> inline float4 cast_to_float4(T *data)
{
return make_float4(util_image_cast_to_float(data[0]),
util_image_cast_to_float(data[1]),
util_image_cast_to_float(data[2]),
util_image_cast_to_float(data[3]));
}
template<typename T> inline void cast_from_float4(T *data, const float4 value)
{
data[0] = util_image_cast_from_float<T>(value.x);
data[1] = util_image_cast_from_float<T>(value.y);
data[2] = util_image_cast_from_float<T>(value.z);
data[3] = util_image_cast_from_float<T>(value.w);
}
/* Slower versions for other all data types, which needs to convert to float and back. */
template<typename T, bool compress_as_srgb = false>
inline void processor_apply_pixels_rgba(const OCIO::Processor *processor,
T *pixels,
const size_t num_pixels,
const bool ignore_alpha)
{
/* TODO: implement faster version for when we know the conversion
* is a simple matrix transform between linear spaces. In that case
* un-premultiply is not needed. */
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
/* Process large images in chunks to keep temporary memory requirement down. */
const size_t chunk_size = std::min((size_t)(16 * 1024 * 1024), num_pixels);
vector<float4> float_pixels(chunk_size);
for (size_t j = 0; j < num_pixels; j += chunk_size) {
const size_t width = std::min(chunk_size, num_pixels - j);
for (size_t i = 0; i < width; i++) {
float4 value = cast_to_float4(pixels + 4 * (j + i));
if (!ignore_alpha && !(value.w <= 0.0f || value.w == 1.0f)) {
const float inv_alpha = 1.0f / value.w;
value.x *= inv_alpha;
value.y *= inv_alpha;
value.z *= inv_alpha;
}
float_pixels[i] = value;
}
const OCIO::PackedImageDesc desc((float *)float_pixels.data(), width, 1, 4);
device_processor->apply(desc);
for (size_t i = 0; i < width; i++) {
float4 value = float_pixels[i];
if (compress_as_srgb) {
value = color_linear_to_srgb_v4(value);
}
if (!ignore_alpha && !(value.w <= 0.0f || value.w == 1.0f)) {
value.x *= value.w;
value.y *= value.w;
value.z *= value.w;
}
cast_from_float4(pixels + 4 * (j + i), value);
}
}
}
template<typename T, bool compress_as_srgb = false>
inline void processor_apply_pixels_grayscale(const OCIO::Processor *processor,
T *pixels,
const size_t num_pixels)
{
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
/* Process large images in chunks to keep temporary memory requirement down. */
const size_t chunk_size = std::min((size_t)(16 * 1024 * 1024), num_pixels);
vector<float> float_pixels(chunk_size * 3);
for (size_t j = 0; j < num_pixels; j += chunk_size) {
const size_t width = std::min(chunk_size, num_pixels - j);
/* Convert to 3 channels, since that's the minimum required by OpenColorIO. */
{
const T *pixel = pixels + j;
float *fpixel = float_pixels.data();
for (size_t i = 0; i < width; i++, pixel++, fpixel += 3) {
const float f = util_image_cast_to_float<T>(*pixel);
fpixel[0] = f;
fpixel[1] = f;
fpixel[2] = f;
}
}
const OCIO::PackedImageDesc desc((float *)float_pixels.data(), width, 1, 3);
device_processor->apply(desc);
{
T *pixel = pixels + j;
const float *fpixel = float_pixels.data();
for (size_t i = 0; i < width; i++, pixel++, fpixel += 3) {
float f = average(make_float3(fpixel[0], fpixel[1], fpixel[2]));
if (compress_as_srgb) {
f = color_linear_to_srgb(f);
}
*pixel = util_image_cast_from_float<T>(f);
}
}
}
}
#endif
template<typename T>
void ColorSpaceManager::to_scene_linear(ustring colorspace,
T *pixels,
const size_t num_pixels,
bool is_rgba,
bool compress_as_srgb,
bool ignore_alpha)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)get_processor(colorspace);
if (processor) {
if (is_rgba) {
if (compress_as_srgb) {
/* Compress output as sRGB. */
processor_apply_pixels_rgba<T, true>(processor, pixels, num_pixels, ignore_alpha);
}
else {
/* Write output as scene linear directly. */
processor_apply_pixels_rgba<T>(processor, pixels, num_pixels, ignore_alpha);
}
}
else {
if (compress_as_srgb) {
/* Compress output as sRGB. */
processor_apply_pixels_grayscale<T, true>(processor, pixels, num_pixels);
}
else {
/* Write output as scene linear directly. */
processor_apply_pixels_grayscale<T>(processor, pixels, num_pixels);
}
}
}
#else
(void)colorspace;
(void)pixels;
(void)num_pixels;
(void)is_rgba;
(void)compress_as_srgb;
#endif
}
void ColorSpaceManager::to_scene_linear(ColorSpaceProcessor *processor_,
float *pixel,
const int channels)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)processor_;
if (processor) {
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
if (channels == 1) {
float3 rgb = make_float3(pixel[0], pixel[0], pixel[0]);
device_processor->applyRGB(&rgb.x);
pixel[0] = average(rgb);
}
if (channels == 3) {
device_processor->applyRGB(pixel);
}
else if (channels == 4) {
if (pixel[3] == 1.0f || pixel[3] == 0.0f) {
/* Fast path for RGBA. */
device_processor->applyRGB(pixel);
}
else {
/* Un-associate and associate alpha since color management should not
* be affected by transparency. */
const float alpha = pixel[3];
const float inv_alpha = 1.0f / alpha;
pixel[0] *= inv_alpha;
pixel[1] *= inv_alpha;
pixel[2] *= inv_alpha;
device_processor->applyRGB(pixel);
pixel[0] *= alpha;
pixel[1] *= alpha;
pixel[2] *= alpha;
}
}
}
#else
(void)processor_;
(void)pixel;
(void)channels;
#endif
}
void ColorSpaceManager::free_memory()
{
#ifdef WITH_OCIO
map_free_memory(cached_colorspaces);
map_free_memory(cached_processors);
#endif
}
void ColorSpaceManager::init_fallback_config()
{
#ifdef WITH_OCIO
OCIO::SetCurrentConfig(OCIO::Config::CreateRaw());
#endif
}
/* Template instantiations so we don't have to inline functions. */
template void ColorSpaceManager::to_scene_linear(ustring, uchar *, size_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(ustring, ushort *, size_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(ustring, half *, size_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(ustring, float *, size_t, bool, bool, bool);
CCL_NAMESPACE_END

View file

@ -2,14 +2,15 @@
*
* SPDX-License-Identifier: Apache-2.0 */
#include "scene/image.h"
#include "device/device.h"
#include "scene/colorspace.h"
#include "scene/image.h"
#include "scene/image_oiio.h"
#include "scene/image_vdb.h"
#include "scene/scene.h"
#include "scene/stats.h"
#include "util/colorspace.h"
#include "util/image.h"
#include "util/image_impl.h"
#include "util/log.h"
@ -232,7 +233,7 @@ ImageMetaData::ImageMetaData()
height(0),
byte_size(0),
type(IMAGE_DATA_NUM_TYPES),
colorspace(u_colorspace_raw),
colorspace(u_colorspace_scene_linear),
colorspace_file_format(""),
use_transform_3d(false),
compress_as_srgb(false)
@ -259,10 +260,10 @@ void ImageMetaData::detect_colorspace()
colorspace = ColorSpaceManager::detect_known_colorspace(
colorspace, colorspace_file_hint.c_str(), colorspace_file_format, is_float());
if (colorspace == u_colorspace_raw) {
if (colorspace == u_colorspace_scene_linear || colorspace == u_colorspace_data) {
/* Nothing to do. */
}
else if (colorspace == u_colorspace_srgb) {
else if (colorspace == u_colorspace_scene_linear_srgb) {
/* Keep sRGB colorspace stored as sRGB, to save memory and/or loading time
* for the common case of 8bit sRGB images like PNG. */
compress_as_srgb = true;
@ -620,15 +621,18 @@ bool ImageManager::file_load_image(Image *img, const int texture_limit)
}
}
if (img->metadata.colorspace != u_colorspace_raw &&
img->metadata.colorspace != u_colorspace_srgb)
if (img->metadata.colorspace != u_colorspace_scene_linear &&
img->metadata.colorspace != u_colorspace_scene_linear_srgb &&
img->metadata.colorspace != u_colorspace_data)
{
/* Convert to scene linear. */
const bool ignore_alpha = img->params.alpha_type == IMAGE_ALPHA_IGNORE ||
img->params.alpha_type == IMAGE_ALPHA_CHANNEL_PACKED;
ColorSpaceManager::to_scene_linear(img->metadata.colorspace,
pixels,
num_pixels,
width,
height,
width * (is_rgba ? 4 : 1),
is_rgba,
img->metadata.compress_as_srgb,
ignore_alpha);

View file

@ -6,8 +6,7 @@
#include "device/memory.h"
#include "scene/colorspace.h"
#include "util/colorspace.h"
#include "util/string.h"
#include "util/thread.h"
#include "util/transform.h"
@ -38,7 +37,7 @@ class ImageParams {
ustring colorspace;
float frame = 0.0f;
ImageParams() : colorspace(u_colorspace_raw) {}
ImageParams() : colorspace(u_colorspace_scene_linear) {}
bool operator==(const ImageParams &other) const
{

View file

@ -6,7 +6,6 @@
#include "scene/background.h"
#include "scene/camera.h"
#include "scene/colorspace.h"
#include "scene/light.h"
#include "scene/osl.h"
#include "scene/scene.h"
@ -124,7 +123,7 @@ bool OSLManager::need_update() const
void OSLManager::device_update_pre(Device *device, Scene *scene)
{
if (scene->shader_manager->use_osl() || !scene->camera->script_name.empty()) {
shading_system_init(scene->shader_manager->get_scene_linear_space());
shading_system_init(scene->shader_manager->get_scene_linear_interop_id());
}
if (!need_update()) {
@ -343,7 +342,7 @@ void OSLManager::texture_system_free()
}
}
void OSLManager::shading_system_init(ShaderManager::SceneLinearSpace colorspace)
void OSLManager::shading_system_init(const string &colorspace_interop_id)
{
/* No need to do anything if we already have shading systems. */
if (!ss_map.empty()) {
@ -353,7 +352,7 @@ void OSLManager::shading_system_init(ShaderManager::SceneLinearSpace colorspace)
/* create shading system, shared between different renders to reduce memory usage */
const thread_scoped_lock lock(ss_shared_mutex);
foreach_osl_device(device_, [this, colorspace](Device *sub_device, OSLGlobals *) {
foreach_osl_device(device_, [this, colorspace_interop_id](Device *sub_device, OSLGlobals *) {
const DeviceType device_type = sub_device->info.type;
if (!ss_shared[device_type]) {
@ -385,18 +384,14 @@ void OSLManager::shading_system_init(ShaderManager::SceneLinearSpace colorspace)
ss->attribute("greedyjit", 1);
/* OSL doesn't accept an arbitrary space, so support a few specific spaces. */
switch (colorspace) {
case ShaderManager::SceneLinearSpace::Rec709:
ss->attribute("colorspace", OSL::Strings::Rec709);
break;
case ShaderManager::SceneLinearSpace::Rec2020:
ss->attribute("colorspace", OSL::Strings::HDTV);
break;
case ShaderManager::SceneLinearSpace::ACEScg:
ss->attribute("colorspace", OSL::Strings::ACEScg);
break;
case ShaderManager::SceneLinearSpace::Unknown:
break;
if (colorspace_interop_id == "lin_rec709_scene") {
ss->attribute("colorspace", OSL::Strings::Rec709);
}
else if (colorspace_interop_id == "lin_rec2020_scene") {
ss->attribute("colorspace", OSL::Strings::HDTV);
}
else if (colorspace_interop_id == "lin_ap1_scene") {
ss->attribute("colorspace", OSL::Strings::ACEScg);
}
const char *groupdata_alloc_str = getenv("CYCLES_OSL_GROUPDATA_ALLOC");
@ -758,7 +753,7 @@ OSLNode *OSLShaderManager::osl_node(ShaderGraph *graph,
}
/* Ensure shading system exists before we try to load a shader. */
scene->osl_manager->shading_system_init(scene->shader_manager->get_scene_linear_space());
scene->osl_manager->shading_system_init(scene->shader_manager->get_scene_linear_interop_id());
/* Load shader code. */
const char *hash;

View file

@ -79,7 +79,7 @@ class OSLManager {
const char *shader_load_filepath(string filepath);
OSLShaderInfo *shader_loaded_info(const string &hash);
void shading_system_init(ShaderManager::SceneLinearSpace colorspace);
void shading_system_init(const string &colorspace_interop_id);
OSL::ShadingSystem *get_shading_system(Device *sub_device);
OSL::TextureSystem *get_texture_system();

View file

@ -20,15 +20,11 @@
#include "scene/tables.h"
#include "scene/volume.h"
#include "util/colorspace.h"
#include "util/log.h"
#include "util/murmurhash.h"
#include "util/transform.h"
#ifdef WITH_OCIO
# include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;
#endif
#include "scene/shader.tables"
CCL_NAMESPACE_BEGIN
@ -734,7 +730,7 @@ void ShaderManager::device_update_common(Device * /*device*/,
kfilm->rec709_to_r = make_float4(rec709_to_r);
kfilm->rec709_to_g = make_float4(rec709_to_g);
kfilm->rec709_to_b = make_float4(rec709_to_b);
kfilm->is_rec709 = scene_linear_space == SceneLinearSpace::Rec709;
kfilm->is_rec709 = scene_linear_interop_id == "lin_rec709_scene";
}
void ShaderManager::device_free_common(Device * /*device*/, DeviceScene *dscene, Scene *scene)
@ -919,37 +915,6 @@ bool ShaderManager::need_update() const
return update_flags != UPDATE_NONE;
}
#ifdef WITH_OCIO
static bool to_scene_linear_transform(OCIO::ConstConfigRcPtr &config,
const char *colorspace,
Transform &to_scene_linear)
{
OCIO::ConstProcessorRcPtr processor;
try {
processor = config->getProcessor("scene_linear", colorspace);
}
catch (OCIO::Exception &) {
return false;
}
if (!processor) {
return false;
}
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
if (!device_processor) {
return false;
}
to_scene_linear = transform_identity();
device_processor->applyRGB(&to_scene_linear.x.x);
device_processor->applyRGB(&to_scene_linear.y.x);
device_processor->applyRGB(&to_scene_linear.z.x);
to_scene_linear = transform_transposed_inverse(to_scene_linear);
return true;
}
#endif
void ShaderManager::compute_thin_film_table(const Transform &xyz_to_rgb)
{
/* Our implementation of Thin Film Fresnel is based on
@ -1015,84 +980,21 @@ void ShaderManager::compute_thin_film_table(const Transform &xyz_to_rgb)
void ShaderManager::init_xyz_transforms()
{
/* Default to ITU-BT.709 in case no appropriate transform found.
* Note XYZ here is defined as having a D65 white point. */
const Transform xyz_to_rec709 = make_transform(3.2404542f,
-1.5371385f,
-0.4985314f,
0.0f,
-0.9692660f,
1.8760108f,
0.0415560f,
0.0f,
0.0556434f,
-0.2040259f,
1.0572252f,
0.0f);
const Transform xyz_to_rgb = ColorSpaceManager::get_xyz_to_scene_linear_rgb();
xyz_to_r = make_float3(xyz_to_rec709.x);
xyz_to_g = make_float3(xyz_to_rec709.y);
xyz_to_b = make_float3(xyz_to_rec709.z);
rgb_to_y = make_float3(0.2126729f, 0.7151522f, 0.0721750f);
white_xyz = make_float3(0.95047f, 1.0f, 1.08883f);
scene_linear_interop_id = ColorSpaceManager::get_scene_linear_interop_id();
rec709_to_r = make_float3(1.0f, 0.0f, 0.0f);
rec709_to_g = make_float3(0.0f, 1.0f, 0.0f);
rec709_to_b = make_float3(0.0f, 0.0f, 1.0f);
scene_linear_space = SceneLinearSpace::Rec709;
compute_thin_film_table(xyz_to_rec709);
#ifdef WITH_OCIO
/* Get from OpenColorO config if it has the required roles. */
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (OCIO::Exception &exception) {
LOG_WARNING << "OCIO config error: " << exception.what();
return;
}
if (!(config && config->hasRole("scene_linear"))) {
return;
}
Transform xyz_to_rgb;
if (config->hasRole("aces_interchange")) {
/* Standard OpenColorIO role, defined as ACES AP0 (ACES2065-1). */
Transform aces_to_rgb;
if (!to_scene_linear_transform(config, "aces_interchange", aces_to_rgb)) {
return;
}
/* This is the OpenColorIO builtin transform:
* UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD. */
const Transform ACES_AP0_to_xyz_D65 = make_transform(0.938280f,
-0.004451f,
0.016628f,
0.000000f,
0.337369f,
0.729522f,
-0.066890f,
0.000000f,
0.001174f,
-0.003711f,
1.091595f,
0.000000f);
const Transform xyz_to_aces = transform_inverse(ACES_AP0_to_xyz_D65);
xyz_to_rgb = aces_to_rgb * xyz_to_aces;
}
else if (config->hasRole("XYZ")) {
/* Custom role used before the standard existed. */
if (!to_scene_linear_transform(config, "XYZ", xyz_to_rgb)) {
return;
}
if (scene_linear_interop_id == "lin_rec709_scene") {
rec709_to_r = make_float3(1.0f, 0.0f, 0.0f);
rec709_to_g = make_float3(0.0f, 1.0f, 0.0f);
rec709_to_b = make_float3(0.0f, 0.0f, 1.0f);
}
else {
/* No reference role found to determine XYZ. */
return;
const Transform xyz_to_rec709 = ColorSpaceManager::get_xyz_to_rec709();
const Transform rec709_to_rgb = xyz_to_rgb * transform_inverse(xyz_to_rec709);
rec709_to_r = make_float3(rec709_to_rgb.x);
rec709_to_g = make_float3(rec709_to_rgb.y);
rec709_to_b = make_float3(rec709_to_rgb.z);
}
xyz_to_r = make_float3(xyz_to_rgb.x);
@ -1103,51 +1005,7 @@ void ShaderManager::init_xyz_transforms()
rgb_to_y = make_float3(rgb_to_xyz.y);
white_xyz = transform_direction(&rgb_to_xyz, one_float3());
const Transform rec709_to_rgb = xyz_to_rgb * transform_inverse(xyz_to_rec709);
rec709_to_r = make_float3(rec709_to_rgb.x);
rec709_to_g = make_float3(rec709_to_rgb.y);
rec709_to_b = make_float3(rec709_to_rgb.z);
compute_thin_film_table(xyz_to_rgb);
const Transform xyz_to_rec2020 = make_transform(1.7166512f,
-0.3556708f,
-0.2533663f,
0.0f,
-0.6666844,
1.6164812f,
0.0157685f,
0.0f,
0.0176399f,
-0.0427706f,
0.9421031f,
0.0f);
const Transform acescg_to_xyz = make_transform(0.652238f,
0.128237f,
0.169983f,
0.0f,
0.267672f,
0.674340f,
0.057988f,
0.0f,
-0.005382f,
0.001369f,
1.093071f,
0.0f);
if (transform_equal_threshold(xyz_to_rgb, xyz_to_rec709, 0.001f)) {
scene_linear_space = SceneLinearSpace::Rec709;
}
else if (transform_equal_threshold(xyz_to_rgb, xyz_to_rec2020, 0.001f)) {
scene_linear_space = SceneLinearSpace::Rec2020;
}
else if (transform_equal_threshold(rgb_to_xyz, acescg_to_xyz, 0.001f)) {
scene_linear_space = SceneLinearSpace::ACEScg;
}
else {
scene_linear_space = SceneLinearSpace::Unknown;
}
#endif
}
size_t ShaderManager::ensure_bsdf_table_impl(DeviceScene *dscene,

View file

@ -224,11 +224,9 @@ class ShaderManager {
void init_xyz_transforms();
enum class SceneLinearSpace { Rec709, Rec2020, ACEScg, Unknown };
SceneLinearSpace get_scene_linear_space()
const string &get_scene_linear_interop_id()
{
return scene_linear_space;
return scene_linear_interop_id;
}
protected:
@ -254,7 +252,7 @@ class ShaderManager {
float3 rec709_to_r;
float3 rec709_to_g;
float3 rec709_to_b;
SceneLinearSpace scene_linear_space;
string scene_linear_interop_id;
vector<float> thin_film_table;
template<std::size_t n>

View file

@ -5,7 +5,6 @@
#include "scene/shader_nodes.h"
#include "kernel/svm/types.h"
#include "kernel/types.h"
#include "scene/colorspace.h"
#include "scene/constant_fold.h"
#include "scene/film.h"
#include "scene/image.h"
@ -21,7 +20,7 @@
#include "sky_nishita.h"
#include "util/color.h"
#include "util/colorspace.h"
#include "util/log.h"
#include "util/math_base.h"
#include "util/string.h"
@ -266,7 +265,7 @@ NODE_DEFINE(ImageTextureNode)
ImageTextureNode::ImageTextureNode() : ImageSlotTextureNode(get_node_type())
{
colorspace = u_colorspace_raw;
colorspace = u_colorspace_scene_linear;
animated = false;
}
@ -466,7 +465,7 @@ void ImageTextureNode::compile(OSLCompiler &compiler)
if (handle.svm_slot() == -1) {
compiler.parameter_texture(
"filename", filename, compress_as_srgb ? u_colorspace_raw : known_colorspace);
"filename", filename, compress_as_srgb ? u_colorspace_scene_linear : known_colorspace);
}
else {
compiler.parameter_texture("filename", handle);
@ -535,7 +534,7 @@ NODE_DEFINE(EnvironmentTextureNode)
EnvironmentTextureNode::EnvironmentTextureNode() : ImageSlotTextureNode(get_node_type())
{
colorspace = u_colorspace_raw;
colorspace = u_colorspace_scene_linear;
animated = false;
}
@ -618,7 +617,7 @@ void EnvironmentTextureNode::compile(OSLCompiler &compiler)
if (handle.svm_slot() == -1) {
compiler.parameter_texture(
"filename", filename, compress_as_srgb ? u_colorspace_raw : known_colorspace);
"filename", filename, compress_as_srgb ? u_colorspace_scene_linear : known_colorspace);
}
else {
compiler.parameter_texture("filename", handle);

View file

@ -6,7 +6,6 @@
#include "device/device.h"
#include "scene/colorspace.h"
#include "scene/scene.h"
#include "scene/shader_graph.h"
#include "scene/shader_nodes.h"

View file

@ -12,6 +12,7 @@ set(INC_SYS
set(SRC
aligned_malloc.cpp
colorspace.cpp
debug.cpp
ies.cpp
log.cpp
@ -45,6 +46,7 @@ set(SRC_HEADERS
atomic.h
boundbox.h
color.h
colorspace.h
concurrent_set.h
concurrent_vector.h
debug.h
@ -150,4 +152,14 @@ if(WITH_OPENVDB)
endif()
endif()
if(WITH_OPENCOLORIO)
add_definitions(-DWITH_OCIO)
list(APPEND LIB
bf::dependencies::optional::opencolorio
)
if(WIN32 AND NOT USD_OVERRIDE_OPENCOLORIO)
add_definitions(-DOpenColorIO_SKIP_IMPORTS)
endif()
endif()
cycles_add_library(cycles_util "${LIB}" ${SRC} ${SRC_HEADERS})

View file

@ -0,0 +1,850 @@
/* SPDX-FileCopyrightText: 2011-2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "util/colorspace.h"
#include "util/color.h"
#include "util/image.h"
#include "util/log.h"
#include "util/map.h"
#include "util/math.h"
#include "util/string.h"
#include "util/thread.h"
#include "util/transform.h"
#include "util/vector.h"
#ifdef WITH_OCIO
# include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;
#endif
CCL_NAMESPACE_BEGIN
/* Builtin colorspaces. */
ustring u_colorspace_auto;
ustring u_colorspace_data("data");
ustring u_colorspace_scene_linear("scene_linear");
ustring u_colorspace_scene_linear_srgb("scene_linear_srgb");
ustring u_colorspace_srgb("__builtin_srgb");
/* Cached data. */
#ifdef WITH_OCIO
static thread_mutex cache_processors_mutex;
static unordered_map<ustring, OCIO::ConstProcessorRcPtr> cache_processors;
static thread_mutex cache_colorspaces_mutex;
static unordered_map<ustring, ustring> cached_colorspaces;
static thread_mutex cache_scene_linear_interop_id_mutex;
static bool cache_scene_linear_interop_id_done = false;
static const char *cache_scene_linear_interop_id = "";
static const char *cache_scene_linear_srgb_interop_id = "";
#endif
static thread_mutex cache_xyz_to_scene_linear_mutex;
static string cache_xyz_to_scene_linear_hash;
ColorSpaceProcessor *ColorSpaceManager::get_processor(ustring colorspace)
{
#ifdef WITH_OCIO
/* Only use this for OpenColorIO color spaces, not the builtin ones. */
assert(colorspace != u_colorspace_scene_linear_srgb && colorspace != u_colorspace_auto);
if (colorspace == u_colorspace_data || colorspace == u_colorspace_scene_linear) {
return nullptr;
}
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_WARNING << "OCIO config error: " << exception.what();
return nullptr;
}
if (!config) {
return nullptr;
}
/* Cache processor until free_memory(), memory overhead is expected to be
* small and the processor is likely to be reused. */
const thread_scoped_lock cache_processors_lock(cache_processors_mutex);
if (cache_processors.find(colorspace) == cache_processors.end()) {
try {
if (colorspace == u_colorspace_srgb) {
/* Linear Rec.709 to sRGB is handled separately in to_scene_linear, here
* we only need the matrix transform from scene_linear to Linear Rec.709. */
if (strcmp(get_scene_linear_interop_id(), "lin_rec709_scene") == 0) {
cache_processors[colorspace] = nullptr;
}
else {
const Transform rgb_to_rec709 = transform_inverse(get_xyz_to_scene_linear_rgb()) *
get_xyz_to_rec709();
const double data[16] = {rgb_to_rec709.x.x,
rgb_to_rec709.x.y,
rgb_to_rec709.x.z,
rgb_to_rec709.x.w,
rgb_to_rec709.y.x,
rgb_to_rec709.y.y,
rgb_to_rec709.y.z,
rgb_to_rec709.y.w,
rgb_to_rec709.z.x,
rgb_to_rec709.z.y,
rgb_to_rec709.z.z,
rgb_to_rec709.z.w,
0.0f,
0.0f,
0.0f,
1.0f};
OCIO::MatrixTransformRcPtr tfm = OCIO::MatrixTransform::Create();
tfm->setMatrix(data);
cache_processors[colorspace] = config->getProcessor(tfm, OCIO::TRANSFORM_DIR_FORWARD);
}
}
else {
cache_processors[colorspace] = config->getProcessor(colorspace.c_str(), "scene_linear");
}
}
catch (const OCIO::Exception &exception) {
cache_processors[colorspace] = OCIO::ConstProcessorRcPtr();
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " can't be converted to scene_linear: " << exception.what();
}
}
const OCIO::Processor *processor = cache_processors[colorspace].get();
return (ColorSpaceProcessor *)processor;
#else
/* No OpenColorIO. */
(void)colorspace;
return nullptr;
#endif
}
bool ColorSpaceManager::colorspace_is_data(ustring colorspace)
{
if (colorspace == u_colorspace_data) {
return true;
}
if (colorspace == u_colorspace_auto || colorspace == u_colorspace_scene_linear ||
colorspace == u_colorspace_scene_linear_srgb || colorspace == u_colorspace_srgb)
{
return false;
}
#ifdef WITH_OCIO
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_WARNING << "OCIO config error: " << exception.what();
return false;
}
if (!config) {
return false;
}
try {
const OCIO::ConstColorSpaceRcPtr space = config->getColorSpace(colorspace.c_str());
return space && space->isData();
}
catch (const OCIO::Exception &) {
return false;
}
#else
return false;
#endif
}
const char *ColorSpaceManager::colorspace_interop_id(ustring colorspace)
{
if (colorspace == u_colorspace_auto) {
return nullptr;
}
if (colorspace == u_colorspace_data) {
return "data";
}
if (colorspace == u_colorspace_srgb) {
return "srgb_rec709_scene";
}
if (colorspace == u_colorspace_scene_linear) {
const char *interop_id = get_scene_linear_interop_id(false);
if (!strcmp(interop_id, "unknown")) {
return interop_id;
}
}
else if (colorspace == u_colorspace_scene_linear_srgb) {
const char *interop_id = get_scene_linear_interop_id(true);
if (!strcmp(interop_id, "unknown")) {
return interop_id;
}
}
#ifdef WITH_OCIO
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
if (!config) {
return nullptr;
}
}
catch (const OCIO::Exception &) {
return nullptr;
}
try {
const OCIO::ConstColorSpaceRcPtr space = config->getColorSpace(colorspace.c_str());
if (space) {
if (space->isData()) {
return "data";
}
/* From https://github.com/AcademySoftwareFoundation/ColorInterop. */
/* TODO: Use native interop ID support in OpenColorIO 2.5. */
const char *interop_ids[] = {"lin_rec709_scene",
"lin_p3d65_scene",
"lin_rec2020_scene",
"lin_adobergb_scene",
"lin_ap1_scene",
"lin_ap0_scene",
"lin_ciexyzd65_scene",
"srgb_rec709_scene",
"g22_rec709_scene",
"g18_rec709_scene",
"srgb_ap1_scene",
"g22_ap1_scene",
"srgb_p3d65_scene",
"g22_adobergb_scene"};
for (const char *interop_id : interop_ids) {
if (space->hasAlias(interop_id)) {
return interop_id;
}
}
}
}
catch (const OCIO::Exception &) {
return nullptr;
}
#endif
return nullptr;
}
ustring ColorSpaceManager::detect_known_colorspace(ustring colorspace,
const char *file_colorspace,
const char *file_format,
bool is_float)
{
if (colorspace == u_colorspace_auto) {
/* Auto detect sRGB or raw if none specified. */
if (is_float) {
const bool srgb = (strcmp(file_colorspace, "sRGB") == 0 ||
strcmp(file_colorspace, "GammaCorrected") == 0 ||
(file_colorspace[0] == '\0' &&
(strcmp(file_format, "png") == 0 || strcmp(file_format, "jpeg") == 0 ||
strcmp(file_format, "tiff") == 0 || strcmp(file_format, "dpx") == 0 ||
strcmp(file_format, "jpeg2000") == 0)));
return srgb ? u_colorspace_srgb : u_colorspace_scene_linear;
}
return u_colorspace_srgb;
}
/* Builtin colorspaces. */
if (colorspace == u_colorspace_srgb || colorspace == u_colorspace_scene_linear ||
colorspace == u_colorspace_scene_linear_srgb || colorspace == u_colorspace_data)
{
return colorspace;
}
if (colorspace_is_data(colorspace)) {
return u_colorspace_data;
}
/* Use OpenColorIO. */
#ifdef WITH_OCIO
{
const thread_scoped_lock cache_lock(cache_colorspaces_mutex);
/* Cached lookup. */
if (cached_colorspaces.find(colorspace) != cached_colorspaces.end()) {
return cached_colorspaces[colorspace];
}
}
/* Detect if it matches a simple builtin colorspace. */
bool is_scene_linear;
bool is_scene_linear_srgb;
is_builtin_colorspace(colorspace, is_scene_linear, is_scene_linear_srgb);
const thread_scoped_lock cache_lock(cache_colorspaces_mutex);
if (is_scene_linear) {
LOG_INFO << "Colorspace " << colorspace.string() << " is no-op";
cached_colorspaces[colorspace] = u_colorspace_scene_linear;
return u_colorspace_scene_linear;
}
if (is_scene_linear_srgb) {
LOG_INFO << "Colorspace " << colorspace.string() << " is scene linear sRGB";
cached_colorspaces[colorspace] = u_colorspace_scene_linear_srgb;
return u_colorspace_scene_linear_srgb;
}
/* Verify if we can convert from the requested color space. */
if (!get_processor(colorspace)) {
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (const OCIO::Exception &exception) {
LOG_WARNING << "OCIO config error: " << exception.what();
return u_colorspace_scene_linear;
}
if (!config || !config->getColorSpace(colorspace.c_str())) {
LOG_WARNING << "Colorspace " << colorspace.c_str() << " not found, using raw instead";
}
else {
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " can't be converted to scene_linear, using raw instead";
}
cached_colorspaces[colorspace] = u_colorspace_scene_linear;
return u_colorspace_scene_linear;
}
/* Convert to/from colorspace with OpenColorIO. */
LOG_INFO << "Colorspace " << colorspace.string() << " handled through OpenColorIO";
cached_colorspaces[colorspace] = colorspace;
return colorspace;
#else
LOG_WARNING << "Colorspace " << colorspace.c_str()
<< " not available, built without OpenColorIO";
return u_colorspace_scene_linear;
#endif
}
void ColorSpaceManager::is_builtin_colorspace(ustring colorspace,
bool &is_scene_linear,
bool &is_scene_linear_srgb)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)get_processor(colorspace);
if (!processor) {
is_scene_linear = false;
is_scene_linear_srgb = false;
return;
}
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
is_scene_linear = true;
is_scene_linear_srgb = true;
for (int i = 0; i < 256; i++) {
const float v = i / 255.0f;
float cR[3] = {v, 0, 0};
float cG[3] = {0, v, 0};
float cB[3] = {0, 0, v};
float cW[3] = {v, v, v};
device_processor->applyRGB(cR);
device_processor->applyRGB(cG);
device_processor->applyRGB(cB);
device_processor->applyRGB(cW);
/* Make sure that there is no channel crosstalk. */
if (fabsf(cR[1]) > 1e-5f || fabsf(cR[2]) > 1e-5f || fabsf(cG[0]) > 1e-5f ||
fabsf(cG[2]) > 1e-5f || fabsf(cB[0]) > 1e-5f || fabsf(cB[1]) > 1e-5f)
{
is_scene_linear = false;
is_scene_linear_srgb = false;
break;
}
/* Make sure that the three primaries combine linearly. */
if (!compare_floats(cR[0], cW[0], 1e-6f, 64) || !compare_floats(cG[1], cW[1], 1e-6f, 64) ||
!compare_floats(cB[2], cW[2], 1e-6f, 64))
{
is_scene_linear = false;
is_scene_linear_srgb = false;
break;
}
/* Make sure that the three channels behave identically. */
if (!compare_floats(cW[0], cW[1], 1e-6f, 64) || !compare_floats(cW[1], cW[2], 1e-6f, 64)) {
is_scene_linear = false;
is_scene_linear_srgb = false;
break;
}
const float out_v = average(make_float3(cW[0], cW[1], cW[2]));
if (!compare_floats(v, out_v, 1e-6f, 64)) {
is_scene_linear = false;
}
if (!compare_floats(color_srgb_to_linear(v), out_v, 1e-4f, 64)) {
is_scene_linear_srgb = false;
}
}
#else
(void)colorspace;
is_scene_linear = false;
is_scene_linear_srgb = false;
#endif
}
#ifdef WITH_OCIO
template<typename T> inline float4 cast_to_float4(T *data)
{
return make_float4(util_image_cast_to_float(data[0]),
util_image_cast_to_float(data[1]),
util_image_cast_to_float(data[2]),
util_image_cast_to_float(data[3]));
}
template<typename T> inline void cast_from_float4(T *data, const float4 value)
{
data[0] = util_image_cast_from_float<T>(value.x);
data[1] = util_image_cast_from_float<T>(value.y);
data[2] = util_image_cast_from_float<T>(value.z);
data[3] = util_image_cast_from_float<T>(value.w);
}
/* Slower versions for other all data types, which needs to convert to float and back. */
template<typename T, bool compress_as_srgb = false>
inline void processor_apply_pixels_rgba(const OCIO::Processor *processor,
T *pixels,
const int64_t width,
const int64_t height,
const int64_t y_stride,
const bool ignore_alpha)
{
/* TODO: implement faster version for when we know the conversion
* is a simple matrix transform between linear spaces. In that case
* un-premultiply is not needed. */
const OCIO::ConstCPUProcessorRcPtr device_processor = (processor) ?
processor->getDefaultCPUProcessor() :
nullptr;
/* Process large images in chunks to keep temporary memory requirement down. */
const int64_t chunk_rows = divide_up(std::min((int64_t)(16 * 1024 * 1024), width * height),
width);
vector<float4> float_pixels(chunk_rows * width);
for (int64_t row = 0; row < height; row += chunk_rows) {
const int64_t num_rows = std::min(chunk_rows, height - row);
for (int64_t j = 0; j < num_rows; j++) {
T *pixel = pixels + (row + j) * y_stride;
float4 *float_pixel = float_pixels.data() + j * width;
for (int64_t i = 0; i < width; i++, pixel += 4, float_pixel++) {
float4 value = cast_to_float4(pixel);
if (!ignore_alpha && !(value.w <= 0.0f || value.w == 1.0f)) {
const float inv_alpha = 1.0f / value.w;
value.x *= inv_alpha;
value.y *= inv_alpha;
value.z *= inv_alpha;
}
*float_pixel = value;
}
}
if (processor) {
const OCIO::PackedImageDesc desc((float *)float_pixels.data(), num_rows * width, 1, 4);
device_processor->apply(desc);
}
for (int64_t j = 0; j < num_rows; j++) {
T *pixel = pixels + (row + j) * y_stride;
float4 *float_pixel = float_pixels.data() + j * width;
for (int64_t i = 0; i < width; i++, pixel += 4, float_pixel++) {
float4 value = *float_pixel;
if (compress_as_srgb) {
value = color_linear_to_srgb_v4(value);
}
if (!ignore_alpha && !(value.w <= 0.0f || value.w == 1.0f)) {
value.x *= value.w;
value.y *= value.w;
value.z *= value.w;
}
cast_from_float4(pixel, value);
}
}
}
}
template<typename T, bool compress_as_srgb = false>
inline void processor_apply_pixels_grayscale(const OCIO::Processor *processor,
T *pixels,
const int64_t width,
const int64_t height,
const int64_t y_stride)
{
const OCIO::ConstCPUProcessorRcPtr device_processor = (processor) ?
processor->getDefaultCPUProcessor() :
nullptr;
/* Process large images in chunks to keep temporary memory requirement down. */
const int64_t chunk_rows = divide_up(std::min((int64_t)(16 * 1024 * 1024), width * height),
width);
vector<float> float_pixels(chunk_rows * width * 3);
for (int64_t row = 0; row < height; row += chunk_rows) {
const int64_t num_rows = std::min(chunk_rows, height - row);
/* Convert to 3 channels, since that's the minimum required by OpenColorIO. */
for (int64_t j = 0; j < num_rows; j++) {
T *pixel = pixels + (row + j) * y_stride;
float *float_pixel = float_pixels.data() + j * width * 3;
for (int64_t i = 0; i < width; i++, pixel++, float_pixel += 3) {
const float f = util_image_cast_to_float<T>(*pixel);
float_pixel[0] = f;
float_pixel[1] = f;
float_pixel[2] = f;
}
}
if (processor) {
const OCIO::PackedImageDesc desc((float *)float_pixels.data(), num_rows * width, 1, 3);
device_processor->apply(desc);
}
for (int64_t j = 0; j < num_rows; j++) {
T *pixel = pixels + (row + j) * y_stride;
float *float_pixel = float_pixels.data() + j * width * 3;
for (int64_t i = 0; i < width; i++, pixel++, float_pixel += 3) {
float f = average(make_float3(float_pixel[0], float_pixel[1], float_pixel[2]));
if (compress_as_srgb) {
f = color_linear_to_srgb(f);
}
*pixel = util_image_cast_from_float<T>(f);
}
}
}
}
#endif
template<typename T>
void ColorSpaceManager::to_scene_linear(ustring colorspace,
T *pixels,
const int64_t width,
const int64_t height,
const int64_t y_stride,
bool is_rgba,
bool compress_as_srgb,
bool ignore_alpha)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)get_processor(colorspace);
/* The processor for sRGB does not include the Linear Rec.709 to sRGB transform, that
* is handled by compress_as_srgb for better performance when scene_linear is Rec.709 */
if (colorspace == u_colorspace_srgb) {
assert(!compress_as_srgb);
compress_as_srgb = true;
}
if (is_rgba) {
if (compress_as_srgb) {
/* Compress output as sRGB. */
processor_apply_pixels_rgba<T, true>(
processor, pixels, width, height, y_stride, ignore_alpha);
}
else {
/* Write output as scene linear directly. */
processor_apply_pixels_rgba<T>(processor, pixels, width, height, y_stride, ignore_alpha);
}
}
else {
if (compress_as_srgb) {
/* Compress output as sRGB. */
processor_apply_pixels_grayscale<T, true>(processor, pixels, width, height, y_stride);
}
else {
/* Write output as scene linear directly. */
processor_apply_pixels_grayscale<T>(processor, pixels, width, height, y_stride);
}
}
#else
(void)colorspace;
(void)pixels;
(void)width;
(void)height;
(void)y_stride;
(void)is_rgba;
(void)compress_as_srgb;
#endif
}
void ColorSpaceManager::to_scene_linear(ColorSpaceProcessor *processor_,
float *pixel,
const int channels)
{
#ifdef WITH_OCIO
const OCIO::Processor *processor = (const OCIO::Processor *)processor_;
if (processor) {
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
if (channels == 1) {
float3 rgb = make_float3(pixel[0], pixel[0], pixel[0]);
device_processor->applyRGB(&rgb.x);
pixel[0] = average(rgb);
}
if (channels == 3) {
device_processor->applyRGB(pixel);
}
else if (channels == 4) {
if (pixel[3] == 1.0f || pixel[3] == 0.0f) {
/* Fast path for RGBA. */
device_processor->applyRGB(pixel);
}
else {
/* Un-associate and associate alpha since color management should not
* be affected by transparency. */
const float alpha = pixel[3];
const float inv_alpha = 1.0f / alpha;
pixel[0] *= inv_alpha;
pixel[1] *= inv_alpha;
pixel[2] *= inv_alpha;
device_processor->applyRGB(pixel);
pixel[0] *= alpha;
pixel[1] *= alpha;
pixel[2] *= alpha;
}
}
}
#else
(void)processor_;
(void)pixel;
(void)channels;
#endif
}
void ColorSpaceManager::free_memory()
{
#ifdef WITH_OCIO
map_free_memory(cached_colorspaces);
map_free_memory(cache_processors);
#endif
}
void ColorSpaceManager::init_fallback_config()
{
#ifdef WITH_OCIO
OCIO::SetCurrentConfig(OCIO::Config::CreateRaw());
#endif
}
#ifdef WITH_OCIO
static bool to_scene_linear_transform(OCIO::ConstConfigRcPtr &config,
const char *colorspace,
Transform &to_scene_linear)
{
OCIO::ConstProcessorRcPtr processor;
try {
processor = config->getProcessor("scene_linear", colorspace);
}
catch (OCIO::Exception &) {
return false;
}
if (!processor) {
return false;
}
const OCIO::ConstCPUProcessorRcPtr device_processor = processor->getDefaultCPUProcessor();
if (!device_processor) {
return false;
}
to_scene_linear = transform_identity();
device_processor->applyRGB(&to_scene_linear.x.x);
device_processor->applyRGB(&to_scene_linear.y.x);
device_processor->applyRGB(&to_scene_linear.z.x);
to_scene_linear = transform_transposed_inverse(to_scene_linear);
return true;
}
#endif
Transform ColorSpaceManager::get_xyz_to_rec709()
{
/* Default to ITU-BT.709 in case no appropriate transform found.
* Note XYZ here is defined as having a D65 white point. */
return make_transform(3.2404542f,
-1.5371385f,
-0.4985314f,
0.0f,
-0.9692660f,
1.8760108f,
0.0415560f,
0.0f,
0.0556434f,
-0.2040259f,
1.0572252f,
0.0f);
}
Transform ColorSpaceManager::get_xyz_to_rec2020()
{
return make_transform(1.7166512f,
-0.3556708f,
-0.2533663f,
0.0f,
-0.6666844,
1.6164812f,
0.0157685f,
0.0f,
0.0176399f,
-0.0427706f,
0.9421031f,
0.0f);
}
Transform ColorSpaceManager::get_xyz_to_acescg()
{
return transform_inverse(make_transform(0.652238f,
0.128237f,
0.169983f,
0.0f,
0.267672f,
0.674340f,
0.057988f,
0.0f,
-0.005382f,
0.001369f,
1.093071f,
0.0f));
}
Transform ColorSpaceManager::get_xyz_to_scene_linear_rgb()
{
Transform xyz_to_rgb = get_xyz_to_rec709();
#ifdef WITH_OCIO
/* Get from OpenColorO config if it has the required roles. */
OCIO::ConstConfigRcPtr config = nullptr;
try {
config = OCIO::GetCurrentConfig();
}
catch (OCIO::Exception &exception) {
LOG_WARNING << "OCIO config error: " << exception.what();
return xyz_to_rgb;
}
if (!(config && config->hasRole("scene_linear"))) {
return xyz_to_rgb;
}
if (config->hasRole("aces_interchange")) {
/* Standard OpenColorIO role, defined as ACES AP0 (ACES2065-1). */
Transform aces_to_rgb;
if (!to_scene_linear_transform(config, "aces_interchange", aces_to_rgb)) {
return xyz_to_rgb;
}
/* This is the OpenColorIO builtin transform:
* UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD. */
const Transform ACES_AP0_to_xyz_D65 = make_transform(0.938280f,
-0.004451f,
0.016628f,
0.000000f,
0.337369f,
0.729522f,
-0.066890f,
0.000000f,
0.001174f,
-0.003711f,
1.091595f,
0.000000f);
const Transform xyz_to_aces = transform_inverse(ACES_AP0_to_xyz_D65);
xyz_to_rgb = aces_to_rgb * xyz_to_aces;
return xyz_to_rgb;
}
if (config->hasRole("XYZ")) {
/* Custom role used before the standard existed. */
if (to_scene_linear_transform(config, "XYZ", xyz_to_rgb)) {
return xyz_to_rgb;
}
}
/* No reference role found to determine XYZ. */
#endif
return xyz_to_rgb;
}
const std::string &ColorSpaceManager::get_xyz_to_scene_linear_rgb_string()
{
/* NOTE: Be careful not to change existing hashes if at all possible, as this
* will cause all texture files to be regenerated with significantly increased
* disk usage. */
const thread_scoped_lock cache_lock(cache_xyz_to_scene_linear_mutex);
if (cache_xyz_to_scene_linear_hash.empty()) {
/* TODO: Verify this results in the same hash across platforms. */
const Transform xyz_to_rgb = get_xyz_to_scene_linear_rgb();
cache_xyz_to_scene_linear_hash = string_printf(
"%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g_%.4g",
xyz_to_rgb.x.x,
xyz_to_rgb.x.y,
xyz_to_rgb.x.z,
xyz_to_rgb.x.w,
xyz_to_rgb.y.x,
xyz_to_rgb.y.y,
xyz_to_rgb.y.z,
xyz_to_rgb.y.w,
xyz_to_rgb.z.x,
xyz_to_rgb.z.y,
xyz_to_rgb.z.z,
xyz_to_rgb.z.w);
}
return cache_xyz_to_scene_linear_hash;
}
const char *ColorSpaceManager::get_scene_linear_interop_id(const bool srgb_encoded)
{
const thread_scoped_lock cache_lock(cache_scene_linear_interop_id_mutex);
if (!cache_scene_linear_interop_id_done) {
const Transform xyz_to_rgb = get_xyz_to_scene_linear_rgb();
if (transform_equal_threshold(xyz_to_rgb, get_xyz_to_rec709(), 0.0001f)) {
cache_scene_linear_interop_id = "lin_rec709_scene";
cache_scene_linear_srgb_interop_id = "srgb_rec709_scene";
}
else if (transform_equal_threshold(xyz_to_rgb, get_xyz_to_rec2020(), 0.0001f)) {
cache_scene_linear_interop_id = "lin_rec2020_scene";
cache_scene_linear_srgb_interop_id = "srgb_rec2020_scene";
}
else if (transform_equal_threshold(xyz_to_rgb, get_xyz_to_acescg(), 0.0001f)) {
cache_scene_linear_interop_id = "lin_ap1_scene";
cache_scene_linear_srgb_interop_id = "srgb_ap1_scene";
}
else {
cache_scene_linear_interop_id = "unknown";
cache_scene_linear_srgb_interop_id = "unknown";
}
cache_scene_linear_interop_id_done = true;
}
return (srgb_encoded) ? cache_scene_linear_srgb_interop_id : cache_scene_linear_interop_id;
}
/* Template instantiations so we don't have to inline functions. */
template void ColorSpaceManager::to_scene_linear(
ustring, uchar *, int64_t, int64_t, int64_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(
ustring, ushort *, int64_t, int64_t, int64_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(
ustring, half *, int64_t, int64_t, int64_t, bool, bool, bool);
template void ColorSpaceManager::to_scene_linear(
ustring, float *, int64_t, int64_t, int64_t, bool, bool, bool);
CCL_NAMESPACE_END

View file

@ -1,18 +1,27 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
/* SPDX-FileCopyrightText: 2011-2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "util/param.h"
#include "util/string.h"
CCL_NAMESPACE_BEGIN
/* Automatically determine colorspace. */
extern ustring u_colorspace_auto;
extern ustring u_colorspace_raw;
/* Non-color data. */
extern ustring u_colorspace_data;
/* Scene linear colorspace used for rendering. */
extern ustring u_colorspace_scene_linear;
/* Scene linear + sRGB transfer function . */
extern ustring u_colorspace_scene_linear_srgb;
/* sRGB. */
extern ustring u_colorspace_srgb;
class ColorSpaceProcessor;
struct Transform;
class ColorSpaceManager {
public:
@ -26,13 +35,17 @@ class ColorSpaceManager {
/* Test if colorspace is for non-color data. */
static bool colorspace_is_data(ustring colorspace);
/* Return color interop forum interop ID. */
static const char *colorspace_interop_id(ustring colorspace);
/* Convert pixels in the specified colorspace to scene linear color for
* rendering. Must be a colorspace returned from detect_known_colorspace. */
template<typename T>
static void to_scene_linear(ustring colorspace,
T *pixels,
const size_t num_pixels,
const int64_t width,
const int64_t height,
const int64_t y_stride,
bool is_rgba,
bool compress_as_srgb,
bool ignore_alpha);
@ -52,8 +65,20 @@ class ColorSpaceManager {
* valid without knowing the actual configuration used by the final application. */
static void init_fallback_config();
/* Compute matrix to convert from XYZ to scene linear RGB, based on the config. */
static Transform get_xyz_to_scene_linear_rgb();
static Transform get_xyz_to_rec709();
static Transform get_xyz_to_rec2020();
static Transform get_xyz_to_acescg();
/* Compute unique string for texture cache hashing and metadata. */
static const string &get_xyz_to_scene_linear_rgb_string();
/* Determine if scene linear is a common known space. */
static const char *get_scene_linear_interop_id(const bool srgb_encoded = false);
private:
static void is_builtin_colorspace(ustring colorspace, bool &is_scene_linear, bool &is_srgb);
static void is_builtin_colorspace(ustring colorspace,
bool &is_scene_linear,
bool &is_scene_linear_srgb);
};
CCL_NAMESPACE_END