diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index cd6ce644..12c7a329 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -76,6 +76,9 @@ existed. | `gfx.postfx.bloom yes/no` | `yes` | glow around bright parts of the image | | `gfx.postfx.bloom.intensity` | `0.04` | share of the glow in the image | | `gfx.postfx.bloom.threshold` | `0.0` | luminance below which nothing glows; 0 = everything contributes a little (physically based) | +| `gfx.postfx.whitebalance yes/no` | `yes` | white balance of the camera, applied to the image before the tonemapping (chromatic adaptation in the Bradford cone space) | +| `gfx.postfx.whitebalance.temperature` | `7500` | 4000..25000 K: colour temperature of the light the camera takes as white. 6500 = no change; more makes the image warmer, less makes it cooler (as in a camera or a photo editor) | +| `gfx.postfx.whitebalance.tint` | `0.0` | -1..1: shift off the daylight colours; + makes the image more magenta, - more green | The automatic exposure and the bloom need the post processing pipeline (`gfx.skippipeline no`, the default). Model previews of the editor use none of these. diff --git a/rendering/common/exposure.cpp b/rendering/common/exposure.cpp index e8dc3c12..1d5c98ce 100644 --- a/rendering/common/exposure.cpp +++ b/rendering/common/exposure.cpp @@ -24,6 +24,51 @@ std::size_t const BLOOM_LEVELS{6}; int const BLOOM_MIN_SIZE{8}; // soft knee of the threshold, a fraction of it float const BLOOM_KNEE{0.5f}; +// colour temperature of the white the image is balanced to: the white of the display (d65 is the daylight of about 6500 K) +float const WHITE_BALANCE_REFERENCE{6500.f}; +// distance off the daylight locus (duv, cie 1960 ucs) of the tint at its ends (-1, 1) +float const WHITE_BALANCE_TINT_DUV{0.02f}; + +// chromaticity (cie xy) of the cie daylight of the correlated colour temperature Kelvin; the formula holds for 4000..25000 K +glm::dvec2 daylight_chromaticity(double const Kelvin) +{ + auto const t{std::clamp(Kelvin, 4000.0, 25000.0)}; + auto const x{t <= 7000.0 ? (-4.6070e9 / (t * t * t) + 2.9678e6 / (t * t) + 0.09911e3 / t + 0.244063) + : (-2.0064e9 / (t * t * t) + 1.9018e6 / (t * t) + 0.24748e3 / t + 0.237040)}; + return glm::dvec2{x, -3.0 * x * x + 2.87 * x - 0.275}; +} + +glm::dvec2 xy_to_uv(glm::dvec2 const &Xy) +{ + auto const d{-2.0 * Xy.x + 12.0 * Xy.y + 3.0}; + return glm::dvec2{4.0 * Xy.x / d, 6.0 * Xy.y / d}; +} + +glm::dvec2 uv_to_xy(glm::dvec2 const &Uv) +{ + auto const d{2.0 * Uv.x - 8.0 * Uv.y + 4.0}; + return glm::dvec2{3.0 * Uv.x / d, 2.0 * Uv.y / d}; +} + +// cone responses (bradford) of the light of the temperature Kelvin, taken off the daylight locus by Tint: + towards green +// (v grows), so a positive tint takes the green out of the image and makes it magenta +glm::dvec3 white_cone_response(double const Kelvin, double const Tint) +{ + auto uv{xy_to_uv(daylight_chromaticity(Kelvin))}; + if (Tint != 0.0) + { + auto const along{xy_to_uv(daylight_chromaticity(Kelvin + 100.0)) - xy_to_uv(daylight_chromaticity(Kelvin - 100.0))}; + auto normal{glm::normalize(glm::dvec2{-along.y, along.x})}; + if (normal.y < 0.0) + normal = -normal; + uv += normal * (Tint * WHITE_BALANCE_TINT_DUV); + } + auto const xy{uv_to_xy(uv)}; + glm::dvec3 const xyz{xy.x / xy.y, 1.0, (1.0 - xy.x - xy.y) / xy.y}; + return glm::dvec3{0.8951 * xyz.x + 0.2664 * xyz.y - 0.1614 * xyz.z, + -0.7502 * xyz.x + 1.7135 * xyz.y + 0.0367 * xyz.z, + 0.0389 * xyz.x - 0.0685 * xyz.y + 1.0296 * xyz.z}; +} } // namespace @@ -77,6 +122,15 @@ glm::vec4 exposure_settings::tonemap_params(std::size_t const Bloomlevels) return glm::vec4{bloom, Global.gfx_postfx_autoexposure ? 1.f : 0.f, std::exp2(Global.gfx_postfx_autoexposure_compensation), Bloomlevels > 0 ? 1.f / static_cast(Bloomlevels) : 0.f}; } +glm::vec3 exposure_settings::white_balance() +{ + if (!Global.gfx_postfx_whitebalance) + return glm::vec3{0.f}; + // von kries: the light of the set temperature becomes the white of the display; the luminance of grey stays + auto const gains{white_cone_response(WHITE_BALANCE_REFERENCE, 0.0) / white_cone_response(Global.gfx_postfx_whitebalance_temperature, Global.gfx_postfx_whitebalance_tint)}; + return glm::vec3{gains}; +} + void exposure_settings::draw_debug_ui(float const *Readback) { ImGui::Checkbox("automatic exposure (gfx.postfx.autoexposure)", &Global.gfx_postfx_autoexposure); @@ -90,6 +144,9 @@ void exposure_settings::draw_debug_ui(float const *Readback) ImGui::Checkbox("bloom (gfx.postfx.bloom)", &Global.gfx_postfx_bloom); ImGui::SliderFloat("bloom intensity", &Global.gfx_postfx_bloom_intensity, 0.f, 0.5f); ImGui::SliderFloat("bloom threshold", &Global.gfx_postfx_bloom_threshold, 0.f, 10.f); + ImGui::Checkbox("white balance (gfx.postfx.whitebalance)", &Global.gfx_postfx_whitebalance); + ImGui::SliderFloat("temperature K, 6500 = none, more = warmer", &Global.gfx_postfx_whitebalance_temperature, 4000.f, 12000.f, "%.0f"); + ImGui::SliderFloat("tint, + magenta, - green", &Global.gfx_postfx_whitebalance_tint, -1.f, 1.f, "%.2f"); } } // namespace gfx diff --git a/rendering/common/exposure.h b/rendering/common/exposure.h index 54269074..7b3eabb5 100644 --- a/rendering/common/exposure.h +++ b/rendering/common/exposure.h @@ -37,6 +37,8 @@ struct exposure_settings static glm::vec4 bloom_params(std::size_t const Level); // parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none) static glm::vec4 tonemap_params(std::size_t const Bloomlevels); + // white balance of the tonemapping (gfx.postfx.whitebalance*): gains of the cone responses (bradford), 0 when off + static glm::vec3 white_balance(); // controls of the debug window; Readback: the adapted luminance, the exposure and the average luminance the gpu worked out static void draw_debug_ui(float const *Readback); }; diff --git a/rendering/opengl46/opengl46exposure.cpp b/rendering/opengl46/opengl46exposure.cpp index c8ef8e4b..d2b5f347 100644 --- a/rendering/opengl46/opengl46exposure.cpp +++ b/rendering/opengl46/opengl46exposure.cpp @@ -132,6 +132,11 @@ glm::vec4 opengl46_exposure::tonemap_params(std::size_t const Bloomlevels) const return gfx::exposure_settings::tonemap_params(Bloomlevels); } +glm::vec3 opengl46_exposure::white_balance() const +{ + return gfx::exposure_settings::white_balance(); +} + std::string opengl46_exposure::info_times() { std::string text; diff --git a/rendering/opengl46/opengl46exposure.h b/rendering/opengl46/opengl46exposure.h index 68613cab..44b91a3d 100644 --- a/rendering/opengl46/opengl46exposure.h +++ b/rendering/opengl46/opengl46exposure.h @@ -60,6 +60,8 @@ class opengl46_exposure opengl_texture *bloom(bloom_chain &Chain, opengl_texture &Image, gl::stream_ubo &Modelubo, gl::model_ubs &Model); // parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none) glm::vec4 tonemap_params(std::size_t const Bloomlevels) const; + // gains of the white balance of the tonemapping (gfx.postfx.whitebalance*), 0 when off + glm::vec3 white_balance() const; // lines for the gpu times of the debug panel std::string info_times(); // controls of the debug window diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 14ff8ad3..27f846e6 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -2055,7 +2055,7 @@ if (!Global.gfx_skippipeline) } model_ubs.param[0] = m_exposure.tonemap_params(bloomlevels); // the dither goes to the last pass before the window: the chromatic aberration, when it follows - model_ubs.param[1] = glm::vec4{Global.gfx_postfx_chromaticaberration_enabled ? 0.f : 1.f, 0.f, 0.f, 0.f}; + model_ubs.param[1] = glm::vec4{Global.gfx_postfx_chromaticaberration_enabled ? 0.f : 1.f, m_exposure.white_balance()}; model_ubo->update(model_ubs); std::vector tonemapinput{vp.main2_tex.get()}; if (bloomtexture != nullptr) @@ -5864,8 +5864,9 @@ bool opengl46_renderer::Render_Preview(TAnimModel *Instance, int const Size, int glViewport(0, 0, targetsize, targetsize); if (!Global.gfx_shadergamma) glEnable(GL_FRAMEBUFFER_SRGB); - // no bloom, no automatic exposure + // no bloom, no automatic exposure, no white balance model_ubs.param[0] = glm::vec4{0.f, 0.f, 1.f, 0.f}; + model_ubs.param[1].y = 0.f; model_ubo->update(model_ubs); m_pfx_tonemapping->apply(*m_preview.hdr_tex, m_preview.ldr_fb.get()); if (!Global.gfx_shadergamma) diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index 96bf1418..c09acdf0 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -3544,7 +3544,7 @@ void vulkan14_renderer::render_post(std::size_t const Index, bool const Motionbl vk14::draw_constants tonemap; tonemap.data[0] = gfx::exposure_settings::tonemap_params(bloom != nullptr ? targets.bloom_sizes.size() : 0); // the dither goes to the last pass before the window: the chromatic aberration, when it follows - tonemap.data[1] = glm::vec4{targets.aberration ? 0.f : 1.f, 0.f, 0.f, 0.f}; + tonemap.data[1] = glm::vec4{targets.aberration ? 0.f : 1.f, gfx::exposure_settings::white_balance()}; std::vector const tonemapinput{hdr, (bloom != nullptr ? bloom : hdr)}; if (targets.aberration && targets.tonemapped) { diff --git a/shaders/ogl46/postfx_tonemapping.frag b/shaders/ogl46/postfx_tonemapping.frag index 6649ace6..5be66d24 100644 --- a/shaders/ogl46/postfx_tonemapping.frag +++ b/shaders/ogl46/postfx_tonemapping.frag @@ -28,13 +28,16 @@ layout(std430, binding = EXPOSURE_SSBO_BINDING) readonly buffer exposure_ssbo // param[0].x: share of the bloom in the image (0: no bloom, the texture is not read), param[0].y: 1 if the automatic // exposure is applied, param[0].z: fixed exposure multiplier, param[0].w: scale of the bloom texture (it holds the sum of -// the levels of the chain), param[1].x: 1 when this is the last pass before the window, which takes the dither +// the levels of the chain), param[1].x: 1 when this is the last pass before the window, which takes the dither, +// param[1].yzw: gains of the white balance (0: none) void main() { vec2 texcoord = f_coords; vec3 hdr_color = texture(tex1, texcoord).xyz; if (param[0].x > 0.0) hdr_color = mix(hdr_color, texture(bloom_tex, texcoord).rgb * param[0].w, param[0].x); + if (param[1].y > 0.0) + hdr_color = white_balance(hdr_color, param[1].yzw); float exposurescale = param[0].z; if (param[0].y > 0.5 && exposure > 0.0) exposurescale *= exposure; diff --git a/shaders/ogl46/tonemapping.glsl b/shaders/ogl46/tonemapping.glsl index b2b8ae1a..4dd64845 100644 --- a/shaders/ogl46/tonemapping.glsl +++ b/shaders/ogl46/tonemapping.glsl @@ -139,6 +139,22 @@ vec3 ApplyAgX(vec3 linearColorRec709) return linearColorRec709; } +// white balance of the camera (von kries in the bradford cone space). Gains: ratios of the cone responses of the white of +// the display to those of the light taken as white (gfx::exposure_settings::white_balance()) +vec3 white_balance(vec3 rgb, vec3 gains) +{ + // linear rec.709 -> xyz -> bradford, and back + const mat3 rgb_to_lms = mat3( + 0.42272529, 0.05569978, 0.02138264, + 0.49134532, 0.96153405, 0.08764187, + 0.02735794, 0.02318381, 0.98050813); + const mat3 lms_to_rgb = mat3( + 2.53804452, -0.14600406, -0.04229851, + -1.29327700, 1.11664831, -0.07160722, + -0.04023688, -0.02232903, 1.02275269); + return lms_to_rgb * (gains * (rgb_to_lms * rgb)); +} + vec4 tonemap(vec4 x) { // Use AgX by default. Reinhard and ACES above are kept for reference. diff --git a/shaders/vk14/postfx_tonemapping.frag b/shaders/vk14/postfx_tonemapping.frag index deef2f56..cb1a1284 100644 --- a/shaders/vk14/postfx_tonemapping.frag +++ b/shaders/vk14/postfx_tonemapping.frag @@ -21,13 +21,15 @@ layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 1) uniform sampler2D bloom // param[0].x: share of the bloom in the image (0: no bloom, the texture is not read), param[0].y: 1 if the automatic // exposure is applied (exposure_ssbo of common.glsl, worked out by exposure_average.comp), param[0].z: fixed exposure multiplier, param[0].w: scale of // the bloom texture (it holds the sum of the levels of the chain), param[1].x: 1 when this is the last pass before the -// window, which takes the dither +// window, which takes the dither, param[1].yzw: gains of the white balance (0: none) void main() { vec2 texcoord = f_coords; vec3 hdr_color = texture(tex1, texcoord).xyz; if (param[0].x > 0.0) hdr_color = mix(hdr_color, texture(bloom_tex, texcoord).rgb * param[0].w, param[0].x); + if (param[1].y > 0.0) + hdr_color = white_balance(hdr_color, param[1].yzw); float exposurescale = param[0].z; if (param[0].y > 0.5 && exposure > 0.0) exposurescale *= exposure; diff --git a/shaders/vk14/tonemapping.glsl b/shaders/vk14/tonemapping.glsl index 7f8e10e4..cee58e5d 100644 --- a/shaders/vk14/tonemapping.glsl +++ b/shaders/vk14/tonemapping.glsl @@ -139,6 +139,22 @@ vec3 ApplyAgX(vec3 linearColorRec709) return linearColorRec709; } +// white balance of the camera (von kries in the bradford cone space). Gains: ratios of the cone responses of the white of +// the display to those of the light taken as white (gfx::exposure_settings::white_balance()) +vec3 white_balance(vec3 rgb, vec3 gains) +{ + // linear rec.709 -> xyz -> bradford, and back + const mat3 rgb_to_lms = mat3( + 0.42272529, 0.05569978, 0.02138264, + 0.49134532, 0.96153405, 0.08764187, + 0.02735794, 0.02318381, 0.98050813); + const mat3 lms_to_rgb = mat3( + 2.53804452, -0.14600406, -0.04229851, + -1.29327700, 1.11664831, -0.07160722, + -0.04023688, -0.02232903, 1.02275269); + return lms_to_rgb * (gains * (rgb_to_lms * rgb)); +} + vec4 tonemap(vec4 x) { // Use AgX by default. Reinhard and ACES above are kept for reference. diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index b8cbee03..0958959c 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1875,6 +1875,9 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To else if( Token == "gfx.postfx.bloom" ) { readswitch( gfx_postfx_bloom ); } else if( Token == "gfx.postfx.bloom.intensity" ) { readvalue( gfx_postfx_bloom_intensity, 0.f, 1.f ); } else if( Token == "gfx.postfx.bloom.threshold" ) { readvalue( gfx_postfx_bloom_threshold, 0.f, 1000.f ); } + else if( Token == "gfx.postfx.whitebalance" ) { readswitch( gfx_postfx_whitebalance ); } + else if( Token == "gfx.postfx.whitebalance.temperature" ) { readvalue( gfx_postfx_whitebalance_temperature, 4000.f, 25000.f ); } + else if( Token == "gfx.postfx.whitebalance.tint" ) { readvalue( gfx_postfx_whitebalance_tint, -1.f, 1.f ); } else if( Token == "gfx.postfx.ao" ) { // yes/gtao: ground truth ambient occlusion, ssao: the older one, anything else: off Parser.getTokens( 1 ); @@ -2353,6 +2356,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.postfx.bloom", gfx_postfx_bloom ); export_as_text( Output, "gfx.postfx.bloom.intensity", gfx_postfx_bloom_intensity ); export_as_text( Output, "gfx.postfx.bloom.threshold", gfx_postfx_bloom_threshold ); + export_as_text( Output, "gfx.postfx.whitebalance", gfx_postfx_whitebalance ); + export_as_text( Output, "gfx.postfx.whitebalance.temperature", gfx_postfx_whitebalance_temperature ); + export_as_text( Output, "gfx.postfx.whitebalance.tint", gfx_postfx_whitebalance_tint ); export_as_text( Output, "gfx.postfx.ao", std::string{ gfx_postfx_ao == 2 ? "gtao" : gfx_postfx_ao == 1 ? "ssao" : "no" } ); export_as_text( Output, "gfx.postfx.ao.radius", gfx_postfx_ao_radius ); export_as_text( Output, "gfx.postfx.ao.strength", gfx_postfx_ao_strength ); diff --git a/utilities/Globals.h b/utilities/Globals.h index a51c2cc4..694e62a1 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -461,6 +461,9 @@ struct global_settings { bool gfx_postfx_bloom { true }; // glow around bright parts of the image float gfx_postfx_bloom_intensity { 0.04f }; // share of the blurred image in the result float gfx_postfx_bloom_threshold { 0.f }; // luminance below which nothing glows, 0 = everything contributes + bool gfx_postfx_whitebalance { true }; // white balance of the camera, applied to the image before the tonemapping + float gfx_postfx_whitebalance_temperature { 7500.f }; // colour temperature of the light taken as white, K; 6500 = none, more = warmer image + float gfx_postfx_whitebalance_tint { 0.f }; // -1..1, shift off the daylight locus: + magenta image, - green image // screen space ambient occlusion of the opengl 4.6 renderer, applied to the image (debug window "Sky and exposure"). // 0 off, 1 the older ssao, 2 ground truth ambient occlusion; when 0 at the start the targets are not made at all int gfx_postfx_ao { 2 }; @@ -617,7 +620,7 @@ struct global_settings { bool ConfigParseHardware(cParser& Parser, const std::string& token); bool ConfigParseDebug(cParser& Parser, const std::string& token); bool ConfigParse_gfx( cParser &parser, std::string_view const Token ); - // sky light and exposure switches of the opengl 4.6 renderer (gfx.ambient.*, gfx.envmap.ggx, gfx.atmosphere*, gfx.postfx.autoexposure*, gfx.postfx.bloom*, gfx.postfx.ao*) + // sky light and exposure switches of the opengl 4.6 renderer (gfx.ambient.*, gfx.envmap.ggx, gfx.atmosphere*, gfx.postfx.autoexposure*, gfx.postfx.bloom*, gfx.postfx.whitebalance*, gfx.postfx.ao*) bool ConfigParse_gfx_sky( cParser &parser, std::string_view const Token ); // sends basic content of the class in legacy (text) format to provided stream void