Color management: Support white balance as part of the display transform

This implements a von-Kries-style chromatic adaption using the Bradford matrix.
The adaption is performed in scene linear space in the OCIO GLSL shader, with
the matrix being computed on the host.

The parameters specify the white point of the input, which is to be mapped to
the white point of the scene linear space. The main parameter is temperature,
specified in Kelvin, which defines the blackbody spectrum that is used as the
input white point. Additionally, a tint parameter can be used to shift the
white point away from pure blackbody spectra (e.g. to match a D illuminant).

The defaults are set to match D65 so there is no immediate color shift when
enabling the option. Tint = 10 is needed since the D-series illuminants aren't
perfect blackbody emitters.

As an alternative to manually specifying the values, there's also a color
picker. When a color is selected, temperature and tint are set such that this
color ends up being balanced to white.
This only works if the color is close enough to a blackbody emitter -
specifically, for tint values within +-150. Beyond this, there can be ambiguity
in the representation.
Currently, in this case, the input is just ignored and temperature/tint aren't
changed. Ideally, we'd eventually give UI feedback for this.

Presets are supported, and all the CIE standard illuminants are included.

One part that I'm not quite happy with is that the tint parameter starts to
give weird results at moderate values when the temperature is low.
The reason for this can be seen here:
https://commons.wikimedia.org/wiki/File:Planckian-locus.png
Tint is moving along the isotherm lines (with the plot corresponding to +-150),
but below 4000K some of that range is outside of the gamut. Not much can
be done there, other than possibly clipping those values...

Adding support for this to the compositor should be quite easy and is planned
as a next step.

Pull Request: https://projects.blender.org/blender/blender/pulls/123278
This commit is contained in:
Lukas Stockner 2024-06-27 23:27:58 +02:00 committed by Lukas Stockner
parent 9ae237d0b4
commit 6967255906
51 changed files with 668 additions and 39 deletions

@ -494,6 +494,9 @@ OCIO_ConstProcessorRcPtr *FallbackImpl::createDisplayProcessor(OCIO_ConstConfigR
const char * /*look*/,
const float scale,
const float exponent,
const float /*temperature*/,
const float /*tint*/,
const bool /*use_white_balance*/,
const bool inverse)
{
FallbackTransform transform;

@ -159,8 +159,8 @@ vec4 OCIO_ProcessColor(vec4 col, vec4 col_overlay)
/* Convert to scene linear (usually a no-op). */
col = OCIO_to_scene_linear(col);
/* Apply exposure in scene linear. */
col.rgb *= parameters.scale;
/* Apply exposure and white balance in scene linear. */
col = parameters.scene_linear_matrix * col;
/* Convert to display space. */
col = OCIO_to_display(col);

@ -257,10 +257,22 @@ OCIO_ConstProcessorRcPtr *OCIO_createDisplayProcessor(OCIO_ConstConfigRcPtr *con
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse)
{
return impl->createDisplayProcessor(
config, input, view, display, look, scale, exponent, inverse);
return impl->createDisplayProcessor(config,
input,
view,
display,
look,
scale,
exponent,
temperature,
tint,
use_white_balance,
inverse);
}
OCIO_PackedImageDesc *OCIO_createOCIO_PackedImageDesc(float *data,
@ -294,9 +306,12 @@ bool OCIO_gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr)
const bool use_hdr,
const bool use_white_balance)
{
return impl->gpuDisplayShaderBind(config,
input,
@ -307,9 +322,12 @@ bool OCIO_gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
scale,
exponent,
dither,
temperature,
tint,
use_predivide,
use_overlay,
use_hdr);
use_hdr,
use_white_balance);
}
void OCIO_gpuDisplayShaderUnbind()

@ -175,6 +175,9 @@ OCIO_ConstProcessorRcPtr *OCIO_createDisplayProcessor(OCIO_ConstConfigRcPtr *con
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse);
struct OCIO_PackedImageDesc *OCIO_createOCIO_PackedImageDesc(float *data,
@ -197,9 +200,12 @@ bool OCIO_gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr);
const bool use_hdr,
const bool use_white_balance);
void OCIO_gpuDisplayShaderUnbind(void);
void OCIO_gpuCacheFree(void);

@ -22,7 +22,9 @@ using namespace OCIO_NAMESPACE;
#include "MEM_guardedalloc.h"
#include "BLI_math_color.h"
#include "BLI_math_color.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "ocio_impl.h"
@ -37,6 +39,10 @@ using namespace OCIO_NAMESPACE;
# define __func__ __FUNCTION__
#endif
using blender::double4x4;
using blender::float3;
using blender::float3x3;
static void OCIO_reportError(const char *err)
{
std::cerr << "OpenColorIO Error: " << err << std::endl;
@ -670,15 +676,18 @@ OCIO_ConstProcessorRcPtr *OCIOImpl::createDisplayProcessor(OCIO_ConstConfigRcPtr
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse)
{
ConstConfigRcPtr config = *(ConstConfigRcPtr *)config_;
GroupTransformRcPtr group = GroupTransform::Create();
/* Exposure. */
if (scale != 1.0f) {
/* Always apply exposure in scene linear. */
/* Linear transforms. */
if (scale != 1.0f || use_white_balance) {
/* Always apply exposure and/or white balance in scene linear. */
ColorSpaceTransformRcPtr ct = ColorSpaceTransform::Create();
ct->setSrc(input);
ct->setDst(ROLE_SCENE_LINEAR);
@ -689,9 +698,26 @@ OCIO_ConstProcessorRcPtr *OCIOImpl::createDisplayProcessor(OCIO_ConstConfigRcPtr
/* Apply scale. */
MatrixTransformRcPtr mt = MatrixTransform::Create();
const double matrix[16] = {
scale, 0.0, 0.0, 0.0, 0.0, scale, 0.0, 0.0, 0.0, 0.0, scale, 0.0, 0.0, 0.0, 0.0, 1.0};
mt->setMatrix(matrix);
float3x3 matrix = float3x3::identity() * scale;
/* Apply white balance. */
if (use_white_balance) {
/* Compute white point of the scene space in XYZ.*/
float3x3 xyz_to_scene;
configGetXYZtoSceneLinear(config_, xyz_to_scene.ptr());
float3x3 scene_to_xyz = blender::math::invert(xyz_to_scene);
float3 target = scene_to_xyz * float3(1.0f);
/* Add operations to the matrix.
* Note: Since we're multiplying from the right, the operations here will be performed in
* reverse list order (scene-to-XYZ, then adaption, then XYZ-to-scene, then exposure). */
matrix *= xyz_to_scene;
matrix *= blender::math::chromatic_adaption_matrix(
blender::math::whitepoint_from_temp_tint(temperature, tint), target);
matrix *= scene_to_xyz;
}
mt->setMatrix(double4x4(blender::math::transpose(matrix)).base_ptr());
group->appendTransform(mt);
}

@ -87,6 +87,9 @@ class IOCIOImpl {
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse) = 0;
virtual OCIO_PackedImageDesc *createOCIO_PackedImageDesc(float *data,
@ -113,9 +116,12 @@ class IOCIOImpl {
const float /*scale*/,
const float /*exponent*/,
const float /*dither*/,
const float /*temperature*/,
const float /*tint*/,
const bool /*use_predivide*/,
const bool /*use_overlay*/,
const bool /*use_hdr*/)
const bool /*use_hdr*/,
const bool /*use_white_balance*/)
{
return false;
}
@ -200,6 +206,9 @@ class FallbackImpl : public IOCIOImpl {
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse);
OCIO_PackedImageDesc *createOCIO_PackedImageDesc(float *data,
@ -291,6 +300,9 @@ class OCIOImpl : public IOCIOImpl {
const char *look,
const float scale,
const float exponent,
const float temperature,
const float tint,
const bool use_white_balance,
const bool inverse);
OCIO_PackedImageDesc *createOCIO_PackedImageDesc(float *data,
@ -313,9 +325,12 @@ class OCIOImpl : public IOCIOImpl {
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr);
const bool use_hdr,
const bool use_white_balance);
void gpuDisplayShaderUnbind(void);
void gpuCacheFree(void);

@ -27,11 +27,17 @@
using namespace OCIO_NAMESPACE;
#include "BLI_math_color.h"
#include "BLI_math_color.hh"
#include "BLI_math_matrix.hh"
#include "MEM_guardedalloc.h"
#include "ocio_impl.h"
#include "ocio_shader_shared.hh"
using blender::float3x3;
/* **** OpenGL drawing routines using GLSL for color space transform ***** */
enum OCIO_GPUTextureSlots {
@ -547,8 +553,8 @@ static void updateGPUCurveMapping(OCIO_GPUCurveMappping &curvemap,
}
static void updateGPUDisplayParameters(OCIO_GPUShader &shader,
float scale,
float exponent,
float4x4 scene_linear_matrix,
float dither,
bool use_predivide,
bool use_overlay,
@ -560,8 +566,8 @@ static void updateGPUDisplayParameters(OCIO_GPUShader &shader,
do_update = true;
}
OCIO_GPUParameters &data = shader.parameters;
if (data.scale != scale) {
data.scale = scale;
if (data.scene_linear_matrix != scene_linear_matrix) {
data.scene_linear_matrix = scene_linear_matrix;
do_update = true;
}
if (data.exponent != exponent) {
@ -645,20 +651,22 @@ static OCIO_GPUDisplayShader &getGPUDisplayShader(
/* Create Processors.
*
* Scale and exponent are handled outside of OCIO shader so we can handle them
* as uniforms at the binding stage. OCIO would otherwise bake them into the
* shader code, requiring slow recompiles when interactively adjusting them.
* Scale, white balance and exponent are handled outside of OCIO shader so we
* can handle them as uniforms at the binding stage. OCIO would otherwise bake
* them into the shader code, requiring slow recompiles when interactively
* adjusting them.
*
* Note that OCIO does have the concept of dynamic properties, however there
* is no dynamic gamma and exposure is part of more expensive operations only.
*
* Since exposure must happen in scene linear, we use two processors. The input
* is usually scene linear already and so that conversion is often a no-op.
* Since exposure and white balance must happen in scene linear, we use two
* processors. The input is usually scene linear already and so that conversion
* is often a no-op.
*/
OCIO_ConstProcessorRcPtr *processor_to_scene_linear = OCIO_configGetProcessorWithNames(
config, input, ROLE_SCENE_LINEAR);
OCIO_ConstProcessorRcPtr *processor_to_display = OCIO_createDisplayProcessor(
config, ROLE_SCENE_LINEAR, view, display, look, 1.0f, 1.0f, false);
config, ROLE_SCENE_LINEAR, view, display, look, 1.0f, 1.0f, 0.0f, 0.0f, false, false);
/* Create shader descriptions. */
if (processor_to_scene_linear && processor_to_display) {
@ -724,9 +732,12 @@ bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr)
const bool use_hdr,
const bool use_white_balance)
{
/* Get GPU shader from cache or create new one. */
OCIO_GPUDisplayShader &display_shader = getGPUDisplayShader(
@ -761,7 +772,24 @@ bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
GPU_uniformbuf_bind(textures.uniforms_buffer, UNIFORMBUF_SLOT_LUTS);
}
updateGPUDisplayParameters(shader, scale, exponent, dither, use_predivide, use_overlay, use_hdr);
float3x3 matrix = float3x3::identity() * scale;
if (use_white_balance) {
/* Compute white point of the scene space in XYZ.*/
float3x3 xyz_to_scene;
configGetXYZtoSceneLinear(config, xyz_to_scene.ptr());
float3x3 scene_to_xyz = blender::math::invert(xyz_to_scene);
float3 target = scene_to_xyz * float3(1.0f);
/* Add operations to the matrix.
* Note: Since we're multiplying from the right, the operations here will be performed in
* reverse list order (scene-to-XYZ, then adaption, then XYZ-to-scene, then exposure). */
matrix *= xyz_to_scene;
matrix *= blender::math::chromatic_adaption_matrix(
blender::math::whitepoint_from_temp_tint(temperature, tint), target);
matrix *= scene_to_xyz;
}
updateGPUDisplayParameters(
shader, exponent, float4x4(matrix), dither, use_predivide, use_overlay, use_hdr);
GPU_uniformbuf_bind(shader.parameters_buffer, UNIFORMBUF_SLOT_DISPLAY);
/* TODO(fclem): remove remains of IMM. */

@ -32,11 +32,12 @@ struct OCIO_GPUCurveMappingParameters {
struct OCIO_GPUParameters {
float dither;
float scale;
float exponent;
bool32_t use_predivide;
bool32_t use_overlay;
bool32_t use_hdr;
int _pad0;
int _pad1;
int _pad2;
float4x4 scene_linear_matrix;
};

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2856
view_settings.white_balance_tint = 0.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4873
view_settings.white_balance_tint = -3.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6774
view_settings.white_balance_tint = -6.4

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 5002
view_settings.white_balance_tint = 9.6

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 5501
view_settings.white_balance_tint = 10.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6502
view_settings.white_balance_tint = 9.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 7506
view_settings.white_balance_tint = 9.6

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 9298
view_settings.white_balance_tint = 9.5

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 5454
view_settings.white_balance_tint = -13.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6424
view_settings.white_balance_tint = 21.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4991
view_settings.white_balance_tint = 11.1

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4001
view_settings.white_balance_tint = 0.5

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 3002
view_settings.white_balance_tint = 0.6

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4225
view_settings.white_balance_tint = 5.9

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 3447
view_settings.white_balance_tint = 2.5

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2940
view_settings.white_balance_tint = -2.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6342
view_settings.white_balance_tint = 32.7

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4148
view_settings.white_balance_tint = 18.6

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6489
view_settings.white_balance_tint = 10.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4995
view_settings.white_balance_tint = 9.7

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4147
view_settings.white_balance_tint = 0.3

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2733
view_settings.white_balance_tint = -2.0

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2997
view_settings.white_balance_tint = -2.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4103
view_settings.white_balance_tint = -1.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 5108
view_settings.white_balance_tint = 1.6

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 6598
view_settings.white_balance_tint = 2.7

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2852
view_settings.white_balance_tint = -0.9

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 2840
view_settings.white_balance_tint = 12.8

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 3079
view_settings.white_balance_tint = 48.9

@ -0,0 +1,5 @@
import bpy
view_settings = bpy.context.scene.view_settings
view_settings.white_balance_temperature = 4070
view_settings.white_balance_tint = 3.3

@ -569,6 +569,24 @@ class AddPresetEEVEERaytracing(AddPresetBase, Operator):
preset_subdir = "eevee/raytracing"
class AddPresetColorManagementWhiteBalance(AddPresetBase, Operator):
"""Add or remove a white balance preset"""
bl_idname = "render.color_management_white_balance_preset_add"
bl_label = "Add White Balance Preset"
preset_menu = "RENDER_PT_color_management_white_balance_presets"
preset_defines = [
"view_settings = bpy.context.scene.view_settings",
]
preset_values = [
"view_settings.white_balance_temperature",
"view_settings.white_balance_tint",
]
preset_subdir = "color_management/white_balance"
class AddPresetNodeColor(AddPresetBase, Operator):
"""Add or remove a Node Color Preset"""
bl_idname = "node.node_color_preset_add"
@ -981,6 +999,7 @@ classes = (
AddPresetGpencilBrush,
AddPresetGpencilMaterial,
AddPresetEEVEERaytracing,
AddPresetColorManagementWhiteBalance,
ExecutePreset,
WM_MT_operator_presets,
WM_PT_operator_presets,

@ -119,7 +119,7 @@ class RENDER_PT_color_management_display_settings(RenderButtonsPanel, Panel):
class RENDER_PT_color_management_curves(RenderButtonsPanel, Panel):
bl_label = "Use Curves"
bl_label = "Curves"
bl_parent_id = "RENDER_PT_color_management"
bl_options = {'DEFAULT_CLOSED'}
COMPAT_ENGINES = {
@ -145,11 +145,62 @@ class RENDER_PT_color_management_curves(RenderButtonsPanel, Panel):
layout.use_property_split = False
layout.use_property_decorate = False # No animation.
layout.enabled = view.use_curve_mapping
layout.active = view.use_curve_mapping
layout.template_curve_mapping(view, "curve_mapping", type='COLOR', levels=True)
class RENDER_PT_color_management_white_balance_presets(PresetPanel, Panel):
bl_label = "White Balance Presets"
preset_subdir = "color_management/white_balance"
preset_operator = "script.execute_preset"
preset_add_operator = "render.color_management_white_balance_preset_add"
class RENDER_PT_color_management_white_balance(RenderButtonsPanel, Panel):
bl_label = "White Balance"
bl_parent_id = "RENDER_PT_color_management"
bl_options = {'DEFAULT_CLOSED'}
COMPAT_ENGINES = {
'BLENDER_RENDER',
'BLENDER_EEVEE',
'BLENDER_EEVEE_NEXT',
'BLENDER_WORKBENCH',
}
def draw_header(self, context):
scene = context.scene
view = scene.view_settings
self.layout.prop(view, "use_white_balance", text="")
def draw_header_preset(self, context):
layout = self.layout
scene = context.scene
view = scene.view_settings
RENDER_PT_color_management_white_balance_presets.draw_panel_header(layout)
eye = layout.operator("ui.eyedropper_color", text="", icon='EYEDROPPER')
eye.prop_data_path = "scene.view_settings.white_balance_whitepoint"
def draw(self, context):
layout = self.layout
scene = context.scene
view = scene.view_settings
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
layout.active = view.use_white_balance
col = layout.column()
col.prop(view, "white_balance_temperature")
col.prop(view, "white_balance_tint")
class RENDER_PT_eevee_ambient_occlusion(RenderButtonsPanel, Panel):
bl_label = "Ambient Occlusion"
bl_options = {'DEFAULT_CLOSED'}
@ -1385,6 +1436,8 @@ classes = (
RENDER_PT_color_management,
RENDER_PT_color_management_display_settings,
RENDER_PT_color_management_curves,
RENDER_PT_color_management_white_balance_presets,
RENDER_PT_color_management_white_balance,
)
if __name__ == "__main__": # only for live edit.

@ -29,7 +29,7 @@ extern "C" {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 3
#define BLENDER_FILE_SUBVERSION 4
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

@ -1954,6 +1954,8 @@ void BKE_color_managed_view_settings_copy(ColorManagedViewSettings *new_settings
new_settings->flag = settings->flag;
new_settings->exposure = settings->exposure;
new_settings->gamma = settings->gamma;
new_settings->temperature = settings->temperature;
new_settings->tint = settings->tint;
if (settings->curve_mapping) {
new_settings->curve_mapping = BKE_curvemapping_copy(settings->curve_mapping);

@ -13,6 +13,7 @@
#include "BLI_color.hh"
#include "BLI_math_base.hh"
#include "BLI_math_matrix_types.hh"
namespace blender::math {
@ -39,4 +40,14 @@ inline ColorSceneLinearByteEncoded4b<Alpha> interpolate(
math::interpolate(a.a, b.a, t)};
}
blender::float3 whitepoint_from_temp_tint(const float temperature, const float tint);
bool whitepoint_to_temp_tint(const blender::float3 white, float &temperature, float &tint);
/* Computes a matrix to perform chromatic adaption from a source white point (given in the form of
* temperature and tint) to a target white point (given as its XYZ values).
* The resulting matrix operates on XYZ values, the caller is responsible for RGB conversion. */
blender::float3x3 chromatic_adaption_matrix(const blender::float3 from_XYZ,
const blender::float3 to_XYZ);
} // namespace blender::math

@ -6,7 +6,11 @@
* \ingroup bli
*/
#include "BLI_array.hh"
#include "BLI_math_color.h"
#include "BLI_math_color.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_simd.hh"
#include "BLI_utildefines.h"
@ -839,3 +843,124 @@ void BLI_init_srgb_conversion()
BLI_color_to_srgb_table[i] = ushort(b * 0x100);
}
}
namespace blender::math {
struct locus_entry_t {
float mired; /* Inverse temperature */
float2 uv; /* CIE 1960 uv coordinates */
float t; /* Isotherm parameter */
float dist(const float2 p) const
{
const float2 diff = p - uv;
return diff.y - t * diff.x;
}
};
/* Tabulated approximation of the Planckian locus.
* Based on http://www.brucelindbloom.com/Eqn_XYZ_to_T.html.
* Original source:
* "Color Science: Concepts and Methods, Quantitative Data and Formulae", Second Edition,
* Gunter Wyszecki and W. S. Stiles, John Wiley & Sons, 1982, pp. 227, 228. */
static const std::array<locus_entry_t, 31> planck_locus{{
{0.0f, {0.18006f, 0.26352f}, -0.24341f}, {10.0f, {0.18066f, 0.26589f}, -0.25479f},
{20.0f, {0.18133f, 0.26846f}, -0.26876f}, {30.0f, {0.18208f, 0.27119f}, -0.28539f},
{40.0f, {0.18293f, 0.27407f}, -0.30470f}, {50.0f, {0.18388f, 0.27709f}, -0.32675f},
{60.0f, {0.18494f, 0.28021f}, -0.35156f}, {70.0f, {0.18611f, 0.28342f}, -0.37915f},
{80.0f, {0.18740f, 0.28668f}, -0.40955f}, {90.0f, {0.18880f, 0.28997f}, -0.44278f},
{100.0f, {0.19032f, 0.29326f}, -0.47888f}, {125.0f, {0.19462f, 0.30141f}, -0.58204f},
{150.0f, {0.19962f, 0.30921f}, -0.70471f}, {175.0f, {0.20525f, 0.31647f}, -0.84901f},
{200.0f, {0.21142f, 0.32312f}, -1.0182f}, {225.0f, {0.21807f, 0.32909f}, -1.2168f},
{250.0f, {0.22511f, 0.33439f}, -1.4512f}, {275.0f, {0.23247f, 0.33904f}, -1.7298f},
{300.0f, {0.24010f, 0.34308f}, -2.0637f}, {325.0f, {0.24792f, 0.34655f}, -2.4681f},
{350.0f, {0.25591f, 0.34951f}, -2.9641f}, {375.0f, {0.26400f, 0.35200f}, -3.5814f},
{400.0f, {0.27218f, 0.35407f}, -4.3633f}, {425.0f, {0.28039f, 0.35577f}, -5.3762f},
{450.0f, {0.28863f, 0.35714f}, -6.7262f}, {475.0f, {0.29685f, 0.35823f}, -8.5955f},
{500.0f, {0.30505f, 0.35907f}, -11.324f}, {525.0f, {0.31320f, 0.35968f}, -15.628f},
{550.0f, {0.32129f, 0.36011f}, -23.325f}, {575.0f, {0.32931f, 0.36038f}, -40.770f},
{600.0f, {0.33724f, 0.36051f}, -116.45f},
}};
bool whitepoint_to_temp_tint(const blender::float3 white, float &temperature, float &tint)
{
/* Convert XYZ -> CIE 1960 uv. */
const float2 uv = float2{4.0f * white.x, 6.0f * white.y} / dot(white, {1.0f, 15.0f, 3.0f});
/* Find first entry that's "to the right" of the white point. */
auto check = [uv](const float val, const locus_entry_t &entry) { return entry.dist(uv) < val; };
const auto entry = std::upper_bound(planck_locus.begin(), planck_locus.end(), 0.0f, check);
if (entry == planck_locus.begin() || entry == planck_locus.end()) {
return false;
}
const size_t i = (size_t)(entry - planck_locus.begin());
const locus_entry_t &low = planck_locus[i - 1], high = planck_locus[i];
/* Find closest point on locus. */
const float d_low = low.dist(uv) / sqrtf(1.0f + low.t * low.t);
const float d_high = high.dist(uv) / sqrtf(1.0f + high.t * high.t);
const float f = d_low / (d_low - d_high);
/* Find tint based on distance to closest point on locus. */
const float2 uv_temp = interpolate(low.uv, high.uv, f);
const float abs_tint = length(uv - uv_temp) * 3000.0f;
if (abs_tint > 150.0f) {
return false;
}
temperature = 1e6f / interpolate(low.mired, high.mired, f);
tint = abs_tint * ((uv.x < uv_temp.x) ? 1.0f : -1.0f);
return true;
}
blender::float3 whitepoint_from_temp_tint(const float temperature, const float tint)
{
/* Find table entry. */
const float mired = clamp(
1e6f / temperature, planck_locus[0].mired, planck_locus[planck_locus.size() - 1].mired);
auto check = [](const locus_entry_t &entry, const float val) { return entry.mired < val; };
const auto entry = std::lower_bound(planck_locus.begin(), planck_locus.end(), mired, check);
const size_t i = (size_t)(entry - planck_locus.begin());
const locus_entry_t &low = planck_locus[i - 1], high = planck_locus[i];
/* Find interpolation factor. */
const float f = (mired - low.mired) / (high.mired - low.mired);
/* Interpolate point along Planckian locus. */
float2 uv = interpolate(low.uv, high.uv, f);
/* Compute and interpolate isotherm. */
const float2 isotherm0 = normalize(float2(1.0f, low.t));
const float2 isotherm1 = normalize(float2(1.0f, high.t));
const float2 isotherm = normalize(interpolate(isotherm0, isotherm1, f));
/* Offset away from the Planckian locus according to the tint.
* Tint is parametrized such that +-3000 tint corresponds to +-1 delta UV. */
uv -= isotherm * tint / 3000.0f;
/* Convert CIE 1960 uv -> xyY. */
const float x = 3.0f * uv.x / (2.0f * uv.x - 8.0f * uv.y + 4.0f);
const float y = 2.0f * uv.y / (2.0f * uv.x - 8.0f * uv.y + 4.0f);
/* Convert xyY -> XYZ (assuming Y=1). */
return float3{x / y, 1.0f, (1.0f - x - y) / y};
}
blender::float3x3 chromatic_adaption_matrix(const blender::float3 from_XYZ,
const blender::float3 to_XYZ)
{
/* Bradford transformation matrix (XYZ -> LMS). */
static const blender::float3x3 bradford{
{0.8951f, -0.7502f, 0.0389f},
{0.2664f, 1.7135f, -0.0685f},
{-0.1614f, 0.0367f, 1.0296f},
};
/* Compute white points in LMS space. */
const float3 from_LMS = bradford * from_XYZ / from_XYZ.y;
const float3 to_LMS = bradford * to_XYZ / to_XYZ.y;
/* Assemble full transform: XYZ -> LMS -> adapted LMS -> adapted XYZ. */
return invert(bradford) * from_scale<float3x3>(to_LMS / from_LMS) * bradford;
}
} // namespace blender::math

@ -4230,6 +4230,13 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 403, 4)) {
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
scene->view_settings.temperature = 6500.0f;
scene->view_settings.tint = 10.0f;
}
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

@ -23,11 +23,14 @@
#include "BKE_context.hh"
#include "BKE_cryptomatte.h"
#include "BKE_image.h"
#include "BKE_report.hh"
#include "BKE_screen.hh"
#include "NOD_composite.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.h"
#include "UI_interface.hh"
@ -84,7 +87,29 @@ static bool eyedropper_init(bContext *C, wmOperator *op)
{
Eyedropper *eye = MEM_cnew<Eyedropper>(__func__);
PropertyRNA *prop;
if ((prop = RNA_struct_find_property(op->ptr, "prop_data_path")) &&
RNA_property_is_set(op->ptr, prop))
{
char *prop_data_path = RNA_string_get_alloc(op->ptr, "prop_data_path", nullptr, 0, nullptr);
BLI_SCOPED_DEFER([&] { MEM_SAFE_FREE(prop_data_path); });
if (!prop_data_path || prop_data_path[0] == '\0') {
MEM_freeN(eye);
return false;
}
PointerRNA ctx_ptr = RNA_pointer_create(nullptr, &RNA_Context, C);
if (!RNA_path_resolve(&ctx_ptr, prop_data_path, &eye->ptr, &eye->prop)) {
BKE_reportf(op->reports, RPT_ERROR, "Could not resolve path '%s'", prop_data_path);
MEM_freeN(eye);
return false;
}
eye->is_undo = true;
}
else {
uiBut *but = UI_context_active_but_prop_get(C, &eye->ptr, &eye->prop, &eye->index);
eye->is_undo = UI_but_flag_is_set(but, UI_BUT_UNDO);
}
const enum PropertySubType prop_subtype = eye->prop ? RNA_property_subtype(eye->prop) :
PropertySubType(0);
@ -99,8 +124,6 @@ static bool eyedropper_init(bContext *C, wmOperator *op)
}
op->customdata = eye;
eye->is_undo = UI_but_flag_is_set(but, UI_BUT_UNDO);
float col[4];
RNA_property_float_get_array(&eye->ptr, eye->prop, col);
if (eye->ptr.type == &RNA_CompositorNodeCryptomatteV2) {
@ -585,4 +608,14 @@ void UI_OT_eyedropper_color(wmOperatorType *ot)
/* flags */
ot->flag = OPTYPE_UNDO | OPTYPE_BLOCKING | OPTYPE_INTERNAL;
/* Paths relative to the context. */
PropertyRNA *prop;
prop = RNA_def_string(ot->srna,
"prop_data_path",
nullptr,
0,
"Data Path",
"Path of property to be set with the depth");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
}

@ -6884,6 +6884,14 @@ void uiTemplateColormanagedViewSettings(uiLayout *layout,
uiTemplateCurveMapping(
col, &view_transform_ptr, "curve_mapping", 'c', true, false, false, false);
}
col = uiLayoutColumn(layout, false);
uiItemR(col, &view_transform_ptr, "use_white_balance", UI_ITEM_NONE, nullptr, ICON_NONE);
if (view_settings->flag & COLORMANAGE_VIEW_USE_WHITE_BALANCE) {
uiItemR(
col, &view_transform_ptr, "white_balance_temperature", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, &view_transform_ptr, "white_balance_tint", UI_ITEM_NONE, nullptr, ICON_NONE);
}
}
/** \} */

@ -85,6 +85,14 @@ BLI_INLINE void IMB_colormanagement_scene_linear_to_aces(float aces[3],
const float scene_linear[3]);
const float *IMB_colormanagement_get_xyz_to_scene_linear();
/**
* Functions for converting between color temperature/tint and RGB white points.
*/
void IMB_colormanagement_get_view_whitepoint(const ColorManagedViewSettings *view_settings,
float whitepoint[3]);
bool IMB_colormanagement_set_view_whitepoint(ColorManagedViewSettings *view_settings,
const float whitepoint[3]);
/** \} */
/* -------------------------------------------------------------------- */

@ -30,6 +30,7 @@
#include "BLI_blenlib.h"
#include "BLI_math_color.h"
#include "BLI_math_color.hh"
#include "BLI_rect.h"
#include "BLI_string.h"
#include "BLI_task.h"
@ -195,6 +196,8 @@ struct ColormanageCacheViewSettings {
float exposure;
float gamma;
float dither;
float temperature;
float tint;
CurveMapping *curve_mapping;
};
@ -213,6 +216,8 @@ struct ColormanageCacheData {
float exposure; /* exposure value cached buffer is calculated with */
float gamma; /* gamma value cached buffer is calculated with */
float dither; /* dither value cached buffer is calculated with */
float temperature; /* temperature value cached buffer is calculated with */
float tint; /* tint value cached buffer is calculated with */
CurveMapping *curve_mapping; /* curve mapping used for cached buffer */
int curve_mapping_timestamp; /* time stamp of curve mapping used for cached buffer */
};
@ -299,6 +304,8 @@ static void colormanage_view_settings_to_cache(ImBuf *ibuf,
cache_view_settings->exposure = view_settings->exposure;
cache_view_settings->gamma = view_settings->gamma;
cache_view_settings->dither = ibuf->dither;
cache_view_settings->temperature = view_settings->temperature;
cache_view_settings->tint = view_settings->tint;
cache_view_settings->flag = view_settings->flag;
cache_view_settings->curve_mapping = view_settings->curve_mapping;
}
@ -378,7 +385,9 @@ static uchar *colormanage_cache_get(ImBuf *ibuf,
if (cache_data->look != view_settings->look ||
cache_data->exposure != view_settings->exposure ||
cache_data->gamma != view_settings->gamma || cache_data->dither != view_settings->dither ||
cache_data->flag != view_settings->flag || cache_data->curve_mapping != curve_mapping ||
cache_data->temperature != view_settings->temperature ||
cache_data->tint != view_settings->tint || cache_data->flag != view_settings->flag ||
cache_data->curve_mapping != curve_mapping ||
cache_data->curve_mapping_timestamp != curve_mapping_timestamp)
{
*cache_handle = nullptr;
@ -424,6 +433,8 @@ static void colormanage_cache_put(ImBuf *ibuf,
cache_data->exposure = view_settings->exposure;
cache_data->gamma = view_settings->gamma;
cache_data->dither = view_settings->dither;
cache_data->temperature = view_settings->temperature;
cache_data->tint = view_settings->tint;
cache_data->flag = view_settings->flag;
cache_data->curve_mapping = curve_mapping;
cache_data->curve_mapping_timestamp = curve_mapping_timestamp;
@ -874,8 +885,11 @@ static ColorSpace *display_transform_get_colorspace(
static OCIO_ConstCPUProcessorRcPtr *create_display_buffer_processor(const char *look,
const char *view_transform,
const char *display,
float exposure,
float gamma,
const float exposure,
const float gamma,
const float temperature,
const float tint,
const bool use_white_balance,
const char *from_colorspace)
{
OCIO_ConstConfigRcPtr *config = OCIO_getCurrentConfig();
@ -890,6 +904,9 @@ static OCIO_ConstCPUProcessorRcPtr *create_display_buffer_processor(const char *
(use_look) ? look : "",
scale,
exponent,
temperature,
tint,
use_white_balance,
false);
OCIO_configRelease(config);
@ -982,6 +999,9 @@ static OCIO_ConstCPUProcessorRcPtr *display_from_scene_linear_processor(
nullptr,
1.0f,
1.0f,
0.0f,
0.0f,
false,
false);
OCIO_configRelease(config);
@ -1011,8 +1031,17 @@ static OCIO_ConstCPUProcessorRcPtr *display_to_scene_linear_processor(ColorManag
OCIO_ConstProcessorRcPtr *processor = nullptr;
if (view_name && config) {
processor = OCIO_createDisplayProcessor(
config, global_role_scene_linear, view_name, display->name, nullptr, 1.0f, 1.0f, true);
processor = OCIO_createDisplayProcessor(config,
global_role_scene_linear,
view_name,
display->name,
nullptr,
1.0f,
1.0f,
0.0f,
0.0f,
false,
true);
OCIO_configRelease(config);
}
@ -1056,6 +1085,8 @@ void IMB_colormanagement_init_default_view_settings(
view_settings->flag = 0;
view_settings->gamma = 1.0f;
view_settings->exposure = 0.0f;
view_settings->temperature = 6500.0f;
view_settings->tint = 10.0f;
view_settings->curve_mapping = nullptr;
}
@ -1484,6 +1515,29 @@ const float *IMB_colormanagement_get_xyz_to_scene_linear()
/** \} */
/* -------------------------------------------------------------------- */
/** \name Functions for converting between color temperature/tint and RGB white points
* \{ */
void IMB_colormanagement_get_view_whitepoint(const ColorManagedViewSettings *view_settings,
float whitepoint[3])
{
blender::float3 xyz = blender::math::whitepoint_from_temp_tint(view_settings->temperature,
view_settings->tint);
IMB_colormanagement_xyz_to_scene_linear(whitepoint, xyz);
}
bool IMB_colormanagement_set_view_whitepoint(ColorManagedViewSettings *view_settings,
const float whitepoint[3])
{
blender::float3 xyz;
IMB_colormanagement_scene_linear_to_xyz(xyz, whitepoint);
return blender::math::whitepoint_to_temp_tint(
xyz, view_settings->temperature, view_settings->tint);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Threaded Display Buffer Transform Routines
* \{ */
@ -3934,12 +3988,16 @@ ColormanageProcessor *IMB_colormanagement_display_processor_new(
cm_processor->is_data_result = display_space->is_data;
}
const bool use_white_balance = applied_view_settings->flag & COLORMANAGE_VIEW_USE_WHITE_BALANCE;
cm_processor->cpu_processor = create_display_buffer_processor(
applied_view_settings->look,
applied_view_settings->view_transform,
display_settings->display_device,
applied_view_settings->exposure,
applied_view_settings->gamma,
applied_view_settings->temperature,
applied_view_settings->tint,
use_white_balance,
global_role_scene_linear);
if (applied_view_settings->flag & COLORMANAGE_VIEW_USE_CURVES) {
@ -4252,6 +4310,10 @@ bool IMB_colormanagement_setup_glsl_draw_from_space(
const float gamma = applied_view_settings->gamma;
const float scale = (exposure == 0.0f) ? 1.0f : powf(2.0f, exposure);
const float exponent = (gamma == 1.0f) ? 1.0f : 1.0f / max_ff(FLT_EPSILON, gamma);
const float temperature = applied_view_settings->temperature;
const float tint = applied_view_settings->tint;
const bool use_white_balance = (applied_view_settings->flag &
COLORMANAGE_VIEW_USE_WHITE_BALANCE) != 0;
const bool use_hdr = GPU_hdr_support() &&
(applied_view_settings->flag & COLORMANAGE_VIEW_USE_HDR) != 0;
@ -4267,9 +4329,12 @@ bool IMB_colormanagement_setup_glsl_draw_from_space(
scale,
exponent,
dither,
temperature,
tint,
predivide,
do_overlay_merge,
use_hdr);
use_hdr,
use_white_balance);
OCIO_configRelease(config);

@ -197,6 +197,9 @@ typedef struct ColorManagedViewSettings {
float exposure;
/** Post-display gamma transform. */
float gamma;
/** White balance parameters. */
float temperature;
float tint;
/** Pre-display RGB curves transform. */
struct CurveMapping *curve_mapping;
void *_pad2;
@ -215,4 +218,5 @@ typedef struct ColorManagedColorspaceSettings {
enum {
COLORMANAGE_VIEW_USE_CURVES = (1 << 0),
COLORMANAGE_VIEW_USE_HDR = (1 << 1),
COLORMANAGE_VIEW_USE_WHITE_BALANCE = (1 << 2),
};

@ -558,6 +558,18 @@ static std::optional<std::string> rna_ColorManagedViewSettings_path(const Pointe
return "view_settings";
}
static void rna_ColorManagedViewSettings_whitepoint_get(PointerRNA *ptr, float value[3])
{
const ColorManagedViewSettings *view_settings = (ColorManagedViewSettings *)ptr->data;
IMB_colormanagement_get_view_whitepoint(view_settings, value);
}
static void rna_ColorManagedViewSettings_whitepoint_set(PointerRNA *ptr, const float value[3])
{
ColorManagedViewSettings *view_settings = (ColorManagedViewSettings *)ptr->data;
IMB_colormanagement_set_view_whitepoint(view_settings, value);
}
static bool rna_ColorManagedColorspaceSettings_is_data_get(PointerRNA *ptr)
{
ColorManagedColorspaceSettings *colorspace = (ColorManagedColorspaceSettings *)ptr->data;
@ -1309,6 +1321,41 @@ static void rna_def_colormanage(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Use Curves", "Use RGB curved for pre-display transformation");
RNA_def_property_update(prop, NC_WINDOW, "rna_ColorManagement_update");
prop = RNA_def_property(srna, "use_white_balance", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", COLORMANAGE_VIEW_USE_WHITE_BALANCE);
RNA_def_property_ui_text(
prop, "Use White Balance", "Perform chromatic adaption from a different white point");
RNA_def_property_update(prop, NC_WINDOW, "rna_ColorManagement_update");
prop = RNA_def_property(srna, "white_balance_temperature", PROP_FLOAT, PROP_COLOR_TEMPERATURE);
RNA_def_property_float_sdna(prop, nullptr, "temperature");
RNA_def_property_float_default(prop, 6500.0f);
RNA_def_property_range(prop, 1800.0f, 100000.0f);
RNA_def_property_ui_range(prop, 2000.0f, 11000.0f, 100, 0);
RNA_def_property_ui_text(prop, "Temperature", "Color temperature of the scene's white point");
RNA_def_property_update(prop, NC_WINDOW, "rna_ColorManagement_update");
prop = RNA_def_property(srna, "white_balance_tint", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_float_sdna(prop, nullptr, "tint");
RNA_def_property_float_default(prop, 10.0f);
RNA_def_property_range(prop, -500.0f, 500.0f);
RNA_def_property_ui_range(prop, -150.0f, 150.0f, 1, 1);
RNA_def_property_ui_text(
prop, "Tint", "Color tint of the scene's white point (the default of 10 matches daylight)");
RNA_def_property_update(prop, NC_WINDOW, "rna_ColorManagement_update");
prop = RNA_def_property(srna, "white_balance_whitepoint", PROP_FLOAT, PROP_COLOR);
RNA_def_property_array(prop, 3);
RNA_def_property_float_funcs(prop,
"rna_ColorManagedViewSettings_whitepoint_get",
"rna_ColorManagedViewSettings_whitepoint_set",
nullptr);
RNA_def_property_ui_text(prop,
"White Point",
"The color which gets mapped to white "
"(automatically converted to/from temperature and tint)");
RNA_def_property_update(prop, NC_WINDOW, "rna_ColorManagement_update");
prop = RNA_def_property(srna, "use_hdr_view", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", COLORMANAGE_VIEW_USE_HDR);
RNA_def_property_ui_text(