Compositor: Normalize Bloom output

This patch normalizes the Bloom output to be more energy conserving and
in a reasonable range. This is essentially constructed such that the
impulse response to a constant input maintains the same input.

The reason why Bloom has a very high range is because it accumulates a
downsampling chain without any sort of attenuation, so the final result
can be quite large. EEVEE fixed that by making the Strength range in the
[0, 0.1] range, so users who are used to that range think the default
value of a unit Strength in the glare node is large and hard to work
with. Hence the need for this patch.

The normalization factor is simply the length of the chain, since for a
constant input, all chain images will have the same constant input.

We need to version this change in a similar manner to how the glare node
was versioned in 004e3d39fa, where the scene render size is assumed. We
also assume the inputs are not connected, because they were turned into
inputs just last week, so we needn't worry about that case.

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

Pull Request: https://projects.blender.org/blender/blender/pulls/133037
This commit is contained in:
Omar Emara
2025-01-14 13:05:15 +01:00
committed by Omar Emara
parent 16e89b7366
commit b92a6eab3a
3 changed files with 168 additions and 26 deletions

View File

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

@@ -1107,6 +1107,87 @@ static void do_version_glare_node_options_to_inputs_recursive(
node_trees_already_versioned.add_new(node_tree);
}
/* The bloom glare is now normalized by its chain length, see the compute_bloom_chain_length method
* in the glare code. So we need to multiply the strength by the chain length to restore its
* original value. Since the chain length depend on the input image size, which is runtime
* information, we assume the render size as a guess. */
static void do_version_glare_node_bloom_strength(const Scene *scene,
bNodeTree *node_tree,
bNode *node)
{
NodeGlare *storage = static_cast<NodeGlare *>(node->storage);
if (!storage) {
return;
}
if (storage->type != CMP_NODE_GLARE_BLOOM) {
return;
}
/* See the get_quality_factor method in the glare code. */
const int quality_factor = 1 << storage->quality;
blender::int2 render_size;
BKE_render_resolution(&scene->r, true, &render_size.x, &render_size.y);
const blender::int2 highlights_size = render_size / quality_factor;
bNodeSocket *size = version_node_add_socket_if_not_exist(
node_tree, node, SOCK_IN, SOCK_FLOAT, PROP_FACTOR, "Size", "Size");
const float size_value = size->default_value_typed<bNodeSocketValueFloat>()->value;
/* See the compute_bloom_chain_length method in the glare code. */
const int smaller_dimension = blender::math::reduce_min(highlights_size);
const float scaled_dimension = smaller_dimension * size_value;
const int chain_length = int(std::log2(blender::math::max(1.0f, scaled_dimension)));
auto scale_strength = [chain_length](const float strength) { return strength * chain_length; };
bNodeSocket *strength_input = version_node_add_socket_if_not_exist(
node_tree, node, SOCK_IN, SOCK_FLOAT, PROP_FACTOR, "Strength", "Strength");
strength_input->default_value_typed<bNodeSocketValueFloat>()->value = scale_strength(
strength_input->default_value_typed<bNodeSocketValueFloat>()->value);
/* Compute the RNA path of the strength input. */
char escaped_node_name[sizeof(node->name) * 2 + 1];
BLI_str_escape(escaped_node_name, node->name, sizeof(escaped_node_name));
const std::string strength_rna_path = fmt::format("nodes[\"{}\"].inputs[4].default_value",
escaped_node_name);
/* Scale F-Curve. */
BKE_fcurves_id_cb(&node_tree->id, [&](ID * /*id*/, FCurve *fcurve) {
if (strength_rna_path == fcurve->rna_path) {
adjust_fcurve_key_frame_values(
fcurve, PROP_FLOAT, [&](const float value) { return scale_strength(value); });
}
});
}
static void do_version_glare_node_bloom_strength_recursive(
const Scene *scene,
bNodeTree *node_tree,
blender::Set<bNodeTree *> &node_trees_already_versioned)
{
if (node_trees_already_versioned.contains(node_tree)) {
return;
}
LISTBASE_FOREACH (bNode *, node, &node_tree->nodes) {
if (node->type_legacy == CMP_NODE_GLARE) {
do_version_glare_node_bloom_strength(scene, node_tree, node);
}
else if (node->is_group()) {
bNodeTree *child_tree = reinterpret_cast<bNodeTree *>(node->id);
if (child_tree) {
do_version_glare_node_bloom_strength_recursive(
scene, child_tree, node_trees_already_versioned);
}
}
}
node_trees_already_versioned.add_new(node_tree);
}
static bool all_scenes_use(Main *bmain, const blender::Span<const char *> engines)
{
if (!bmain->scenes.first) {
@@ -1382,6 +1463,35 @@ void do_versions_after_linking_400(FileData *fd, Main *bmain)
version_node_socket_index_animdata(bmain, NTREE_COMPOSIT, CMP_NODE_GLARE, 2, 2, 13);
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 21)) {
blender::Set<bNodeTree *> node_trees_already_versioned;
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
bNodeTree *node_tree = scene->nodetree;
if (!node_tree) {
continue;
}
do_version_glare_node_bloom_strength_recursive(
scene, node_tree, node_trees_already_versioned);
}
/* The above loop versioned all node trees used in a scene, but other node trees might exist
* that are not used in a scene. For those, assume the first scene in the file, as this is
* better than not doing versioning at all. */
Scene *scene = static_cast<Scene *>(bmain->scenes.first);
LISTBASE_FOREACH (bNodeTree *, node_tree, &bmain->nodetrees) {
if (node_trees_already_versioned.contains(node_tree)) {
continue;
}
LISTBASE_FOREACH (bNode *, node, &node_tree->nodes) {
if (node->type_legacy == CMP_NODE_GLARE) {
do_version_glare_node_bloom_strength(scene, node_tree, node);
}
}
node_trees_already_versioned.add_new(node_tree);
}
}
/**
* 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

@@ -290,7 +290,7 @@ class GlareOperation : public NodeOperation {
GPU_texture_filter_mode(input_image, true);
input_image.bind_as_texture(shader, "input_tx");
const int2 highlights_size = get_highlights_size();
const int2 highlights_size = this->get_glare_image_size();
Result highlights_result = context().create_result(ResultType::Color);
highlights_result.allocate_texture(highlights_size);
highlights_result.bind_as_image(shader, "output_img");
@@ -312,7 +312,7 @@ class GlareOperation : public NodeOperation {
const Result &input = get_input("Image");
const int2 highlights_size = this->get_highlights_size();
const int2 highlights_size = this->get_glare_image_size();
Result output = context().create_result(ResultType::Color);
output.allocate_texture(highlights_size);
@@ -439,14 +439,6 @@ class GlareOperation : public NodeOperation {
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. */
int2 get_highlights_size()
{
return this->compute_domain().size / this->get_quality_factor();
}
/* Writes the given input highlights by upsampling it using bilinear interpolation to match the
* size of the original input, allocating the highlights output and writing the result to it. */
void write_highlights_output(const Result &highlights)
@@ -1627,15 +1619,7 @@ class GlareOperation : public NodeOperation {
* achieved when down-sampling happens down to the smallest size of 2. */
Result execute_bloom(Result &highlights)
{
/* The maximum possible glare size is achieved when we down-sampled down to the smallest size
* of 2, which would result in a down-sampling chain length of the binary logarithm of the
* smaller dimension of the size of the highlights.
*
* However, as users might want a smaller glare size, we reduce the chain length by the
* size supplied by the user. Also make sure that log2 does not get zero. */
const int smaller_dimension = math::reduce_min(highlights.domain().size);
const float scaled_dimension = smaller_dimension * this->get_size();
const int chain_length = int(std::log2(math::max(1.0f, scaled_dimension)));
const int chain_length = this->compute_bloom_chain_length();
/* If the chain length is less than 2, that means no down-sampling will happen, so we just
* return a copy of the highlights. This is a sanitization of a corner case, so no need to
@@ -1935,6 +1919,20 @@ class GlareOperation : public NodeOperation {
math::safe_rcp(math::reduce_add(weights));
}
/* The maximum possible glare size is achieved when we down-sampled down to the smallest size of
* 2, which would result in a down-sampling chain length of the binary logarithm of the smaller
* dimension of the size of the highlights.
*
* However, as users might want a smaller glare size, we reduce the chain length by the size
* supplied by the user. Also make sure that log2 does not get zero. */
int compute_bloom_chain_length()
{
const int2 image_size = this->get_glare_image_size();
const int smaller_dimension = math::reduce_min(image_size);
const float scaled_dimension = smaller_dimension * this->get_size();
return int(std::log2(math::max(1.0f, scaled_dimension)));
}
/* ---------------
* Fog Glow Glare.
* --------------- */
@@ -2152,7 +2150,7 @@ class GlareOperation : public NodeOperation {
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "saturation", this->get_saturation());
GPU_shader_uniform_3fv(shader, "tint", this->get_tint());
GPU_shader_uniform_3fv(shader, "tint", this->get_corrected_tint());
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
@@ -2176,7 +2174,7 @@ class GlareOperation : public NodeOperation {
void execute_mix_cpu(const Result &glare_result)
{
const float saturation = this->get_saturation();
const float3 tint = this->get_tint();
const float3 tint = this->get_corrected_tint();
const Result &input = get_input("Image");
@@ -2223,7 +2221,7 @@ class GlareOperation : public NodeOperation {
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "saturation", this->get_saturation());
GPU_shader_uniform_3fv(shader, "tint", this->get_tint());
GPU_shader_uniform_3fv(shader, "tint", this->get_corrected_tint());
GPU_texture_filter_mode(glare, true);
GPU_texture_extend_mode(glare, GPU_SAMPLER_EXTEND_MODE_EXTEND);
@@ -2244,7 +2242,7 @@ class GlareOperation : public NodeOperation {
void write_glare_output_cpu(const Result &glare)
{
const float saturation = this->get_saturation();
const float3 tint = this->get_tint();
const float3 tint = this->get_corrected_tint();
const Result &image_input = this->get_input("Image");
Result &output = this->get_result("Glare");
@@ -2267,6 +2265,33 @@ class GlareOperation : public NodeOperation {
});
}
/* Combine the tint, strength, and normalization scale into a single factor that can be
* multiplied to the glare. */
float3 get_corrected_tint()
{
return this->get_tint() * this->get_strength() / this->get_normalization_scale();
}
/* The computed glare might need to be normalized to be energy conserving or be in a reasonable
* range, instead of doing that in a separate step as part of the glare computation, we delay the
* normalization until the mixing step as an optimization, since we multiply by the tint and
* strength anyways. */
float get_normalization_scale()
{
switch (static_cast<CMPNodeGlareType>(node_storage(bnode()).type)) {
case CMP_NODE_GLARE_BLOOM:
/* Bloom adds a number of layers equivalent to the chain length, so we need to normalize by
* the chain length, see the bloom code for more information. */
return this->compute_bloom_chain_length();
case CMP_NODE_GLARE_SIMPLE_STAR:
case CMP_NODE_GLARE_FOG_GLOW:
case CMP_NODE_GLARE_STREAKS:
case CMP_NODE_GLARE_GHOST:
return 1.0f;
}
return 1.0f;
}
/* -------
* Common.
* ------- */
@@ -2283,8 +2308,7 @@ class GlareOperation : public NodeOperation {
float3 get_tint()
{
return this->get_input("Tint").get_single_value_default(float4(1.0f)).xyz() *
this->get_strength();
return this->get_input("Tint").get_single_value_default(float4(1.0f)).xyz();
}
float get_size()
@@ -2308,6 +2332,14 @@ class GlareOperation : public NodeOperation {
this->get_input("Color Modulation").get_single_value_default(0.25f), 0.0f, 1.0f);
}
/* As a performance optimization, the operation can compute the glare on a fraction of the input
* image size, so the input is downsampled then upsampled at the end, and this method returns the
* size after downsampling. */
int2 get_glare_image_size()
{
return this->compute_domain().size / this->get_quality_factor();
}
/* The glare node can compute the glare on a fraction of the input image size to improve
* performance. The quality values and their corresponding quality factors are as follows:
*