Compositor: Add Glare Highlights Smoothness and Max

This patch adds two new inputs to the Glare node, Highlights Smoothness
and Max Highlights. Smoothness allows the user to control how smooth the
highlights are after thresholding and Max allows the user to suppress
very high brightness pixels.

Those are essentially similar to the Knee and Clamp options in old EEVEE
bloom, though they work differently.

The issue with the Knee parameter in old EEVEE bloom, aside from being
named after a body part, is that it actually isn't smooth or continuous
around zero if the threshold is sufficiently close to zero relative to
the Knee parameter. That's because zero lies in the smoothing kernel
region in those cases, and since zero pixels becoming highlights is very
bad, EEVEE just returned zero as a special case for zero brightness, but
values like 0.0001 will be full blown highlights.

The new nicely named Smoothness input uses adaptive smoothing such that
the smoothing kernel size will be reduced as the threshold nears zero,
such that smoothed highlights will be continuous and smooth around zero.

The Max Highlights input is similar to clamped, it it suppresses very
bright highlights such that their brightness doesn't exceed the
specified max.

This is a partial implementation of #124176 to address #131325.

Pull Request: https://projects.blender.org/blender/blender/pulls/132864
This commit is contained in:
Omar Emara
2025-01-13 13:54:07 +01:00
committed by Omar Emara
parent 2fb3d283ef
commit 3b28cf276e
6 changed files with 216 additions and 24 deletions

View File

@@ -31,7 +31,7 @@ extern "C" {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 19
#define BLENDER_FILE_SUBVERSION 20
/* 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

View File

@@ -1377,6 +1377,11 @@ void do_versions_after_linking_400(FileData *fd, Main *bmain)
version_node_socket_index_animdata(bmain, NTREE_COMPOSIT, CMP_NODE_GLARE, 3, 2, 11);
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 20)) {
/* Two new inputs were added, Highlights Smoothness and Highlights suppression. */
version_node_socket_index_animdata(bmain, NTREE_COMPOSIT, CMP_NODE_GLARE, 2, 2, 13);
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View File

@@ -4,24 +4,85 @@
#include "gpu_shader_common_color_utils.glsl"
/* A Quadratic Polynomial smooth minimum function *without* normalization, based on:
*
* https://iquilezles.org/articles/smin/
*
* This should not be converted into a common utility function because the glare code is
* specifically designed for it as can be seen in the adaptive_smooth_clamp method, and it is
* intentionally not normalized. */
float smooth_min(float a, float b, float smoothness)
{
if (smoothness == 0.0) {
return min(a, b);
}
float h = max(smoothness - abs(a - b), 0.0) / smoothness;
return min(a, b) - h * h * smoothness * (1.0 / 4.0);
}
float smooth_max(float a, float b, float smoothness)
{
return -smooth_min(-a, -b, smoothness);
}
/* Clamps the input x within min_value and max_value using a quadratic polynomial smooth minimum
* and maximum functions, with individual control over their smoothness. */
float smooth_clamp(
float x, float min_value, float max_value, float min_smoothness, float max_smoothness)
{
return smooth_min(max_value, smooth_max(min_value, x, min_smoothness), max_smoothness);
}
/* A variant of smooth_clamp that limits the smoothness such that the function evaluates to the
* given min for 0 <= min <= max and x >= 0. The aforementioned guarantee holds for the standard
* clamp function by definition, but since the smooth clamp function gradually increases before
* the specified min/max, if min/max are sufficiently close together or to zero, they will not
* evaluate to min at zero or at min, since zero or min will be at the region of the gradual
* increase.
*
* It can be shown that the width of the gradual increase region is equivalent to the smoothness
* parameter, so smoothness can't be larger than the difference between the min/max and zero, or
* larger than the difference between min and max themselves. Otherwise, zero or min will lie
* inside the gradual increase region of min/max. So we limit the smoothness of min/max by taking
* the minimum with the distances to zero and to the distance to the other bound. */
float adaptive_smooth_clamp(float x, float min_value, float max_value, float smoothness)
{
float range_distance = distance(min_value, max_value);
float distance_from_min_to_zero = distance(min_value, 0.0);
float distance_from_max_to_zero = distance(max_value, 0.0);
float max_safe_smoothness_for_min = min(distance_from_min_to_zero, range_distance);
float max_safe_smoothness_for_max = min(distance_from_max_to_zero, range_distance);
float min_smoothness = min(smoothness, max_safe_smoothness_for_min);
float max_smoothness = min(smoothness, max_safe_smoothness_for_max);
return smooth_clamp(x, min_value, max_value, min_smoothness, max_smoothness);
}
void main()
{
/* The dispatch domain covers the output image size, which might be a fraction of the input image
* size, so you will notice the output image size used throughout the shader instead of the input
* one. */
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
/* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image size
* to get the coordinates into the sampler's expected [0, 1] range. */
vec2 normalized_coordinates = (vec2(texel) + vec2(0.5)) / vec2(imageSize(output_img));
vec4 hsva;
rgb_to_hsv(texture(input_tx, normalized_coordinates), hsva);
/* The pixel whose luminance value is less than the threshold luminance is not considered part of
* the highlights and is given a value of zero. Otherwise, the pixel is considered part of the
* highlights, whose luminance value is the difference to the threshold. */
hsva.z = max(0.0, hsva.z - threshold);
/* Clamp the brightness of the highlights such that pixels whose brightness are less than the
* threshold will be equal to the threshold and will become zero once threshold is subtracted
* later. We also clamp by the specified max brightness to suppress very bright highlights.
*
* We use a smooth clamping function such that highlights do not become very sharp but use
* the adaptive variant such that we guarantee that zero highlights remain zero even after
* smoothing. Notice that when we mention zero, we mean zero after subtracting the threshold,
* so we actually mean the minimum bound, the threshold. See the adaptive_smooth_clamp
* function for more information. */
float clamped_brightness = adaptive_smooth_clamp(
hsva.z, threshold, max_brightness, highlights_smoothness);
/* The final brightness is relative to the threshold. */
hsva.z = clamped_brightness - threshold;
vec4 rgba;
hsv_to_rgb(hsva, rgba);

View File

@@ -11,6 +11,8 @@
GPU_SHADER_CREATE_INFO(compositor_glare_highlights)
LOCAL_GROUP_SIZE(16, 16)
PUSH_CONSTANT(FLOAT, threshold)
PUSH_CONSTANT(FLOAT, highlights_smoothness)
PUSH_CONSTANT(FLOAT, max_brightness)
SAMPLER(0, FLOAT_2D, input_tx)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
COMPUTE_SOURCE("compositor_glare_highlights.glsl")

View File

@@ -126,6 +126,18 @@ DEFINE_VALUE("REDUCE(lhs, rhs)", "max(lhs, rhs)")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_maximum_brightness)
ADDITIONAL_INFO(compositor_parallel_reduction_shared)
TYPEDEF_SOURCE("common_math_lib.glsl")
IMAGE(0, GPU_R32F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("TYPE", "float")
DEFINE_VALUE("IDENTITY", "FLT_MIN")
DEFINE_VALUE("INITIALIZE(value)", "max_v3(value.rgb)")
DEFINE_VALUE("LOAD(value)", "value.x")
DEFINE_VALUE("REDUCE(lhs, rhs)", "max(lhs, rhs)")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_maximum_float)
ADDITIONAL_INFO(compositor_parallel_reduction_shared)
TYPEDEF_SOURCE("common_math_lib.glsl")

View File

@@ -9,6 +9,7 @@
#include <array>
#include <cmath>
#include <complex>
#include <limits>
#include <memory>
#include "MEM_guardedalloc.h"
@@ -64,6 +65,20 @@ static void cmp_node_glare_declare(NodeDeclarationBuilder &b)
"Defines the luminance at which pixels start to be considered part of the highlights "
"that will produce a glare")
.compositor_expects_single_value();
b.add_input<decl::Float>("Smoothness", "Highlights Smoothness")
.default_value(0.1f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR)
.description("The smoothness of the extracted highlights")
.compositor_expects_single_value();
b.add_input<decl::Float>("Maximum", "Maximum Highlights")
.default_value(0.0f)
.min(0.0f)
.description(
"Suppresses the highlights such that their brightness are not larger than this value. "
"Zero disables suppression and has no effect")
.compositor_expects_single_value();
b.add_input<decl::Float>("Strength")
.default_value(1.0f)
.min(0.0f)
@@ -259,6 +274,8 @@ class GlareOperation : public NodeOperation {
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "threshold", this->get_threshold());
GPU_shader_uniform_1f(shader, "highlights_smoothness", this->get_highlights_smoothness());
GPU_shader_uniform_1f(shader, "max_brightness", this->get_maximum_brightness());
const Result &input_image = get_input("Image");
GPU_texture_filter_mode(input_image, true);
@@ -281,6 +298,8 @@ class GlareOperation : public NodeOperation {
Result execute_highlights_cpu()
{
const float threshold = this->get_threshold();
const float highlights_smoothness = this->get_highlights_smoothness();
const float max_brightness = this->get_maximum_brightness();
const Result &input = get_input("Image");
@@ -288,21 +307,26 @@ class GlareOperation : public NodeOperation {
Result output = context().create_result(ResultType::Color);
output.allocate_texture(highlights_size);
/* The dispatch domain covers the output image size, which might be a fraction of the input
* image size, so you will notice the glare size used throughout the code instead of the input
* one. */
parallel_for(highlights_size, [&](const int2 texel) {
/* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image
* size to get the coordinates into the sampler's expected [0, 1] range. */
float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(highlights_size);
float4 hsva;
rgb_to_hsv_v(input.sample_bilinear_extended(normalized_coordinates), hsva);
/* The pixel whose luminance value is less than the threshold luminance is not considered
* part of the highlights and is given a value of zero. Otherwise, the pixel is considered
* part of the highlights, whose luminance value is the difference to the threshold. */
hsva.z = math::max(0.0f, hsva.z - threshold);
/* Clamp the brightness of the highlights such that pixels whose brightness are less than the
* threshold will be equal to the threshold and will become zero once threshold is subtracted
* later. We also clamp by the specified max brightness to suppress very bright highlights.
*
* We use a smooth clamping function such that highlights do not become very sharp but use
* the adaptive variant such that we guarantee that zero highlights remain zero even after
* smoothing. Notice that when we mention zero, we mean zero after subtracting the threshold,
* so we actually mean the minimum bound, the threshold. See the adaptive_smooth_clamp
* function for more information. */
const float clamped_brightness = this->adaptive_smooth_clamp(
hsva.z, threshold, max_brightness, highlights_smoothness);
/* The final brightness is relative to the threshold. */
hsva.z = clamped_brightness - threshold;
float4 rgba;
hsv_to_rgb_v(hsva, rgba);
@@ -313,6 +337,99 @@ class GlareOperation : public NodeOperation {
return output;
}
float get_maximum_brightness()
{
const float max_highlights = this->get_max_highlights();
/* Disabled when zero. Return the maximum possible brightness. */
if (max_highlights == 0.0f) {
return std::numeric_limits<float>::max();
}
/* Brightness of the highlights are relative to the threshold, see execute_highlights_cpu, so
* we add the threshold such that the maximum brightness corresponds to the actual brightness
* of the computed highlights. */
return this->get_threshold() + max_highlights;
}
/* A Quadratic Polynomial smooth minimum function *without* normalization, based on:
*
* https://iquilezles.org/articles/smin/
*
* This should not be converted into a common utility function in BLI because the glare code is
* specifically designed for it as can be seen in the adaptive_smooth_clamp method, and it is
* intentionally not normalized. */
float smooth_min(const float a, const float b, const float smoothness)
{
if (smoothness == 0.0f) {
return math::min(a, b);
}
const float h = math::max(smoothness - math::abs(a - b), 0.0f) / smoothness;
return math::min(a, b) - h * h * smoothness * (1.0f / 4.0f);
}
float smooth_max(const float a, const float b, const float smoothness)
{
return -this->smooth_min(-a, -b, smoothness);
}
/* Clamps the input x within min_value and max_value using a quadratic polynomial smooth minimum
* and maximum functions, with individual control over their smoothness. */
float smooth_clamp(const float x,
const float min_value,
const float max_value,
const float min_smoothness,
const float max_smoothness)
{
return this->smooth_min(
max_value, this->smooth_max(min_value, x, min_smoothness), max_smoothness);
}
/* A variant of smooth_clamp that limits the smoothness such that the function evaluates to the
* given min for 0 <= min <= max and x >= 0. The aforementioned guarantee holds for the standard
* clamp function by definition, but since the smooth clamp function gradually increases before
* the specified min/max, if min/max are sufficiently close together or to zero, they will not
* evaluate to min at zero or at min, since zero or min will be at the region of the gradual
* increase.
*
* It can be shown that the width of the gradual increase region is equivalent to the smoothness
* parameter, so smoothness can't be larger than the difference between the min/max and zero, or
* larger than the difference between min and max themselves. Otherwise, zero or min will lie
* inside the gradual increase region of min/max. So we limit the smoothness of min/max by taking
* the minimum with the distances to zero and to the distance to the other bound. */
float adaptive_smooth_clamp(const float x,
const float min_value,
const float max_value,
const float smoothness)
{
const float range_distance = math::distance(min_value, max_value);
const float distance_from_min_to_zero = math::distance(min_value, 0.0f);
const float distance_from_max_to_zero = math::distance(max_value, 0.0f);
const float max_safe_smoothness_for_min = math::min(distance_from_min_to_zero, range_distance);
const float max_safe_smoothness_for_max = math::min(distance_from_max_to_zero, range_distance);
const float min_smoothness = math::min(smoothness, max_safe_smoothness_for_min);
const float max_smoothness = math::min(smoothness, max_safe_smoothness_for_max);
return this->smooth_clamp(x, min_value, max_value, min_smoothness, max_smoothness);
}
float get_threshold()
{
return math::max(0.0f, this->get_input("Threshold").get_single_value_default(1.0f));
}
float get_highlights_smoothness()
{
return math::max(0.0f,
this->get_input("Highlights Smoothness").get_single_value_default(0.1f));
}
float get_max_highlights()
{
return math::max(0.0f, this->get_input("Maximum Highlights").get_single_value_default(0.0f));
}
/* As a performance optimization, the operation can compute the glare on a fraction of the input
* image size, so we extract the highlights to a smaller result, whose size is returned by this
* method. */
@@ -2145,11 +2262,6 @@ class GlareOperation : public NodeOperation {
* Common.
* ------- */
float get_threshold()
{
return math::max(0.0f, this->get_input("Threshold").get_single_value_default(1.0f));
}
float get_strength()
{
return math::max(0.0f, this->get_input("Strength").get_single_value_default(1.0f));