Compositor: Improve Glare quality option

This patch enhances the quality setting of the Glare node. Previously,
Medium and Low quality were implemented using a single tap interpolation
downsampling filter, which means small highlights that only span a small
number of pixels could be missed during downsampling, and that might
change across frames, causing flickering.

To fix this, we use a higher quality downsampling filter that averages
the entire 2x2 or 4x4 pixel blocks for the Medium and Low quality
settings respectively.

The upsampling pass also needs to be improved to avoid offsets, but this
will be implemented separately.

Pull Request: https://projects.blender.org/blender/blender/pulls/140237
This commit is contained in:
Mohamed Hassan
2025-06-16 10:38:40 +02:00
committed by Omar Emara
parent 53228d48be
commit 0a4b92678c
5 changed files with 112 additions and 8 deletions

View File

@@ -3,6 +3,11 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_common_color_utils.glsl"
#include "gpu_shader_compositor_texture_utilities.glsl"
#define CMP_NODE_GLARE_QUALITY_HIGH 0
#define CMP_NODE_GLARE_QUALITY_MEDIUM 1
#define CMP_NODE_GLARE_QUALITY_LOW 2
/* A Quadratic Polynomial smooth minimum function *without* normalization, based on:
*
@@ -64,10 +69,52 @@ void main()
{
int2 texel = int2(gl_GlobalInvocationID.xy);
float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(imageSize(output_img));
float4 color = float4(0.0f);
switch (quality) {
case CMP_NODE_GLARE_QUALITY_HIGH: {
color = texture_load(input_tx, texel);
break;
}
/* Down-sample the image 2 times to match the output size by averaging the 2x2 block of
* pixels into a single output pixel. This is done due to the bilinear interpolation at the
* center of the 2x2 block of pixels. */
case CMP_NODE_GLARE_QUALITY_MEDIUM: {
float2 normalized_coordinates = (float2(texel) * 2.0f + float2(1.0f)) /
float2(texture_size(input_tx));
color = texture(input_tx, normalized_coordinates);
break;
}
/* Down-sample the image 4 times to match the output size by averaging each 4x4 block of
* pixels into a single output pixel. This is done by averaging 4 bilinear taps at the
* center of each of the corner 2x2 pixel blocks, which are themselves the average of the
* 2x2 block due to the bilinear interpolation at the center. */
case CMP_NODE_GLARE_QUALITY_LOW: {
float2 lower_left_coordinates = (float2(texel) * 4.0f + float2(1.0f)) /
float2(texture_size(input_tx));
float4 lower_left_color = texture(input_tx, lower_left_coordinates);
float2 lower_right_coordinates = (float2(texel) * 4.0f + float2(3.0f, 1.0f)) /
float2(texture_size(input_tx));
float4 lower_right_color = texture(input_tx, lower_right_coordinates);
float2 upper_left_coordinates = (float2(texel) * 4.0f + float2(1.0f, 3.0f)) /
float2(texture_size(input_tx));
float4 upper_left_color = texture(input_tx, upper_left_coordinates);
float2 upper_right_coordinates = (float2(texel) * 4.0f + float2(3.0f)) /
float2(texture_size(input_tx));
float4 upper_right_color = texture(input_tx, upper_right_coordinates);
color = (upper_left_color + upper_right_color + lower_left_color + lower_right_color) / 4.0f;
break;
}
}
float4 hsva;
rgb_to_hsv(texture(input_tx, normalized_coordinates), hsva);
rgb_to_hsv(color, hsva);
/* 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

View File

@@ -13,6 +13,7 @@ LOCAL_GROUP_SIZE(16, 16)
PUSH_CONSTANT(float, threshold)
PUSH_CONSTANT(float, highlights_smoothness)
PUSH_CONSTANT(float, max_brightness)
PUSH_CONSTANT(int, quality)
SAMPLER(0, sampler2D, input_tx)
IMAGE(0, GPU_RGBA16F, write, image2D, output_img)
COMPUTE_SOURCE("compositor_glare_highlights.glsl")

View File

@@ -1336,6 +1336,13 @@ typedef struct NodeGlare {
char _pad1[4];
} NodeGlare;
/* Glare Node. Stored in NodeGlare.quality. */
typedef enum CMPNodeGlareQuality {
CMP_NODE_GLARE_QUALITY_HIGH = 0,
CMP_NODE_GLARE_QUALITY_MEDIUM = 1,
CMP_NODE_GLARE_QUALITY_LOW = 2,
} CMPNodeGlareQuality;
/** Tone-map node. */
typedef struct NodeTonemap {
float key DNA_DEPRECATED;

View File

@@ -7410,9 +7410,9 @@ static void def_cmp_glare(BlenderRNA * /*brna*/, StructRNA *srna)
};
static const EnumPropertyItem quality_items[] = {
{0, "HIGH", 0, "High", ""},
{1, "MEDIUM", 0, "Medium", ""},
{2, "LOW", 0, "Low", ""},
{CMP_NODE_GLARE_QUALITY_HIGH, "HIGH", 0, "High", ""},
{CMP_NODE_GLARE_QUALITY_MEDIUM, "MEDIUM", 0, "Medium", ""},
{CMP_NODE_GLARE_QUALITY_LOW, "LOW", 0, "Low", ""},
{0, nullptr, 0, nullptr, nullptr},
};

View File

@@ -316,6 +316,7 @@ class GlareOperation : public NodeOperation {
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());
GPU_shader_uniform_1i(shader, "quality", node_storage(bnode()).quality);
const Result &input_image = get_input("Image");
GPU_texture_filter_mode(input_image, true);
@@ -347,11 +348,59 @@ class GlareOperation : public NodeOperation {
Result output = context().create_result(ResultType::Color);
output.allocate_texture(highlights_size);
const CMPNodeGlareQuality quality = static_cast<CMPNodeGlareQuality>(
node_storage(bnode()).quality);
const int2 input_size = input.domain().size;
parallel_for(highlights_size, [&](const int2 texel) {
float2 normalized_coordinates = (float2(texel) + float2(0.5f)) / float2(highlights_size);
float4 color = float4(0.0f);
switch (quality) {
case CMP_NODE_GLARE_QUALITY_HIGH: {
color = input.load_pixel<float4>(texel);
break;
}
/* Down-sample the image 2 times to match the output size by averaging the 2x2 block of
* pixels into a single output pixel. This is done due to the bilinear interpolation at the
* center of the 2x2 block of pixels */
case CMP_NODE_GLARE_QUALITY_MEDIUM: {
float2 normalized_coordinates = (float2(texel) * 2.0f + float2(1.0f)) /
float2(input_size);
color = input.sample_bilinear_extended(normalized_coordinates);
break;
}
/* Down-sample the image 4 times to match the output size by averaging each 4x4 block of
* pixels into a single output pixel. This is done by averaging 4 bilinear taps at the
* center of each of the corner 2x2 pixel blocks, which are themselves the average of the
* 2x2 block due to the bilinear interpolation at the center. */
case CMP_NODE_GLARE_QUALITY_LOW: {
float2 lower_left_coordinates = (float2(texel) * 4.0f + float2(1.0f)) /
float2(input_size);
float4 lower_left_color = input.sample_bilinear_extended(lower_left_coordinates);
float2 lower_right_coordinates = (float2(texel) * 4.0f + float2(3.0f, 1.0f)) /
float2(input_size);
float4 lower_right_color = input.sample_bilinear_extended(lower_right_coordinates);
float2 upper_left_coordinates = (float2(texel) * 4.0f + float2(1.0f, 3.0f)) /
float2(input_size);
float4 upper_left_color = input.sample_bilinear_extended(upper_left_coordinates);
float2 upper_right_coordinates = (float2(texel) * 4.0f + float2(3.0f)) /
float2(input_size);
float4 upper_right_color = input.sample_bilinear_extended(upper_right_coordinates);
color = (upper_left_color + upper_right_color + lower_left_color + lower_right_color) /
4.0f;
break;
}
}
float4 hsva;
rgb_to_hsv_v(input.sample_bilinear_extended(normalized_coordinates), hsva);
rgb_to_hsv_v(color, hsva);
/* 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
@@ -2377,7 +2426,7 @@ class GlareOperation : public NodeOperation {
* size after downsampling. */
int2 get_glare_image_size()
{
return this->compute_domain().size / this->get_quality_factor();
return math::divide_ceil(this->compute_domain().size, int2(this->get_quality_factor()));
}
/* The glare node can compute the glare on a fraction of the input image size to improve