Realtime Compositor: Implement Tone Map node

This patch implements the tone map node for the realtime compositor
based on the two papers:

Reinhard, Erik, et al. "Photographic tone reproduction for digital
images." Proceedings of the 29th annual conference on Computer graphics
and interactive techniques. 2002.

Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by
photoreceptor physiology." IEEE transactions on visualization and
computer graphics 11.1 (2005): 13-24.

The original implementation should be revisited later due to apparent
incompatibilities with the reference papers, which makes the operation
less useful.

Differential Revision: https://developer.blender.org/D16306

Reviewed By: Clement Foucault
This commit is contained in:
Omar Emara 2022-10-20 15:02:41 +02:00
parent fd7a3e2622
commit 7f2cd2d969
10 changed files with 475 additions and 6 deletions

View File

@ -27,6 +27,13 @@ float sum_blue(Context &context, GPUTexture *texture);
* coefficients to compute the luminance. */
float sum_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients);
/* Computes the sum of the logarithm of the luminance of all pixels in the given texture, using the
* given luminance coefficients to compute the luminance. */
float sum_log_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients);
/* Computes the sum of the colors of all pixels in the given texture. */
float4 sum_color(Context &context, GPUTexture *texture);
/* --------------------------------------------------------------------
* Sum Of Squared Difference Reductions.
*/
@ -55,4 +62,20 @@ float sum_luminance_squared_difference(Context &context,
float3 luminance_coefficients,
float subtrahend);
/* --------------------------------------------------------------------
* Maximum Reductions.
*/
/* Computes the maximum luminance of all pixels in the given texture, using the given luminance
* coefficients to compute the luminance. */
float maximum_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients);
/* --------------------------------------------------------------------
* Minimum Reductions.
*/
/* Computes the minimum luminance of all pixels in the given texture, using the given luminance
* coefficients to compute the luminance. */
float minimum_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients);
} // namespace blender::realtime_compositor

View File

@ -134,6 +134,34 @@ float sum_luminance(Context &context, GPUTexture *texture, float3 luminance_coef
return sum;
}
float sum_log_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients)
{
GPUShader *shader = context.shader_manager().get("compositor_sum_log_luminance");
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(context, texture, shader, GPU_R32F);
const float sum = *reduced_value;
MEM_freeN(reduced_value);
GPU_shader_unbind();
return sum;
}
float4 sum_color(Context &context, GPUTexture *texture)
{
GPUShader *shader = context.shader_manager().get("compositor_sum_color");
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(context, texture, shader, GPU_RGBA32F);
const float4 sum = float4(reduced_value);
MEM_freeN(reduced_value);
GPU_shader_unbind();
return sum;
}
/* --------------------------------------------------------------------
* Sum Of Squared Difference Reductions.
*/
@ -202,4 +230,42 @@ float sum_luminance_squared_difference(Context &context,
return sum;
}
/* --------------------------------------------------------------------
* Maximum Reductions.
*/
float maximum_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients)
{
GPUShader *shader = context.shader_manager().get("compositor_maximum_luminance");
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(context, texture, shader, GPU_R32F);
const float maximum = *reduced_value;
MEM_freeN(reduced_value);
GPU_shader_unbind();
return maximum;
}
/* --------------------------------------------------------------------
* Minimum Reductions.
*/
float minimum_luminance(Context &context, GPUTexture *texture, float3 luminance_coefficients)
{
GPUShader *shader = context.shader_manager().get("compositor_minimum_luminance");
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(context, texture, shader, GPU_R32F);
const float minimum = *reduced_value;
MEM_freeN(reduced_value);
GPU_shader_unbind();
return minimum;
}
} // namespace blender::realtime_compositor

View File

@ -357,6 +357,8 @@ set(GLSL_SRC
shaders/compositor/compositor_split_viewer.glsl
shaders/compositor/compositor_symmetric_blur.glsl
shaders/compositor/compositor_symmetric_separable_blur.glsl
shaders/compositor/compositor_tone_map_photoreceptor.glsl
shaders/compositor/compositor_tone_map_simple.glsl
shaders/compositor/library/gpu_shader_compositor_alpha_over.glsl
shaders/compositor/library/gpu_shader_compositor_blur_common.glsl
@ -639,6 +641,8 @@ set(SRC_SHADER_CREATE_INFOS
shaders/compositor/infos/compositor_split_viewer_info.hh
shaders/compositor/infos/compositor_symmetric_blur_info.hh
shaders/compositor/infos/compositor_symmetric_separable_blur_info.hh
shaders/compositor/infos/compositor_tone_map_photoreceptor_info.hh
shaders/compositor/infos/compositor_tone_map_simple_info.hh
)
set(SRC_SHADER_CREATE_INFOS_MTL

View File

@ -0,0 +1,22 @@
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
/* Tone mapping based on equation (1) and the trilinear interpolation between equations (6) and (7)
* from Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by photoreceptor
* physiology." IEEE transactions on visualization and computer graphics 11.1 (2005): 13-24. */
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
vec4 input_color = texture_load(input_tx, texel);
float input_luminance = dot(input_color.rgb, luminance_coefficients);
/* Trilinear interpolation between equations (6) and (7) from Reinhard's 2005 paper. */
vec4 local_adaptation_level = mix(vec4(input_luminance), input_color, chromatic_adaptation);
vec4 adaptation_level = mix(global_adaptation_level, local_adaptation_level, light_adaptation);
/* Equation (1) from Reinhard's 2005 paper, assuming Vmax is 1. */
vec4 semi_saturation = pow(intensity * adaptation_level, vec4(contrast));
vec4 tone_mapped_color = input_color / (input_color + semi_saturation);
imageStore(output_img, texel, vec4(tone_mapped_color.rgb, input_color.a));
}

View File

@ -0,0 +1,26 @@
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_common_math_utils.glsl)
/* Tone mapping based on equation (3) from Reinhard, Erik, et al. "Photographic tone reproduction
* for digital images." Proceedings of the 29th annual conference on Computer graphics and
* interactive techniques. 2002. */
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
vec4 input_color = texture_load(input_tx, texel);
/* Equation (2) from Reinhard's 2002 paper. */
vec4 scaled_color = input_color * luminance_scale;
/* Equation (3) from Reinhard's 2002 paper, but with the 1 replaced with the blend factor for
* more flexibility. See ToneMapOperation::compute_luminance_scale_blend_factor. */
vec4 denominator = luminance_scale_blend_factor + scaled_color;
vec4 tone_mapped_color = safe_divide(scaled_color, denominator);
if (inverse_gamma != 0.0) {
tone_mapped_color = pow(max(tone_mapped_color, vec4(0.0)), vec4(inverse_gamma));
}
imageStore(output_img, texel, vec4(tone_mapped_color.rgb, input_color.a));
}

View File

@ -12,13 +12,16 @@ GPU_SHADER_CREATE_INFO(compositor_parallel_reduction_shared)
* Sum Reductions.
*/
GPU_SHADER_CREATE_INFO(compositor_sum_float_shared)
GPU_SHADER_CREATE_INFO(compositor_sum_shared)
.additional_info("compositor_parallel_reduction_shared")
.define("IDENTITY", "vec4(0.0)")
.define("REDUCE(lhs, rhs)", "lhs + rhs");
GPU_SHADER_CREATE_INFO(compositor_sum_float_shared)
.additional_info("compositor_sum_shared")
.image(0, GPU_R32F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.define("TYPE", "float")
.define("IDENTITY", "vec4(0.0)")
.define("LOAD(value)", "value.x")
.define("REDUCE(lhs, rhs)", "lhs + rhs");
.define("LOAD(value)", "value.x");
GPU_SHADER_CREATE_INFO(compositor_sum_red)
.additional_info("compositor_sum_float_shared")
@ -41,6 +44,20 @@ GPU_SHADER_CREATE_INFO(compositor_sum_luminance)
.define("INITIALIZE(value)", "dot(value.rgb, luminance_coefficients)")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_sum_log_luminance)
.additional_info("compositor_sum_float_shared")
.push_constant(Type::VEC3, "luminance_coefficients")
.define("INITIALIZE(value)", "log(max(dot(value.rgb, luminance_coefficients), 1e-5))")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_sum_color)
.additional_info("compositor_sum_shared")
.image(0, GPU_RGBA32F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.define("TYPE", "vec4")
.define("INITIALIZE(value)", "value")
.define("LOAD(value)", "value")
.do_static_compilation(true);
/* --------------------------------------------------------------------
* Sum Of Squared Difference Reductions.
*/
@ -74,3 +91,35 @@ GPU_SHADER_CREATE_INFO(compositor_sum_luminance_squared_difference)
.push_constant(Type::VEC3, "luminance_coefficients")
.define("INITIALIZE(value)", "pow(dot(value.rgb, luminance_coefficients) - subtrahend, 2.0)")
.do_static_compilation(true);
/* --------------------------------------------------------------------
* Maximum Reductions.
*/
GPU_SHADER_CREATE_INFO(compositor_maximum_luminance)
.additional_info("compositor_parallel_reduction_shared")
.typedef_source("common_math_lib.glsl")
.image(0, GPU_R32F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.push_constant(Type::VEC3, "luminance_coefficients")
.define("TYPE", "float")
.define("IDENTITY", "vec4(FLT_MIN)")
.define("INITIALIZE(value)", "dot(value.rgb, luminance_coefficients)")
.define("LOAD(value)", "value.x")
.define("REDUCE(lhs, rhs)", "max(lhs, rhs)")
.do_static_compilation(true);
/* --------------------------------------------------------------------
* Minimum Reductions.
*/
GPU_SHADER_CREATE_INFO(compositor_minimum_luminance)
.additional_info("compositor_parallel_reduction_shared")
.typedef_source("common_math_lib.glsl")
.image(0, GPU_R32F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.push_constant(Type::VEC3, "luminance_coefficients")
.define("TYPE", "float")
.define("IDENTITY", "vec4(FLT_MAX)")
.define("INITIALIZE(value)", "dot(value.rgb, luminance_coefficients)")
.define("LOAD(value)", "value.x")
.define("REDUCE(lhs, rhs)", "min(lhs, rhs)")
.do_static_compilation(true);

View File

@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_tone_map_photoreceptor)
.local_group_size(16, 16)
.push_constant(Type::VEC4, "global_adaptation_level")
.push_constant(Type::FLOAT, "contrast")
.push_constant(Type::FLOAT, "intensity")
.push_constant(Type::FLOAT, "chromatic_adaptation")
.push_constant(Type::FLOAT, "light_adaptation")
.push_constant(Type::VEC3, "luminance_coefficients")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_tone_map_photoreceptor.glsl")
.do_static_compilation(true);

View File

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_tone_map_simple)
.local_group_size(16, 16)
.push_constant(Type::FLOAT, "luminance_scale")
.push_constant(Type::FLOAT, "luminance_scale_blend_factor")
.push_constant(Type::FLOAT, "inverse_gamma")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_tone_map_simple.glsl")
.do_static_compilation(true);

View File

@ -2083,6 +2083,12 @@ typedef enum CMPNodeLevelsChannel {
CMP_NODE_LEVLES_LUMINANCE_BT709 = 5,
} CMPNodeLevelsChannel;
/* Tone Map Node. Stored in NodeTonemap.type. */
typedef enum CMPNodeToneMapType {
CMP_NODE_TONE_MAP_SIMPLE = 0,
CMP_NODE_TONE_MAP_PHOTORECEPTOR = 1,
} CMPNodeToneMapType;
/* Plane track deform node. */
enum {

View File

@ -5,20 +5,35 @@
* \ingroup cmpnodes
*/
#include <cmath>
#include "BLI_assert.h"
#include "BLI_math_base.hh"
#include "BLI_math_vec_types.hh"
#include "BLI_math_vector.hh"
#include "RNA_access.h"
#include "UI_interface.h"
#include "UI_resources.h"
#include "IMB_colormanagement.h"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "node_composite_util.hh"
namespace blender::nodes::node_composite_tonemap_cc {
NODE_STORAGE_FUNCS(NodeTonemap)
static void cmp_node_tonemap_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>(N_("Image")).default_value({1.0f, 1.0f, 1.0f, 1.0f});
b.add_input<decl::Color>(N_("Image"))
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.compositor_domain_priority(0);
b.add_output<decl::Color>(N_("Image"));
}
@ -68,7 +83,236 @@ class ToneMapOperation : public NodeOperation {
void execute() override
{
get_input("Image").pass_through(get_result("Image"));
Result &input_image = get_input("Image");
Result &output_image = get_result("Image");
if (input_image.is_single_value()) {
input_image.pass_through(output_image);
return;
}
switch (get_type()) {
case CMP_NODE_TONE_MAP_SIMPLE:
execute_simple();
return;
case CMP_NODE_TONE_MAP_PHOTORECEPTOR:
execute_photoreceptor();
return;
default:
BLI_assert_unreachable();
return;
}
}
/* Tone mapping based on equation (3) from Reinhard, Erik, et al. "Photographic tone reproduction
* for digital images." Proceedings of the 29th annual conference on Computer graphics and
* interactive techniques. 2002. */
void execute_simple()
{
const float luminance_scale = compute_luminance_scale();
const float luminance_scale_blend_factor = compute_luminance_scale_blend_factor();
const float gamma = node_storage(bnode()).gamma;
const float inverse_gamma = gamma != 0.0f ? 1.0f / gamma : 0.0f;
GPUShader *shader = shader_manager().get("compositor_tone_map_simple");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "luminance_scale", luminance_scale);
GPU_shader_uniform_1f(shader, "luminance_scale_blend_factor", luminance_scale_blend_factor);
GPU_shader_uniform_1f(shader, "inverse_gamma", inverse_gamma);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
/* Computes the scaling factor in equation (2) from Reinhard's 2002 paper. */
float compute_luminance_scale()
{
const float geometric_mean = compute_geometric_mean_of_luminance();
return geometric_mean != 0.0 ? node_storage(bnode()).key / geometric_mean : 0.0f;
}
/* Computes equation (1) from Reinhard's 2002 paper. However, note that the equation in the paper
* is most likely wrong, and the intention is actually to compute the geometric mean through a
* logscale arithmetic mean, that is, the division should happen inside the exponential function,
* not outside of it. That's because the sum of the log luminance will be a very large negative
* number, whose exponential will almost always be zero, which is unexpected and useless. */
float compute_geometric_mean_of_luminance()
{
return std::exp(compute_average_log_luminance());
}
/* Equation (3) from Reinhard's 2002 paper blends between high luminance scaling for high
* luminance values and low luminance scaling for low luminance values. This is done by adding 1
* to the denominator, since for low luminance values, the denominator will be close to 1 and for
* high luminance values, the 1 in the denominator will be relatively insignificant. But the
* response of such function is not always ideal, so in this implementation, the 1 was exposed as
* a parameter to the user for more flexibility. */
float compute_luminance_scale_blend_factor()
{
return node_storage(bnode()).offset;
}
/* Tone mapping based on equation (1) and the trilinear interpolation between equations (6) and
* (7) from Reinhard, Erik, and Kate Devlin. "Dynamic range reduction inspired by photoreceptor
* physiology." IEEE transactions on visualization and computer graphics 11.1 (2005): 13-24. */
void execute_photoreceptor()
{
const float4 global_adaptation_level = compute_global_adaptation_level();
const float contrast = compute_contrast();
const float intensity = compute_intensity();
const float chromatic_adaptation = get_chromatic_adaptation();
const float light_adaptation = get_light_adaptation();
GPUShader *shader = shader_manager().get("compositor_tone_map_photoreceptor");
GPU_shader_bind(shader);
GPU_shader_uniform_4fv(shader, "global_adaptation_level", global_adaptation_level);
GPU_shader_uniform_1f(shader, "contrast", contrast);
GPU_shader_uniform_1f(shader, "intensity", intensity);
GPU_shader_uniform_1f(shader, "chromatic_adaptation", chromatic_adaptation);
GPU_shader_uniform_1f(shader, "light_adaptation", light_adaptation);
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
output_image.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.size);
GPU_shader_unbind();
output_image.unbind_as_image();
input_image.unbind_as_texture();
}
/* Computes the global adaptation level from the trilinear interpolation equations constructed
* from equations (6) and (7) in Reinhard's 2005 paper. */
float4 compute_global_adaptation_level()
{
const float4 average_color = compute_average_color();
const float average_luminance = compute_average_luminance();
const float chromatic_adaptation = get_chromatic_adaptation();
return math::interpolate(float4(average_luminance), average_color, chromatic_adaptation);
}
float4 compute_average_color()
{
/* The average color will reduce to zero if chromatic adaptation is zero, so just return zero
* in this case to avoid needlessly computing the average. See the trilinear interpolation
* equations constructed from equations (6) and (7) in Reinhard's 2005 paper. */
if (get_chromatic_adaptation() == 0.0f) {
return float4(0.0f);
}
const Result &input = get_input("Image");
return sum_color(context(), input.texture()) / (input.domain().size.x * input.domain().size.y);
}
float compute_average_luminance()
{
/* The average luminance will reduce to zero if chromatic adaptation is one, so just return
* zero in this case to avoid needlessly computing the average. See the trilinear interpolation
* equations constructed from equations (6) and (7) in Reinhard's 2005 paper. */
if (get_chromatic_adaptation() == 1.0f) {
return 0.0f;
}
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const Result &input = get_input("Image");
float sum = sum_luminance(context(), input.texture(), luminance_coefficients);
return sum / (input.domain().size.x * input.domain().size.y);
}
/* Computes equation (5) from Reinhard's 2005 paper. */
float compute_intensity()
{
return std::exp(-node_storage(bnode()).f);
}
/* If the contrast is not zero, return it, otherwise, a zero contrast denote automatic derivation
* of the contrast value based on equations (2) and (4) from Reinhard's 2005 paper. */
float compute_contrast()
{
if (node_storage(bnode()).m != 0.0f) {
return node_storage(bnode()).m;
}
const float log_maximum_luminance = compute_log_maximum_luminance();
const float log_minimum_luminance = compute_log_minimum_luminance();
/* This is merely to guard against zero division later. */
if (log_maximum_luminance == log_minimum_luminance) {
return 1.0f;
}
const float average_log_luminance = compute_average_log_luminance();
const float dynamic_range = log_maximum_luminance - log_minimum_luminance;
const float luminance_key = (log_maximum_luminance - average_log_luminance) / (dynamic_range);
return 0.3f + 0.7f * std::pow(luminance_key, 1.4f);
}
float compute_average_log_luminance()
{
const Result &input_image = get_input("Image");
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float sum_of_log_luminance = sum_log_luminance(
context(), input_image.texture(), luminance_coefficients);
return sum_of_log_luminance / (input_image.domain().size.x * input_image.domain().size.y);
}
float compute_log_maximum_luminance()
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float maximum = maximum_luminance(
context(), get_input("Image").texture(), luminance_coefficients);
return std::log(math::max(maximum, 1e-5f));
}
float compute_log_minimum_luminance()
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
const float minimum = minimum_luminance(
context(), get_input("Image").texture(), luminance_coefficients);
return std::log(math::max(minimum, 1e-5f));
}
float get_chromatic_adaptation()
{
return node_storage(bnode()).c;
}
float get_light_adaptation()
{
return node_storage(bnode()).a;
}
CMPNodeToneMapType get_type()
{
return static_cast<CMPNodeToneMapType>(node_storage(bnode()).type);
}
};