Color Management: Add working color space for blend files

* Store scene linear to XYZ conversion matrix in each blend file, along
  with the colorspace name. The matrix is the source of truth. The name
  is currently only used for error logging about unknown color spaces.
* Add Working Space option in color management panel, to change the
  working space for the entire blend file. Changing this will pop up
  a dialog, with a default enabled option to convert all colors in
  the blend file to the new working space. Note this is necessarily only
  an approximation.
* Link and append automatically converts to the color space of the main
  open blend file.
* There is builtin support for Rec.709, Rec.2020 and ACEScg working spaces,
  in addition to the working space of custom OpenColorIO configs.
* Undo of working space for linked datablocks isn't quite correct when going
  to a smaller gamut working space. This can be fixed by reloading the file
  so the linked datablocks are reloaded.

Compatibility with blend files saved with a custom OpenColorIO config
is tricky, as we can not detect this.

* We assume that if the blend file has no information about the scene
  linear color space, it is the default one from the active OCIO config.
  And the same for any blend files linked or appended. This is effectively
  the same behavior as before.
* Now that there is a warning when color spaces are missing, it is more
  likely that a user will notice something is wrong and only save the
  blend file with the correct config active.
* As no automatic working space conversion happens on file load, there is
  an opportunity to correct things by changing the working space with
  "Convert Colors" disabled. This can also be scripted for all blend files
  in a project.

Ref #144911

Pull Request: https://projects.blender.org/blender/blender/pulls/145476
This commit is contained in:
Brecht Van Lommel
2025-08-31 02:58:12 +02:00
parent a4e9e4869d
commit 6a083a5464
32 changed files with 811 additions and 11 deletions

View File

@@ -85,7 +85,34 @@ class RENDER_PT_color_management(RenderButtonsPanel, Panel):
col.prop(view, "exposure")
col.prop(view, "gamma")
col.separator()
class RENDER_PT_color_management_working_space(RenderButtonsPanel, Panel):
bl_label = "Working Space"
bl_parent_id = "RENDER_PT_color_management"
bl_options = {'DEFAULT_CLOSED'}
COMPAT_ENGINES = {
'BLENDER_RENDER',
'BLENDER_EEVEE',
'BLENDER_WORKBENCH',
}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
scene = context.scene
blend_colorspace = context.blend_data.colorspace
flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True)
col = flow.column()
split = col.split(factor=0.4)
row = split.row()
row.label(text="File")
row.alignment = 'RIGHT'
split.operator_menu_enum("wm.set_working_color_space", "working_space", text=blend_colorspace.working_space)
col.prop(scene.sequencer_colorspace_settings, "name", text="Sequencer")
@@ -1112,6 +1139,7 @@ classes = (
RENDER_PT_opengl_film,
RENDER_PT_hydra_debug,
RENDER_PT_color_management,
RENDER_PT_color_management_working_space,
RENDER_PT_color_management_curves,
RENDER_PT_color_management_white_balance_presets,
RENDER_PT_color_management_white_balance,

View File

@@ -11,6 +11,8 @@
#include "BLI_string_ref.hh"
#include "BKE_main.hh"
struct FileData;
struct Library;
struct ListBase;
@@ -55,6 +57,9 @@ struct LibraryRuntime {
/** See BLENDER_FILE_VERSION, BLENDER_FILE_SUBVERSION, needed for do_versions. */
short versionfile = 0;
short subversionfile = 0;
/* Colorspace information. */
MainColorspace colorspace;
};
/**

View File

@@ -25,6 +25,7 @@
#include "DNA_listBase.h"
#include "BLI_compiler_attrs.h"
#include "BLI_math_matrix_types.hh"
#include "BLI_sys_types.h"
#include "BLI_utility_mixins.hh"
#include "BLI_vector_set.hh"
@@ -141,6 +142,14 @@ enum {
};
struct MainColorspace {
/*
* File working colorspace for all scene linear colors.
* The name is only for the user interface and is not a unique identifier, the matrix is
* the XYZ colorspace is the source of truth.
* */
char scene_linear_name[64 /*MAX_COLORSPACE_NAME*/] = "";
blender::float3x3 scene_linear_to_xyz = blender::float3x3::zero();
/*
* A colorspace, view or display was not found, which likely means the OpenColorIO config
* used to create this blend file is missing.

View File

@@ -982,6 +982,9 @@ static void setup_app_data(bContext *C,
reuse_data.old_bmain = bmain;
reuse_data.wm_setup_data = wm_setup_data;
const bool reuse_editable_assets = mode != LOAD_UNDO && !params->is_factory_settings &&
reuse_editable_asset_needed(&reuse_data);
if (mode != LOAD_UNDO) {
const short ui_id_codes[]{ID_WS, ID_SCR};
@@ -1008,7 +1011,7 @@ static void setup_app_data(bContext *C,
BKE_main_idmap_destroy(reuse_data.id_map);
if (!params->is_factory_settings && reuse_editable_asset_needed(&reuse_data)) {
if (reuse_editable_assets) {
unpin_file_local_grease_pencil_brush_materials(&reuse_data);
/* Keep linked brush asset data, similar to UI data. Only does a known
* subset know. Could do everything, but that risks dragging along more
@@ -1229,9 +1232,8 @@ static void setup_app_data(bContext *C,
/* Setting scene might require having a dependency graph, with copy-on-eval
* we need to make sure we ensure scene has correct color management before
* constructing dependency graph. */
if (mode != LOAD_UNDO) {
IMB_colormanagement_check_file_config(bmain);
}
IMB_colormanagement_working_space_check(bmain, mode == LOAD_UNDO, reuse_editable_assets);
IMB_colormanagement_check_file_config(bmain);
BKE_scene_set_background(bmain, curscene);

View File

@@ -38,6 +38,7 @@
#include "BKE_main_namemap.hh"
#include "BKE_report.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
@@ -88,6 +89,7 @@ Main::~Main()
Main *BKE_main_new()
{
Main *bmain = MEM_new<Main>(__func__);
IMB_colormanagement_working_space_init(bmain);
return bmain;
}

View File

@@ -112,6 +112,8 @@
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
#include "IMB_colormanagement.hh"
#include "readfile.hh"
#include "versioning_common.hh"
@@ -444,6 +446,7 @@ void blo_split_main(Main *bmain)
libmain->has_forward_compatibility_issues = !MAIN_VERSION_FILE_OLDER_OR_EQUAL(
libmain, BLENDER_FILE_VERSION, BLENDER_FILE_SUBVERSION);
libmain->is_asset_edit_file = (lib->runtime->tag & LIBRARY_IS_ASSET_EDIT_FILE) != 0;
libmain->colorspace = lib->runtime->colorspace;
bmain->split_mains->add_new(libmain);
libmain->split_mains = bmain->split_mains;
lib->runtime->temp_index = i;
@@ -464,7 +467,7 @@ void blo_split_main(Main *bmain)
MEM_freeN(lib_main_array);
}
static void read_file_version(FileData *fd, Main *main)
static void read_file_version_and_colorspace(FileData *fd, Main *main)
{
BHead *bhead;
@@ -485,6 +488,9 @@ static void read_file_version(FileData *fd, Main *main)
main->has_forward_compatibility_issues = !MAIN_VERSION_FILE_OLDER_OR_EQUAL(
main, BLENDER_FILE_VERSION, BLENDER_FILE_SUBVERSION);
main->is_asset_edit_file = (fg->fileflags & G_FILE_ASSET_EDIT_FILE) != 0;
STRNCPY(main->colorspace.scene_linear_name, fg->colorspace_scene_linear_name);
main->colorspace.scene_linear_to_xyz = blender::float3x3(
fg->colorspace_scene_linear_to_xyz);
MEM_freeN(fg);
}
else if (bhead->code == BLO_CODE_ENDB) {
@@ -497,6 +503,7 @@ static void read_file_version(FileData *fd, Main *main)
main->curlib->runtime->subversionfile = main->subversionfile;
SET_FLAG_FROM_TEST(
main->curlib->runtime->tag, main->is_asset_edit_file, LIBRARY_IS_ASSET_EDIT_FILE);
main->curlib->runtime->colorspace = main->colorspace;
}
}
@@ -588,7 +595,7 @@ static Main *blo_find_main(FileData *fd, const char *filepath, const char *relab
m->curlib = lib;
read_file_version(fd, m);
read_file_version_and_colorspace(fd, m);
if (G.debug & G_DEBUG) {
CLOG_DEBUG(&LOG, "Added new lib %s", filepath);
@@ -3228,6 +3235,10 @@ static BHead *read_global(BlendFileData *bfd, FileData *fd, BHead *bhead)
STRNCPY(bfd->main->build_hash, fg->build_hash);
bfd->main->is_asset_edit_file = (fg->fileflags & G_FILE_ASSET_EDIT_FILE) != 0;
STRNCPY(bfd->main->colorspace.scene_linear_name, fg->colorspace_scene_linear_name);
bfd->main->colorspace.scene_linear_to_xyz = blender::float3x3(
fg->colorspace_scene_linear_to_xyz);
bfd->fileflags = fg->fileflags;
bfd->globalf = fg->globalf;
@@ -4004,6 +4015,7 @@ BlendFileData *blo_read_file_internal(FileData *fd, const char *filepath)
mainvar->curlib->runtime->filedata :
fd,
mainvar);
IMB_colormanagement_working_space_convert(mainvar, bfd->main);
}
blo_join_main(bfd->main);
@@ -4597,7 +4609,7 @@ static Main *library_link_begin(Main *mainvar,
/* needed for do_version */
mainl->versionfile = short(fd->fileversion);
read_file_version(fd, mainl);
read_file_version_and_colorspace(fd, mainl);
read_file_bhead_idname_map_create(fd);
return mainl;
@@ -4646,6 +4658,7 @@ static void split_main_newid(Main *mainptr, Main *main_newid)
main_newid->subversionfile = mainptr->subversionfile;
STRNCPY(main_newid->filepath, mainptr->filepath);
main_newid->curlib = mainptr->curlib;
main_newid->colorspace = mainptr->colorspace;
MainListsArray lbarray = BKE_main_lists_get(*mainptr);
MainListsArray lbarray_newid = BKE_main_lists_get(*main_newid);
@@ -4728,6 +4741,7 @@ static void library_link_end(Main *mainl, FileData **fd, const int flag, ReportL
main_newid->curlib->runtime->filedata :
*fd,
main_newid);
IMB_colormanagement_working_space_convert(main_newid, mainvar);
add_main_to_main(mainlib, main_newid);
@@ -5037,7 +5051,7 @@ static FileData *read_library_file_data(FileData *basefd, Main *bmain, Main *lib
lib_bmain->versionfile = fd->fileversion;
/* subversion */
read_file_version(fd, lib_bmain);
read_file_version_and_colorspace(fd, lib_bmain);
read_file_bhead_idname_map_create(fd);
}
else {
@@ -5047,6 +5061,7 @@ static FileData *read_library_file_data(FileData *basefd, Main *bmain, Main *lib
/* Set lib version to current main one... Makes assert later happy. */
lib_bmain->versionfile = lib_bmain->curlib->runtime->versionfile = bmain->versionfile;
lib_bmain->subversionfile = lib_bmain->curlib->runtime->subversionfile = bmain->subversionfile;
lib_bmain->colorspace = lib_bmain->curlib->runtime->colorspace = bmain->colorspace;
}
if (fd == nullptr) {

View File

@@ -97,6 +97,7 @@
#include "BLI_fileops.hh"
#include "BLI_implicit_sharing.hh"
#include "BLI_math_base.h"
#include "BLI_math_matrix.h"
#include "BLI_multi_value_map.hh"
#include "BLI_path_utils.hh"
#include "BLI_set.hh"
@@ -1241,6 +1242,9 @@ static void write_global(WriteData *wd, const int fileflags, Main *mainvar)
fg.curscene = scene;
fg.cur_view_layer = view_layer;
STRNCPY(fg.colorspace_scene_linear_name, mainvar->colorspace.scene_linear_name);
copy_m3_m3(fg.colorspace_scene_linear_to_xyz, mainvar->colorspace.scene_linear_to_xyz.ptr());
/* Prevent to save this, is not good convention, and feature with concerns. */
fg.fileflags = (fileflags & ~G_FILE_FLAG_ALL_RUNTIME);

View File

@@ -73,6 +73,7 @@ set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blenloader
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::gpu
bf_imbuf_openimageio

View File

@@ -19,6 +19,7 @@ struct ColorManagedColorspaceSettings;
struct ColorManagedDisplaySettings;
struct ColorManagedViewSettings;
struct ColormanageProcessor;
struct ID;
struct EnumPropertyItem;
struct ImBuf;
struct ImageFormatData;
@@ -392,6 +393,37 @@ void IMB_colormanagement_colorspace_from_ibuf_ftype(
/** \} */
/* -------------------------------------------------------------------- */
/** \name Working Space Functions
* \{ */
const char *IMB_colormanagement_working_space_get_default();
const char *IMB_colormanagement_working_space_get();
bool IMB_colormanagement_working_space_set_from_name(const char *name);
bool IMB_colormanagement_working_space_set_from_matrix(
const char *name, const blender::float3x3 &scene_linear_to_xyz);
void IMB_colormanagement_working_space_check(Main *bmain,
const bool for_undo,
const bool have_editable_assets);
void IMB_colormanagement_working_space_init(Main *bmain);
void IMB_colormanagement_working_space_convert(
Main *bmain,
const blender::float3x3 &current_scene_linear_to_xyz,
const blender::float3x3 &new_xyz_to_scene_linear,
const bool depsgraph_tag = false,
const bool linked_only = false,
const bool editable_assets_only = false);
void IMB_colormanagement_working_space_convert(Main *bmain, const Main *reference_bmain);
int IMB_colormanagement_working_space_get_named_index(const char *name);
const char *IMB_colormanagement_working_space_get_indexed_name(int index);
void IMB_colormanagement_working_space_items_add(EnumPropertyItem **items, int *totitem);
/** \} */
/* -------------------------------------------------------------------- */
/** \name RNA Helper Functions
* \{ */

View File

@@ -12,6 +12,7 @@
#include <cmath>
#include <cstring>
#include "DNA_ID.h"
#include "DNA_color_types.h"
#include "DNA_image_types.h"
#include "DNA_movieclip_types.h"
@@ -29,6 +30,7 @@
#include "MEM_guardedalloc.h"
#include "BLI_color.hh"
#include "BLI_colorspace.hh"
#include "BLI_fileops.hh"
#include "BLI_listbase.h"
@@ -49,8 +51,11 @@
#include "BKE_colortools.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_image_format.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "GPU_capabilities.hh"
@@ -59,6 +64,8 @@
#include "SEQ_iterator.hh"
#include "DEG_depsgraph.hh"
#include "CLG_log.h"
#include "OCIO_api.hh"
@@ -92,6 +99,12 @@ static char global_role_default_float[MAX_COLORSPACE_NAME];
static char global_role_default_sequencer[MAX_COLORSPACE_NAME];
static char global_role_aces_interchange[MAX_COLORSPACE_NAME];
/* Defaults from the config that never change with working space. */
static char global_role_scene_linear_default[MAX_COLORSPACE_NAME];
static char global_role_default_float_default[MAX_COLORSPACE_NAME];
float3x3 global_scene_linear_to_xyz_default = float3x3::zero();
/* lock used by pre-cached processors getters, so processor wouldn't
* be created several times
* LOCK_COLORMANAGE can not be used since this mutex could be needed to
@@ -587,6 +600,11 @@ static bool colormanage_load_config(ocio::Config &config)
colormanage_update_matrices();
/* Defaults that don't change with file working space. */
STRNCPY(global_role_scene_linear_default, global_role_scene_linear);
STRNCPY(global_role_default_float_default, global_role_default_float);
global_scene_linear_to_xyz_default = blender::colorspace::scene_linear_to_xyz;
return ok;
}
@@ -3069,6 +3087,391 @@ const char *IMB_colormanagement_look_validate_for_view(const char *view_name,
/** \} */
/* -------------------------------------------------------------------- */
/** \name Working Space Functions
* \{ */
/* Should have enough bits of precision, and this can be reasonably high assuming
* that if colorspaces are really this close, no point converting anyway. */
static const float imb_working_space_compare_threshold = 0.001f;
const char *IMB_colormanagement_working_space_get_default()
{
return global_role_scene_linear_default;
}
int IMB_colormanagement_working_space_get_named_index(const char *name)
{
return IMB_colormanagement_colorspace_get_named_index(name);
}
const char *IMB_colormanagement_working_space_get_indexed_name(int index)
{
return IMB_colormanagement_colorspace_get_indexed_name(index);
}
void IMB_colormanagement_working_space_items_add(EnumPropertyItem **items, int *totitem)
{
const ColorSpace *scene_linear = g_config->get_color_space(OCIO_ROLE_SCENE_LINEAR);
blender::Vector<const ColorSpace *> working_spaces = {
IMB_colormanagement_space_from_interop_id("lin_rec709_scene"),
IMB_colormanagement_space_from_interop_id("lin_rec2020_scene"),
IMB_colormanagement_space_from_interop_id("lin_ap1_scene")};
if (!working_spaces.contains(scene_linear)) {
working_spaces.prepend(scene_linear);
}
for (const ColorSpace *colorspace : working_spaces) {
if (colorspace == nullptr) {
continue;
}
EnumPropertyItem item;
item.value = colorspace->index;
item.name = colorspace->name().c_str();
item.identifier = colorspace->name().c_str();
item.icon = 0;
item.description = colorspace->description().c_str();
RNA_enum_item_add(items, totitem, &item);
}
}
const char *IMB_colormanagement_working_space_get()
{
return global_role_scene_linear;
}
bool IMB_colormanagement_working_space_set_from_name(const char *name)
{
if (STREQ(global_role_scene_linear, name)) {
return false;
}
CLOG_DEBUG(&LOG, "Setting blend file working color space to '%s'", name);
/* Change default float along with working space for convenience, if it was the same. */
if (STREQ(global_role_default_float_default, global_role_scene_linear_default)) {
STRNCPY(global_role_default_float, name);
}
else {
STRNCPY(global_role_default_float, global_role_default_float_default);
}
STRNCPY(global_role_scene_linear, name);
g_config->set_scene_linear_role(name);
colormanage_update_matrices();
return true;
}
bool IMB_colormanagement_working_space_set_from_matrix(
const char *name, const blender::float3x3 &scene_linear_to_xyz)
{
StringRefNull interop_id;
/* Check if we match the working space defined by the config. */
if (blender::math::is_equal(scene_linear_to_xyz,
global_scene_linear_to_xyz_default,
imb_working_space_compare_threshold))
{
return IMB_colormanagement_working_space_set_from_name(global_role_scene_linear_default);
}
/* Check if we match a common known working space, that hopefully exists in the config. */
if (blender::math::is_equal(
scene_linear_to_xyz, ocio::ACES_TO_XYZ, imb_working_space_compare_threshold))
{
interop_id = "lin_ap0_scene";
}
else if (blender::math::is_equal(
scene_linear_to_xyz, ocio::ACESCG_TO_XYZ, imb_working_space_compare_threshold))
{
interop_id = "lin_ap1_scene";
}
else if (blender::math::is_equal(scene_linear_to_xyz,
blender::math::invert(ocio::XYZ_TO_REC709),
imb_working_space_compare_threshold))
{
interop_id = "lin_rec709_scene";
}
else if (blender::math::is_equal(scene_linear_to_xyz,
blender::math::invert(ocio::XYZ_TO_REC2020),
imb_working_space_compare_threshold))
{
interop_id = "lin_rec2020_scene";
}
const ColorSpace *colorspace = g_config->get_color_space_by_interop_id(interop_id);
if (colorspace) {
return IMB_colormanagement_working_space_set_from_name(colorspace->name().c_str());
}
CLOG_ERROR(
&LOG, "Unknown scene linear working space '%s'. Missing OpenColorIO configuration?", name);
return IMB_colormanagement_working_space_set_from_name(global_role_scene_linear_default);
}
void IMB_colormanagement_working_space_check(Main *bmain,
const bool for_undo,
const bool have_editable_assets)
{
/* For old files without info, assume current OpenColorIO config. */
if (blender::math::is_zero(bmain->colorspace.scene_linear_to_xyz)) {
STRNCPY(bmain->colorspace.scene_linear_name, global_role_scene_linear_default);
bmain->colorspace.scene_linear_to_xyz = global_scene_linear_to_xyz_default;
CLOG_DEBUG(&LOG,
"Blend file has unknown scene linear working color space, setting to default");
}
const blender::float3x3 current_scene_linear_to_xyz = blender::colorspace::scene_linear_to_xyz;
/* Change the working space to the one from the blend file. */
const bool working_space_changed = IMB_colormanagement_working_space_set_from_matrix(
bmain->colorspace.scene_linear_name, bmain->colorspace.scene_linear_to_xyz);
if (!working_space_changed) {
return;
}
/* For undo, we need to convert the linked datablocks as they were left unchanged by undo.
* For file load, we need to convert editable assets that came from the previous main. */
if (!(for_undo || have_editable_assets)) {
return;
}
IMB_colormanagement_working_space_convert(
bmain,
current_scene_linear_to_xyz,
blender::math::invert(bmain->colorspace.scene_linear_to_xyz),
for_undo,
for_undo,
!for_undo && have_editable_assets);
}
static blender::float3 imb_working_space_convert(const blender::float3x3 &m,
const bool is_smaller_gamut,
const blender::float3 in_rgb)
{
blender::float3 rgb = m * in_rgb;
for (int i = 0; i < 3; i++) {
/* Round to nicer fractions. */
rgb[i] = 1e-5f * roundf(rgb[i] * 1e5f);
/* Snap to 0 and 1. */
if (fabsf(rgb[i]) < 5e-5) {
rgb[i] = 0.0f;
}
else if (fabsf(1.0f - rgb[i]) < 5e-5) {
rgb[i] = 1.0f;
}
/* Clamp when goig to smaller gamut. We can't really distinguish
* between HDR and out of gamut colors. */
if (is_smaller_gamut) {
rgb[i] = blender::math::clamp(rgb[i], 0.0f, 1.0f);
}
}
return rgb;
}
static blender::ColorGeometry4f imb_working_space_convert(const blender::float3x3 &m,
const bool is_smaller_gamut,
const blender::ColorGeometry4f color)
{
using namespace blender;
const float3 in_rgb = float3(color::unpremultiply_alpha(color));
const float3 rgb = imb_working_space_convert(m, is_smaller_gamut, in_rgb);
return color::premultiply_alpha(ColorGeometry4f(rgb[0], rgb[1], rgb[2], color[3]));
}
void IMB_colormanagement_working_space_convert(
Main *bmain,
const blender::float3x3 &current_scene_linear_to_xyz,
const blender::float3x3 &new_xyz_to_scene_linear,
const bool depsgraph_tag,
const bool linked_only,
const bool editable_assets_only)
{
using namespace blender;
/* If unknown, assume it's the OpenColorIO config scene linear space. */
float3x3 bmain_scene_linear_to_xyz = (math::is_zero(current_scene_linear_to_xyz)) ?
global_scene_linear_to_xyz_default :
current_scene_linear_to_xyz;
float3x3 M = new_xyz_to_scene_linear * bmain_scene_linear_to_xyz;
/* Already in the same space? */
if (math::is_equal(M, float3x3::identity(), imb_working_space_compare_threshold)) {
return;
}
if (math::determinant(M) == 0.0f) {
CLOG_ERROR(&LOG, "Working space conversion matrix is not invertible");
return;
}
/* Determine if we are going to a smaller gamut and need to clamp. We prefer not to,
* to preserve HDR colors, although they should not be common in properties. */
bool is_smaller_gamut = false;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (M[i][j] < 0.0f) {
is_smaller_gamut = true;
}
}
}
/* Single color. */
const auto single = [&M, is_smaller_gamut](float rgb[3]) {
copy_v3_v3(rgb, imb_working_space_convert(M, is_smaller_gamut, float3(rgb)));
};
/* Array with implicit sharing.
*
* We store references to all color arrays, so we can efficiently preserve implicit
* sharing and write in place when possible. */
struct ColorArrayInfo {
Vector<ColorGeometry4f **> data_ptrs;
Vector<ImplicitSharingPtr<> *> sharing_info_ptrs;
/* Though it is unlikely, the same data array could be used among multiple geometries with
* different domain sizes, so keep track of the maximum size among all users. */
size_t max_size;
};
Map<const ImplicitSharingInfo *, ColorArrayInfo> color_array_map;
const auto implicit_sharing_array =
[&](ImplicitSharingPtr<> &sharing_info, ColorGeometry4f *&data, size_t size) {
/* No data? */
if (!sharing_info) {
BLI_assert(size == 0);
return;
}
color_array_map.add_or_modify(
sharing_info.get(),
[&](ColorArrayInfo *value) {
new (value) ColorArrayInfo();
value->data_ptrs.append(&data);
value->sharing_info_ptrs.append(&sharing_info);
value->max_size = size;
},
[&](ColorArrayInfo *value) {
BLI_assert(data == *value->data_ptrs.last());
value->data_ptrs.append(&data);
value->sharing_info_ptrs.append(&sharing_info);
value->max_size = std::max(value->max_size, size);
});
};
IDTypeForeachColorFunctionCallback fn = {single, implicit_sharing_array};
/* Iterate over IDs and embedded IDs. No need to do it for master collections
* though, they don't have colors. */
/* TODO: Multithreading over IDs? */
ID *id_iter;
FOREACH_MAIN_ID_BEGIN (bmain, id_iter) {
if (linked_only) {
if (!id_iter->lib) {
continue;
}
}
if (editable_assets_only) {
if (!(id_iter->lib && (id_iter->lib->runtime->tag & LIBRARY_ASSET_EDITABLE))) {
continue;
}
}
const IDTypeInfo *id_type = BKE_idtype_get_info_from_id(id_iter);
if (id_type->foreach_working_space_color) {
id_type->foreach_working_space_color(id_iter, fn);
if (depsgraph_tag) {
DEG_id_tag_update(id_iter, ID_RECALC_ALL);
}
}
if (bNodeTree *node_tree = bke::node_tree_from_id(id_iter)) {
const IDTypeInfo *id_type = BKE_idtype_get_info_from_id(&node_tree->id);
if (id_type->foreach_working_space_color) {
id_type->foreach_working_space_color(&node_tree->id, fn);
}
}
}
FOREACH_MAIN_ID_END;
/* Handle implicit sharing arrays. */
Vector<Map<const ImplicitSharingInfo *, ColorArrayInfo>::Item> color_array_items(
color_array_map.items().begin(), color_array_map.items().end());
threading::parallel_for(color_array_items.index_range(), 64, [&](const IndexRange range) {
for (const int item_index : range) {
const auto &item = color_array_items[item_index];
if (item.value.data_ptrs.size() == item.key->strong_users()) {
/* All of the users of the array data are from the main we're converting, so we can change
* the data array in place without allocating a new version. */
item.key->tag_ensured_mutable();
MutableSpan<ColorGeometry4f> data(*item.value.data_ptrs.first(), item.value.max_size);
threading::parallel_for(data.index_range(), 1024, [&](const IndexRange range) {
for (const int64_t i : range) {
data[i] = imb_working_space_convert(M, is_smaller_gamut, data[i]);
}
});
}
else {
/* Somehow the data is used by something outside of the Main we're currently converting, it
* has to be duplicated before being converted to avoid changing the original. */
const Span<ColorGeometry4f> src_data(*item.value.data_ptrs.first(), item.value.max_size);
auto *dst_data = MEM_malloc_arrayN<ColorGeometry4f>(
src_data.size(), "IMB_colormanagement_working_space_convert");
const ImplicitSharingPtr<> sharing_ptr(implicit_sharing::info_for_mem_free(dst_data));
threading::parallel_for(src_data.index_range(), 1024, [&](const IndexRange range) {
for (const int64_t i : range) {
dst_data[i] = imb_working_space_convert(M, is_smaller_gamut, src_data[i]);
}
});
/* Replace the data pointer and the sharing info pointer with the new data in all of the
* users from the main data-base. The sharing pointer assignment adds a user. */
for (ColorGeometry4f **pointer : item.value.data_ptrs) {
*pointer = dst_data;
}
for (ImplicitSharingPtr<> *pointer : item.value.sharing_info_ptrs) {
*pointer = sharing_ptr;
}
}
}
});
}
void IMB_colormanagement_working_space_convert(Main *bmain, const Main *reference_bmain)
{
/* If unknown, assume it's the OpenColorIO config scene linear space. */
float3x3 reference_scene_linear_to_xyz = blender::math::is_zero(
reference_bmain->colorspace.scene_linear_to_xyz) ?
global_scene_linear_to_xyz_default :
reference_bmain->colorspace.scene_linear_to_xyz;
IMB_colormanagement_working_space_convert(bmain,
bmain->colorspace.scene_linear_to_xyz,
blender::math::invert(reference_scene_linear_to_xyz),
false);
STRNCPY(bmain->colorspace.scene_linear_name, reference_bmain->colorspace.scene_linear_name);
bmain->colorspace.scene_linear_to_xyz = reference_bmain->colorspace.scene_linear_to_xyz;
}
void IMB_colormanagement_working_space_init(Main *bmain)
{
STRNCPY(bmain->colorspace.scene_linear_name, global_role_scene_linear_default);
bmain->colorspace.scene_linear_to_xyz = global_scene_linear_to_xyz_default;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name RNA Helper Functions
* \{ */

View File

@@ -9,6 +9,7 @@
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "DNA_windowmanager_types.h"
namespace blender::ocio {

View File

@@ -3,6 +3,8 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "libocio_colorspace.hh"
#include "OCIO_cpu_processor.hh"
#include "intern/cpu_processor_cache.hh"
#if defined(WITH_OPENCOLORIO)

View File

@@ -34,6 +34,12 @@ typedef struct FileGlobal {
char build_hash[16];
/** File path where this was saved, for recover. */
char filepath[/*FILE_MAX*/ 1024];
/* Working colorspace, for automatic conversion. Note the matrix is
* the source of truth, the name is only for user interface and diagnosis. */
char colorspace_scene_linear_name[/*MAX_COLORSPACE_NAME*/ 64];
float colorspace_scene_linear_to_xyz[3][3];
int _pad2[3];
} FileGlobal;
/* minversion: in file, the oldest past blender version you can use compliant */

View File

@@ -12,11 +12,14 @@
#include "BLI_path_utils.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "rna_internal.hh"
#ifdef RNA_RUNTIME
# include "IMB_colormanagement.hh"
# include "DNA_windowmanager_types.h"
# include "BKE_global.hh"
@@ -88,6 +91,28 @@ static PointerRNA rna_Main_colorspace_get(PointerRNA *ptr)
return PointerRNA(nullptr, &RNA_BlendFileColorspace, &bmain->colorspace);
}
static int rna_MainColorspace_working_space_get(PointerRNA *ptr)
{
MainColorspace *colorspace = ptr->data_as<MainColorspace>();
return IMB_colormanagement_working_space_get_named_index(colorspace->scene_linear_name);
}
static const EnumPropertyItem *rna_MainColorspace_working_space_itemf(bContext * /*C*/,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
bool *r_free)
{
EnumPropertyItem *items = nullptr;
int totitem = 0;
IMB_colormanagement_working_space_items_add(&items, &totitem);
RNA_enum_item_end(&items, &totitem);
*r_free = true;
return items;
}
static bool rna_MainColorspace_is_missing_opencolorio_config_get(PointerRNA *ptr)
{
MainColorspace *colorspace = ptr->data_as<MainColorspace>();
@@ -185,7 +210,20 @@ static void rna_def_main_colorspace(BlenderRNA *brna)
srna = RNA_def_struct(brna, "BlendFileColorspace", nullptr);
RNA_def_struct_ui_text(srna,
"Blend-File Color Space",
"Information about the color space used for datablocks in a blend file");
"Information about the color space used for data-blocks in a blend file");
prop = RNA_def_property(srna, "working_space", PROP_ENUM, PROP_NONE);
RNA_def_property_flag(prop, PROP_ENUM_NO_CONTEXT);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_enum_items(prop, rna_enum_dummy_NULL_items);
RNA_def_property_ui_text(prop,
"Working Space",
"Color space used for all scene linear colors in this file, and "
"for compositing, shader and geometry nodes processing");
RNA_def_property_enum_funcs(prop,
"rna_MainColorspace_working_space_get",
nullptr,
"rna_MainColorspace_working_space_itemf");
prop = RNA_def_property(srna, "is_missing_opencolorio_config", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
@@ -511,7 +549,7 @@ void RNA_def_main(BlenderRNA *brna)
RNA_def_property_ui_text(
prop,
"Color Space",
"Information about the color space used for datablocks in a blend file");
"Information about the color space used for data-blocks in a blend file");
RNA_api_main(srna);

View File

@@ -26,6 +26,7 @@ set(SRC
intern/wm_event_system.cc
intern/wm_files.cc
intern/wm_files_link.cc
intern/wm_files_colorspace.cc
intern/wm_gesture.cc
intern/wm_gesture_ops.cc
intern/wm_init_exit.cc

View File

@@ -0,0 +1,206 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup wm
*
* Functions for handling file colorspaces.
*/
#include "BLI_colorspace.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BKE_context.hh"
#include "BKE_image.hh"
#include "BKE_main.hh"
#include "BKE_movieclip.h"
#include "BKE_report.hh"
#include "DNA_windowmanager_enums.h"
#include "DNA_windowmanager_types.h"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "IMB_colormanagement.hh"
#include "DEG_depsgraph.hh"
#include "UI_interface_c.hh"
#include "UI_interface_icons.hh"
#include "UI_interface_layout.hh"
#include "BLT_translation.hh"
#include "ED_image.hh"
#include "ED_render.hh"
#include "RE_pipeline.h"
#include "SEQ_prefetch.hh"
#include "SEQ_relations.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "wm_files.hh"
/* -------------------------------------------------------------------- */
/** \name Set Working Color Space Operator
* \{ */
static const EnumPropertyItem *working_space_itemf(bContext * /*C*/,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
bool *r_free)
{
EnumPropertyItem *item = nullptr;
int totitem = 0;
IMB_colormanagement_working_space_items_add(&item, &totitem);
RNA_enum_item_end(&item, &totitem);
*r_free = true;
return item;
}
static bool wm_set_working_space_check_safe(bContext *C, wmOperator *op)
{
const wmWindowManager *wm = CTX_wm_manager(C);
const Main *bmain = CTX_data_main(C);
const Scene *scene = CTX_data_scene(C);
if (WM_jobs_test(wm, scene, WM_JOB_TYPE_ANY)) {
BKE_report(
op->reports, RPT_WARNING, RPT_("Can't change working space while josb are running"));
return false;
}
if (ED_image_should_save_modified(bmain)) {
BKE_report(op->reports,
RPT_WARNING,
RPT_("Can't change working space with modified images, save them first"));
return false;
}
return true;
}
static wmOperatorStatus wm_set_working_color_space_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
const bool convert_colors = RNA_boolean_get(op->ptr, "convert_colors");
const int working_space_index = RNA_enum_get(op->ptr, "working_space");
const char *working_space = IMB_colormanagement_working_space_get_indexed_name(
working_space_index);
if (!wm_set_working_space_check_safe(C, op)) {
return OPERATOR_CANCELLED;
}
if (working_space[0] == '\0' || STREQ(working_space, bmain->colorspace.scene_linear_name)) {
return OPERATOR_CANCELLED;
}
/* Stop all viewport renders. */
ED_render_engine_changed(bmain, true);
RE_FreeAllPersistentData();
/* Change working space. */
IMB_colormanagement_working_space_set_from_name(working_space);
if (convert_colors) {
const bool depsgraph_tag = true;
IMB_colormanagement_working_space_convert(bmain,
bmain->colorspace.scene_linear_to_xyz,
blender::colorspace::xyz_to_scene_linear,
depsgraph_tag);
}
STRNCPY(bmain->colorspace.scene_linear_name, working_space);
bmain->colorspace.scene_linear_to_xyz = blender::colorspace::scene_linear_to_xyz;
/* Free all render, compositor and sequencer caches. */
RE_FreeAllRenderResults();
RE_FreeInteractiveCompositorRenders();
blender::seq::prefetch_stop_all();
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
blender::seq::cache_cleanup(scene);
}
/* Free all images, they may have scene linear float buffers. */
LISTBASE_FOREACH (Image *, image, &bmain->images) {
DEG_id_tag_update(&image->id, ID_RECALC_SOURCE);
BKE_image_signal(bmain, image, nullptr, IMA_SIGNAL_COLORMANAGE);
BKE_image_partial_update_mark_full_update(image);
}
LISTBASE_FOREACH (MovieClip *, clip, &bmain->movieclips) {
BKE_movieclip_clear_cache(clip);
BKE_movieclip_free_gputexture(clip);
DEG_id_tag_update(&clip->id, ID_RECALC_SOURCE);
}
/* Redraw everything. */
WM_main_add_notifier(NC_SCENE | ND_SEQUENCER, nullptr);
WM_main_add_notifier(NC_SCENE | ND_RENDER_OPTIONS, nullptr);
WM_main_add_notifier(NC_SCENE | ND_NODES, nullptr);
WM_main_add_notifier(NC_WINDOW, nullptr);
return OPERATOR_FINISHED;
}
static wmOperatorStatus wm_set_working_color_space_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
if (!wm_set_working_space_check_safe(C, op)) {
return OPERATOR_CANCELLED;
}
if (RNA_enum_get(op->ptr, "working_space") == -1) {
RNA_enum_set(op->ptr,
"working_space",
IMB_colormanagement_working_space_get_named_index(
IMB_colormanagement_working_space_get_default()));
}
return WM_operator_props_popup_confirm_ex(
C,
op,
event,
std::nullopt,
IFACE_("Apply"),
false,
IFACE_("To match renders with the previous working space as closely as possible,\n"
"colors in all materials, lights and geometry must be converted.\n\n"
"Some nodes graphs cannot be converted accurately and may need manual fix-ups."));
}
void WM_OT_set_working_color_space(wmOperatorType *ot)
{
ot->name = "Set Blend File Working Color Space";
ot->idname = "WM_OT_set_working_color_space";
ot->description = "Change the working color space of all colors in this blend file";
ot->exec = wm_set_working_color_space_exec;
ot->invoke = wm_set_working_color_space_invoke;
ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
RNA_def_boolean(ot->srna,
"convert_colors",
true,
"Convert Colors in All Data-blocks",
"Change colors in all data-blocks to the new working space");
PropertyRNA *prop = RNA_def_enum(ot->srna,
"working_space",
rna_enum_dummy_NULL_items,
-1,
"Working Space",
"Color space to set");
RNA_def_enum_funcs(prop, working_space_itemf);
ot->prop = prop;
}
/** \} */

View File

@@ -4242,6 +4242,7 @@ void wm_operatortypes_register()
WM_operatortype_append(WM_OT_previews_ensure);
WM_operatortype_append(WM_OT_previews_clear);
WM_operatortype_append(WM_OT_doc_view_manual_ui_context);
WM_operatortype_append(WM_OT_set_working_color_space);
#ifdef WITH_XR_OPENXR
wm_xr_operatortypes_register();

View File

@@ -131,3 +131,7 @@ void WM_OT_id_linked_relocate(wmOperatorType *ot);
void WM_OT_lib_relocate(wmOperatorType *ot);
void WM_OT_lib_reload(wmOperatorType *ot);
/* `wm_files_colorspace.cc` */
void WM_OT_set_working_color_space(wmOperatorType *ot);

BIN
tests/files/render/colorspace/acescg_blackbody.blend (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/files/render/colorspace/rec2020_lights.blend (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -669,6 +669,7 @@ if((WITH_CYCLES OR WITH_GPU_RENDER_TESTS) AND TEST_SRC_DIR_EXISTS)
set(render_tests
attributes
camera
colorspace
bsdf
hair
image_colorspace

View File

@@ -280,6 +280,9 @@ def main():
if (test_dir_name in {'volume', 'openvdb'}):
report.set_fail_threshold(0.048)
report.set_fail_percent(3)
# OSL blackbody output is a little different.
if (test_dir_name in {'colorspace'}):
report.set_fail_threshold(0.05)
ok = report.run(args.testdir, args.blender, get_arguments, batch=args.batch)