Blender V5.0
libocio_config.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include "libocio_config.hh"
6
7#if defined(WITH_OPENCOLORIO)
8
9# include <algorithm>
10# include <numeric>
11
12# include <fmt/format.h>
13
14# include "BLI_array.hh"
15# include "BLI_assert.h"
16# include "BLI_index_range.hh"
17# include "BLI_math_matrix.hh"
18
19# include "OCIO_matrix.hh"
20# include "OCIO_role_names.hh"
21
22# include "error_handling.hh"
23# include "libocio_colorspace.hh"
26# include "libocio_processor.hh"
27
28namespace blender::ocio {
29
30/* -------------------------------------------------------------------- */
33
34std::unique_ptr<Config> LibOCIOConfig::create_from_environment()
35{
36 try {
37 OCIO_NAMESPACE::ConstConfigRcPtr ocio_config = OCIO_NAMESPACE::Config::CreateFromEnv();
38 if (!ocio_config) {
39 return nullptr;
40 }
41
42 return std::unique_ptr<LibOCIOConfig>(new LibOCIOConfig(ocio_config));
43 }
44 catch (OCIO_NAMESPACE::Exception &exception) {
45 report_exception(exception);
46 }
47
48 return nullptr;
49}
50
51std::unique_ptr<Config> LibOCIOConfig::create_from_file(const StringRefNull filename)
52{
53 try {
54 OCIO_NAMESPACE::ConstConfigRcPtr ocio_config = OCIO_NAMESPACE::Config::CreateFromFile(
55 filename.c_str());
56 if (!ocio_config) {
57 return nullptr;
58 }
59
60 return std::unique_ptr<LibOCIOConfig>(new LibOCIOConfig(ocio_config));
61 }
62 catch (OCIO_NAMESPACE::Exception &exception) {
63 report_exception(exception);
64 }
65
66 return nullptr;
67}
68
69LibOCIOConfig::LibOCIOConfig(const OCIO_NAMESPACE::ConstConfigRcPtr &ocio_config)
70{
71 BLI_assert(ocio_config);
72
73 /* Set the global OpenColorIO configuration so that other parts of Blender can access it. For
74 * example, Cycles uses for own color management of textures.
75 *
76 * Acquire a pointer to the configuration and pass it around explicitly to avoid unneeded shared
77 * pointer acquisition. */
78 OCIO_NAMESPACE::SetCurrentConfig(ocio_config);
79 ocio_config_ = OCIO_NAMESPACE::GetCurrentConfig();
80
81 initialize_active_color_spaces();
82 initialize_inactive_color_spaces();
83 initialize_hdr_color_spaces();
84 initialize_looks();
85 initialize_displays();
86}
87
88LibOCIOConfig::~LibOCIOConfig() {}
89
90void LibOCIOConfig::initialize_active_color_spaces()
91{
92 OCIO_NAMESPACE::ColorSpaceSetRcPtr ocio_color_spaces;
93
94 try {
95 ocio_color_spaces = ocio_config_->getColorSpaces(nullptr);
96 }
97 catch (OCIO_NAMESPACE::Exception &exception) {
98 report_exception(exception);
99 return;
100 }
101
102 if (!ocio_color_spaces) {
103 report_error("Invalid OpenColorIO configuration: color spaces set is nullptr");
104 return;
105 }
106
107 const int num_color_spaces = ocio_color_spaces->getNumColorSpaces();
108 if (num_color_spaces < 0) {
109 report_error(fmt::format(
110 "Invalid OpenColorIO configuration: invalid number of color spaces {}", num_color_spaces));
111 return;
112 }
113
114 color_spaces_.reserve(num_color_spaces);
115
116 for (const int i : IndexRange(num_color_spaces)) {
117 const OCIO_NAMESPACE::ConstColorSpaceRcPtr ocio_color_space =
118 ocio_color_spaces->getColorSpaceByIndex(i);
119 color_spaces_.append_as(i, ocio_config_, ocio_color_space);
120 }
121
122 /* Create index array for access to the color space in alphabetic order. */
123 sorted_color_space_index_.resize(num_color_spaces);
124 std::iota(sorted_color_space_index_.begin(), sorted_color_space_index_.end(), 0);
125 std::sort(sorted_color_space_index_.begin(), sorted_color_space_index_.end(), [&](int a, int b) {
126 return color_spaces_[a].name() < color_spaces_[b].name();
127 });
128}
129
130void LibOCIOConfig::initialize_inactive_color_spaces()
131{
132 const int num_inactive_color_spaces = ocio_config_->getNumColorSpaces(
133 OCIO_NAMESPACE::SEARCH_REFERENCE_SPACE_ALL, OCIO_NAMESPACE::COLORSPACE_INACTIVE);
134 if (num_inactive_color_spaces < 0) {
135 report_error(fmt::format(
136 "Invalid OpenColorIO configuration: invalid number of inactive color spaces {}",
137 num_inactive_color_spaces));
138 return;
139 }
140
141 for (const int i : IndexRange(num_inactive_color_spaces)) {
142 const char *colorspace_name = ocio_config_->getColorSpaceNameByIndex(
143 OCIO_NAMESPACE::SEARCH_REFERENCE_SPACE_ALL, OCIO_NAMESPACE::COLORSPACE_INACTIVE, i);
144
145 OCIO_NAMESPACE::ConstColorSpaceRcPtr ocio_color_space;
146 try {
147 ocio_color_space = ocio_config_->getColorSpace(colorspace_name);
148 }
149 catch (OCIO_NAMESPACE::Exception &exception) {
150 report_exception(exception);
151 continue;
152 }
153
154 inactive_color_spaces_.append_as(i, ocio_config_, ocio_color_space);
155 }
156}
157
158void LibOCIOConfig::initialize_looks()
159{
160 const int num_looks = ocio_config_->getNumLooks();
161
162 looks_.reserve(num_looks + 1);
163
164 /* Add entry for look None. */
165 looks_.append_as(0, nullptr);
166
167 for (const int i : IndexRange(num_looks)) {
168 const StringRefNull view_name = ocio_config_->getLookNameByIndex(i);
169
170 /* Look None is built-in and always exists. Skip it from the configuration. */
171 if (view_name == "None") {
172 continue;
173 }
174
175 const OCIO_NAMESPACE::ConstLookRcPtr ocio_look = ocio_config_->getLook(view_name.c_str());
176 looks_.append_as(i + 1, ocio_look);
177 }
178}
179
180void LibOCIOConfig::initialize_displays()
181{
182 const int num_displays = ocio_config_->getNumDisplays();
183 if (num_displays < 0) {
184 report_error(fmt::format("Invalid OpenColorIO configuration: invalid number of displays {}",
185 num_displays));
186 return;
187 }
188
189 displays_.reserve(num_displays);
190
191 for (const int i : IndexRange(num_displays)) {
192 displays_.append_as(i, *this);
193 }
194}
195
197
198/* -------------------------------------------------------------------- */
201
202float3 LibOCIOConfig::get_default_luma_coefs() const
203{
204 try {
205 double rgb_double[3];
206 ocio_config_->getDefaultLumaCoefs(rgb_double);
207
208 return float3(rgb_double[0], rgb_double[1], rgb_double[2]);
209 }
210 catch (OCIO_NAMESPACE::Exception &exception) {
211 report_exception(exception);
212 }
213
214 /* Fallback to the older Blender assumed primaries of ITU-BT.709 / sRGB, matching the
215 * coefficients used in the fallback implementation. */
216 return float3(0.2126f, 0.7152f, 0.0722f);
217}
218
219static bool to_scene_linear_matrix(const OCIO_NAMESPACE::ConstConfigRcPtr &ocio_config,
220 const StringRefNull colorspace,
221 float3x3 &to_scene_linear)
222{
223 const OCIO_NAMESPACE::ConstProcessorRcPtr processor = create_ocio_processor(
224 ocio_config, colorspace.c_str(), OCIO_NAMESPACE::ROLE_SCENE_LINEAR);
225 if (!processor) {
226 return false;
227 }
228
229 const OCIO_NAMESPACE::ConstCPUProcessorRcPtr cpu_processor = processor->getDefaultCPUProcessor();
231 cpu_processor->applyRGB(to_scene_linear[0]);
232 cpu_processor->applyRGB(to_scene_linear[1]);
233 cpu_processor->applyRGB(to_scene_linear[2]);
234
235 return true;
236}
237
238float3x3 LibOCIOConfig::get_xyz_to_scene_linear_matrix() const
239{
240 /* Default to ITU-BT.709 in case no appropriate transform found.
241 * Note XYZ is defined here as having a D65 white point. */
243
244 /* Get from OpenColorO config if it has the required roles. */
245 if (!ocio_config_->hasRole(OCIO_NAMESPACE::ROLE_SCENE_LINEAR)) {
246 return xyz_to_scene_linear;
247 }
248
249 if (ocio_config_->hasRole("aces_interchange")) {
250 /* Standard OpenColorIO role, defined as ACES AP0 (ACES2065-1). */
252 if (to_scene_linear_matrix(ocio_config_, "aces_interchange", aces_to_scene_linear)) {
253 float3x3 xyz_to_aces = math::invert(ACES_TO_XYZ);
255 }
256 }
257 else if (ocio_config_->hasRole("XYZ")) {
258 /* Custom role used before the standard existed. */
259 to_scene_linear_matrix(ocio_config_, "XYZ", xyz_to_scene_linear);
260 }
261
262 return xyz_to_scene_linear;
263}
264
265const char *LibOCIOConfig::get_color_space_from_filepath(const char *filepath) const
266{
267 /* Ignore the default rule, same behavior as for example OpenImageIO and xStudio.
268 * The ACES studio config has only a default rule set to ACES2065-1, which works
269 * poorly if we assign it to every file as default.
270 *
271 * It's unclear if the default rule should be used for anything, and if not why
272 * it even exists. */
273 if (ocio_config_->filepathOnlyMatchesDefaultRule(filepath)) {
274 return nullptr;
275 }
276
277 return ocio_config_->getColorSpaceFromFilepath(filepath);
278}
279
281
282/* -------------------------------------------------------------------- */
285
286const ColorSpace *LibOCIOConfig::get_color_space(const StringRefNull name) const
287{
288 OCIO_NAMESPACE::ConstColorSpaceRcPtr ocio_color_space;
289
290 try {
291 /* Lookup color space in the OpenColorIO, letting it resolve role name or an alias. */
292 ocio_color_space = ocio_config_->getColorSpace(name.c_str());
293 }
294 catch (OCIO_NAMESPACE::Exception &exception) {
295 report_exception(exception);
296 return nullptr;
297 }
298
299 if (!ocio_color_space) {
300 return nullptr;
301 }
302
303 /* TODO(sergey): Is there faster way to lookup Blender-side color space?
304 * It does not seem that pointer in ConstColorSpaceRcPtr is unique enough to use for
305 * comparison. */
306 for (const LibOCIOColorSpace &color_space : color_spaces_) {
307 if (color_space.name() == ocio_color_space->getName()) {
308 return &color_space;
309 }
310 }
311
312 /* Also lookup in the inactive color space, as the requested space might be coming from the
313 * display and marked as inactive to prevent it from showing up in the application menu. */
314 for (const LibOCIOColorSpace &color_space : inactive_color_spaces_) {
315 if (color_space.name() == ocio_color_space->getName()) {
316 return &color_space;
317 }
318 }
319
320 if (!ocio_config_->isInactiveColorSpace(ocio_color_space->getName())) {
321 report_error(
322 fmt::format("Invalid OpenColorIO configuration: color space {} not found on Blender side",
323 ocio_color_space->getName()));
324 }
325
326 return nullptr;
327}
328
329int LibOCIOConfig::get_num_color_spaces() const
330{
331 return color_spaces_.size();
332}
333
334const ColorSpace *LibOCIOConfig::get_color_space_by_index(int const index) const
335{
336 if (index < 0 || index >= color_spaces_.size()) {
337 return nullptr;
338 }
339 return &color_spaces_[index];
340}
341
342const ColorSpace *LibOCIOConfig::get_sorted_color_space_by_index(const int index) const
343{
344 BLI_assert(color_spaces_.size() == sorted_color_space_index_.size());
345 if (index < 0 || index >= color_spaces_.size()) {
346 return nullptr;
347 }
348 return get_color_space_by_index(sorted_color_space_index_[index]);
349}
350
351const ColorSpace *LibOCIOConfig::get_color_space_by_interop_id(StringRefNull interop_id) const
352{
353 for (const LibOCIOColorSpace &color_space : color_spaces_) {
354 if (color_space.interop_id() == interop_id) {
355 return &color_space;
356 }
357 }
358
359 for (const LibOCIOColorSpace &color_space : inactive_color_spaces_) {
360 if (color_space.interop_id() == interop_id) {
361 return &color_space;
362 }
363 }
364
365 return nullptr;
366}
367
369
370/* -------------------------------------------------------------------- */
373
374const ColorSpace *LibOCIOConfig::get_color_space_for_hdr_image(StringRefNull name) const
375{
376 /* Based on emperical testing, ideo works with 100 nits diffuse white, while
377 * images need 203 nits diffuse whites to show matching results. */
378 const ColorSpace *colorspece = get_color_space(name);
379 if (colorspece->interop_id() == "pq_rec2020_display") {
380 return get_color_space("blender:pq_rec2020_display_203nits");
381 }
382 if (colorspece->interop_id() == "hlg_rec2020_display") {
383 return get_color_space("blender:hlg_rec2020_display_203nits");
384 }
385 return nullptr;
386}
387
388void LibOCIOConfig::initialize_hdr_color_spaces()
389{
390 for (StringRefNull interop_id : {"pq_rec2020_display", "hlg_rec2020_display"}) {
391 const auto *colorspace = static_cast<const LibOCIOColorSpace *>(
392 get_color_space_by_interop_id(interop_id));
393 if (!colorspace || !colorspace->is_display_referred()) {
394 continue;
395 }
396
397 /* Create colorspace that uses 203 nits diffuse white instead of 100 nits. */
398 const auto hdr_100_colorspace = ocio_config_->getColorSpace(colorspace->name().c_str());
399 const auto hdr_colorspace = OCIO_NAMESPACE::ColorSpace::Create(
400 OCIO_NAMESPACE::REFERENCE_SPACE_DISPLAY);
401 const auto group = OCIO_NAMESPACE::GroupTransform::Create();
402
403 hdr_colorspace->setName(("blender:" + interop_id + "_203nits").c_str());
404
405 const auto to_203_nits = OCIO_NAMESPACE::MatrixTransform::Create();
406 to_203_nits->setMatrix(double4x4(double3x3::diagonal(203.0 / 100.0)).base_ptr());
407 group->appendTransform(to_203_nits);
408
409 const auto to_display = hdr_100_colorspace
410 ->getTransform(OCIO_NAMESPACE::COLORSPACE_DIR_FROM_REFERENCE)
411 ->createEditableCopy();
412 group->appendTransform(to_display);
413
414 hdr_colorspace->setTransform(group, OCIO_NAMESPACE::COLORSPACE_DIR_FROM_REFERENCE);
415
416 OCIO_NAMESPACE::Config *mutable_ocio_config = const_cast<OCIO_NAMESPACE::Config *>(
417 ocio_config_.get());
418 mutable_ocio_config->addColorSpace(hdr_colorspace);
419
420 inactive_color_spaces_.append_as(inactive_color_spaces_.size(), ocio_config_, hdr_colorspace);
421 }
422}
423
425
426/* -------------------------------------------------------------------- */
429
430void LibOCIOConfig::set_scene_linear_role(StringRefNull name)
431{
432 if (ocio_config_->getRoleColorSpace(OCIO_NAMESPACE::ROLE_SCENE_LINEAR) == name) {
433 return;
434 }
435
436 /* This is a bad const cast, but seems to work ok, and reloading the whole config is
437 * something we don't support yet. When we do this could be changed. */
438 OCIO_NAMESPACE::Config *mutable_ocio_config = const_cast<OCIO_NAMESPACE::Config *>(
439 ocio_config_.get());
440 mutable_ocio_config->setRole(OCIO_NAMESPACE::ROLE_SCENE_LINEAR, name.c_str());
441
442 for (LibOCIOColorSpace &color_space : color_spaces_) {
443 color_space.clear_caches();
444 }
445 for (LibOCIOColorSpace &color_space : inactive_color_spaces_) {
446 color_space.clear_caches();
447 }
448 for (LibOCIODisplay &display : displays_) {
449 display.clear_caches();
450 }
451 gpu_shader_binder_.clear_caches();
452}
453
455
456/* -------------------------------------------------------------------- */
459
460const Display *LibOCIOConfig::get_default_display() const
461{
462 if (displays_.is_empty()) {
463 return nullptr;
464 }
465 /* Matches the behavior of OpenColorIO, but avoids using API which potentially throws exception
466 * and requires string lookups. */
467 return &displays_[0];
468}
469
470const Display *LibOCIOConfig::get_display_by_name(const StringRefNull name) const
471{
472 /* TODO(@sergey): Is there faster way to lookup Blender-side display? */
473 for (const LibOCIODisplay &display : displays_) {
474 if (display.name() == name) {
475 return &display;
476 }
477 }
478 return nullptr;
479}
480
481int LibOCIOConfig::get_num_displays() const
482{
483 return displays_.size();
484}
485
486const Display *LibOCIOConfig::get_display_by_index(int index) const
487{
488 if (index < 0 || index >= displays_.size()) {
489 return nullptr;
490 }
491 return &displays_[index];
492}
493
495
496/* -------------------------------------------------------------------- */
499
500const ColorSpace *LibOCIOConfig::get_display_view_color_space(const StringRefNull display,
501 const StringRefNull view) const
502{
503 StringRefNull display_color_space;
504
505 try {
506 display_color_space = ocio_config_->getDisplayViewColorSpaceName(display.c_str(),
507 view.c_str());
508 /* OpenColorIO does not resolve this token for us, so do it ourselves. */
509 if (strcasecmp(display_color_space.c_str(), "<USE_DISPLAY_NAME>") == 0) {
510 display_color_space = display;
511 }
512 }
513 catch (OCIO_NAMESPACE::Exception &exception) {
514 report_exception(exception);
515 display_color_space = display;
516 }
517
518 return get_color_space(display_color_space);
519}
520
522
523/* -------------------------------------------------------------------- */
526
527const Look *LibOCIOConfig::get_look_by_name(const StringRefNull name) const
528{
529 /* TODO(sergey): Is there faster way to lookup Blender-side look? */
530 for (const LibOCIOLook &look : looks_) {
531 if (look.name() == name) {
532 return &look;
533 }
534 }
535 return nullptr;
536}
537
538int LibOCIOConfig::get_num_looks() const
539{
540 return looks_.size();
541}
542
543const Look *LibOCIOConfig::get_look_by_index(const int index) const
544{
545 if (index < 0 || index >= looks_.size()) {
546 return nullptr;
547 }
548 return &looks_[index];
549}
550
552
553/* -------------------------------------------------------------------- */
556
557std::shared_ptr<const CPUProcessor> LibOCIOConfig::get_display_cpu_processor(
558 const DisplayParameters &display_parameters) const
559{
560 OCIO_NAMESPACE::ConstProcessorRcPtr processor = create_ocio_display_processor(
561 *this, display_parameters);
562 if (!processor) {
563 return nullptr;
564 }
565 return std::make_shared<LibOCIOCPUProcessor>(processor->getDefaultCPUProcessor());
566}
567
568std::shared_ptr<const CPUProcessor> LibOCIOConfig::get_cpu_processor(
569 const StringRefNull from_colorspace, const StringRefNull to_colorspace) const
570{
571 const OCIO_NAMESPACE::ConstProcessorRcPtr processor = create_ocio_processor(
572 ocio_config_, from_colorspace.c_str(), to_colorspace.c_str());
573 if (!processor) {
574 return nullptr;
575 }
576 return std::make_shared<LibOCIOCPUProcessor>(processor->getDefaultCPUProcessor());
577}
578
580
581/* -------------------------------------------------------------------- */
584
585const GPUShaderBinder &LibOCIOConfig::get_gpu_shader_binder() const
586{
587 return gpu_shader_binder_;
588}
589
591
592} // namespace blender::ocio
593
594#endif
#define BLI_assert(a)
Definition BLI_assert.h:46
static AppView * view
constexpr const char * c_str() const
virtual StringRefNull interop_id() const =0
BLI_INLINE ColorSceneLinear4f< eAlpha::Straight > to_scene_linear(const ColorTheme4f &theme4f)
Definition BLI_color.hh:126
CartesianBasis invert(const CartesianBasis &basis)
static const float3x3 ACES_TO_XYZ
static const float3x3 XYZ_TO_REC709
MatBase< float, 3, 3 > float3x3
VecBase< float, 3 > float3
MatBase< double, 4, 4 > double4x4
const char * name
static MatBase diagonal(double value)
i
Definition text_draw.cc:230