From b7742e6988052d9aae95d60009c45609b8eff733 Mon Sep 17 00:00:00 2001 From: Mohamed Hassan Date: Wed, 20 Aug 2025 15:10:18 +0200 Subject: [PATCH] Compositor: Add Jitter input to Glare Sun beams This patch introduces a new Jitter option for the Sun Beams mode of the Glare node. If non-zero, is uses an approximation that is faster to compute but makes the result more noisy. Internally, this is computed by reducing the number of samples used to integrate the results and introducing a random offset during integration. Pull Request: https://projects.blender.org/blender/blender/pulls/141789 --- .../shaders/compositor_glare_sun_beams.glsl | 40 +++++++++- .../shaders/infos/compositor_glare_info.hh | 13 +++- .../composite/nodes/node_composite_glare.cc | 75 +++++++++++++++++-- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/source/blender/compositor/shaders/compositor_glare_sun_beams.glsl b/source/blender/compositor/shaders/compositor_glare_sun_beams.glsl index 2e8a4bb0269..cb26656b022 100644 --- a/source/blender/compositor/shaders/compositor_glare_sun_beams.glsl +++ b/source/blender/compositor/shaders/compositor_glare_sun_beams.glsl @@ -2,9 +2,27 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "gpu_shader_common_hash.glsl" #include "gpu_shader_compositor_texture_utilities.glsl" #include "gpu_shader_math_base_lib.glsl" +/* Returns an index for a position along the path between the texel and the source. + * + * When jitter is enabled, the position index is computed using the Global Shift + * sampling technique: a hash-based global shift is applied to the indices which is then + * factored to cover the range [0, steps]. + * Without jitter, the integer index `i` is returned + * directly. + */ +float get_sample_position(int i, float random_offset) +{ +#if defined(JITTER) + return safe_divide(i + random_offset, 1.0f - jitter_factor); +#else + return i; +#endif +} + void main() { int2 texel = int2(gl_GlobalInvocationID.xy); @@ -26,8 +44,17 @@ void main() float accumulated_weight = 0.0f; float4 accumulated_color = float4(0.0f); - for (int i = 0; i <= steps; i++) { - float2 position = coordinates + i * step_vector; + +#if defined(JITTER) + int number_of_steps = int((1.0f - jitter_factor) * steps); +#else + int number_of_steps = steps; +#endif + float random_offset = hash_uint2_to_float(uint(texel.x), uint(texel.y)); + + for (int i = 0; i <= number_of_steps; i++) { + float position_index = get_sample_position(i, random_offset); + float2 position = coordinates + position_index * step_vector; /* We are already past the image boundaries, and any future steps are also past the image * boundaries, so break. */ @@ -39,12 +66,17 @@ void main() /* Attenuate the contributions of pixels that are further away from the source using a * quadratic falloff. */ - float weight = square(1.0f - i / float(steps)); + float weight = square(1.0f - position_index / float(steps)); accumulated_weight += weight; accumulated_color += sample_color * weight; } - accumulated_color /= accumulated_weight != 0.0f ? accumulated_weight : 1.0f; + if (accumulated_weight != 0.0f) { + accumulated_color /= accumulated_weight; + } + else { + accumulated_color = texture(input_tx, coordinates); + } imageStore(output_img, texel, accumulated_color); } diff --git a/source/blender/compositor/shaders/infos/compositor_glare_info.hh b/source/blender/compositor/shaders/infos/compositor_glare_info.hh index b02da6f1ed9..76b8a1052db 100644 --- a/source/blender/compositor/shaders/infos/compositor_glare_info.hh +++ b/source/blender/compositor/shaders/infos/compositor_glare_info.hh @@ -173,12 +173,23 @@ GPU_SHADER_CREATE_END() * Sun Beams * ----- */ -GPU_SHADER_CREATE_INFO(compositor_glare_sun_beams) +GPU_SHADER_CREATE_INFO(compositor_glare_sun_beams_shared) LOCAL_GROUP_SIZE(16, 16) PUSH_CONSTANT(float2, source) PUSH_CONSTANT(int, max_steps) SAMPLER(0, sampler2D, input_tx) IMAGE(0, SFLOAT_16_16_16_16, write, image2D, output_img) COMPUTE_SOURCE("compositor_glare_sun_beams.glsl") +GPU_SHADER_CREATE_END() + +GPU_SHADER_CREATE_INFO(compositor_glare_sun_beams) +ADDITIONAL_INFO(compositor_glare_sun_beams_shared) +DO_STATIC_COMPILATION() +GPU_SHADER_CREATE_END() + +GPU_SHADER_CREATE_INFO(compositor_glare_sun_beams_jitter) +ADDITIONAL_INFO(compositor_glare_sun_beams_shared) +DEFINE("JITTER") +PUSH_CONSTANT(float, jitter_factor) DO_STATIC_COMPILATION() GPU_SHADER_CREATE_END() diff --git a/source/blender/nodes/composite/nodes/node_composite_glare.cc b/source/blender/nodes/composite/nodes/node_composite_glare.cc index c282921dccf..8efb5501aca 100644 --- a/source/blender/nodes/composite/nodes/node_composite_glare.cc +++ b/source/blender/nodes/composite/nodes/node_composite_glare.cc @@ -26,6 +26,7 @@ #include "BLI_math_color.h" #include "BLI_math_vector.hh" #include "BLI_math_vector_types.hh" +#include "BLI_noise.hh" #include "BLI_task.hh" #include "DNA_scene_types.h" @@ -171,6 +172,14 @@ static void cmp_node_glare_declare(NodeDeclarationBuilder &b) .description( "The position of the source of the rays in normalized coordinates. 0 means lower left " "corner and 1 means upper right corner"); + glare_panel.add_input("Jitter") + .default_value(0.0f) + .min(0.0f) + .max(1.0) + .subtype(PROP_FACTOR) + .description( + "The amount of jitter to introduce while computing rays, higher jitter can be faster " + "but can produce grainy or noisy results"); } static void node_composit_init_glare(bNodeTree * /*ntree*/, bNode *node) @@ -222,6 +231,10 @@ static void node_update(bNodeTree *ntree, bNode *node) bNodeSocket *source_input = bke::node_find_socket(*node, SOCK_IN, "Sun Position"); blender::bke::node_set_socket_availability( *ntree, *source_input, glare_type == CMP_NODE_GLARE_SUN_BEAMS); + + bNodeSocket *jitter_steps = bke::node_find_socket(*node, SOCK_IN, "Jitter"); + blender::bke::node_set_socket_availability( + *ntree, *jitter_steps, glare_type == CMP_NODE_GLARE_SUN_BEAMS); } class SocketSearchOp { @@ -2247,10 +2260,11 @@ class GlareOperation : public NodeOperation { Result execute_sun_beams_gpu(Result &highlights, const int max_steps) { - gpu::Shader *shader = context().get_shader("compositor_glare_sun_beams"); + gpu::Shader *shader = context().get_shader(this->get_compositor_sun_beams_shader()); GPU_shader_bind(shader); GPU_shader_uniform_2fv(shader, "source", this->get_sun_position()); + GPU_shader_uniform_1f(shader, "jitter_factor", this->get_jitter_factor()); GPU_shader_uniform_1i(shader, "max_steps", max_steps); GPU_texture_filter_mode(highlights, true); @@ -2270,6 +2284,14 @@ class GlareOperation : public NodeOperation { return output_image; } + const char *get_compositor_sun_beams_shader() + { + if (this->get_use_jitter()) { + return "compositor_glare_sun_beams_jitter"; + } + return "compositor_glare_sun_beams"; + } + Result execute_sun_beams_cpu(Result &highlights, const int max_steps) { const float2 source = this->get_sun_position(); @@ -2278,6 +2300,8 @@ class GlareOperation : public NodeOperation { output.allocate_texture(highlights.domain()); const int2 input_size = highlights.domain().size; + float jitter_factor = this->get_jitter_factor(); + bool use_jitter = this->get_use_jitter(); parallel_for(input_size, [&](const int2 texel) { /* The number of steps is the distance in pixels from the source to the current texel. With * at least a single step and at most the user specified maximum ray length, which is @@ -2296,8 +2320,14 @@ class GlareOperation : public NodeOperation { float accumulated_weight = 0.0f; float4 accumulated_color = float4(0.0f); - for (int i = 0; i <= steps; i++) { - float2 position = coordinates + i * step_vector; + + int number_of_steps = (1.0f - jitter_factor) * steps; + float random_offset = noise::hash_to_float(texel.x, texel.y); + + for (int i = 0; i <= number_of_steps; i++) { + float position_index = this->get_sample_position( + i, use_jitter, jitter_factor, random_offset); + float2 position = coordinates + position_index * step_vector; /* We are already past the image boundaries, and any future steps are also past the image * boundaries, so break. */ @@ -2309,18 +2339,53 @@ class GlareOperation : public NodeOperation { /* Attenuate the contributions of pixels that are further away from the source using a * quadratic falloff. */ - float weight = math::square(1.0f - i / float(steps)); + float weight = math::square(1.0f - position_index / float(steps)); accumulated_weight += weight; accumulated_color += sample_color * weight; } - accumulated_color /= accumulated_weight != 0.0f ? accumulated_weight : 1.0f; + if (accumulated_weight != 0.0f) { + accumulated_color /= accumulated_weight; + } + else { + accumulated_color = highlights.sample_bilinear_zero(coordinates); + } output.store_pixel(texel, accumulated_color); }); return output; } + /* Returns an index for a position along the path between the texel and the source. + * + * When jitter is enabled, the position index is computed using the Global Shift + * sampling technique: a hash-based global shift is applied to the indices which is then + * factored to cover the range [0, steps]. + * Without jitter, the integer index `i` is returned + * directly. + */ + + float get_sample_position(const int i, + const bool use_jitter, + float jitter_factor, + const float random_offset) + { + if (use_jitter) { + return math::safe_divide(i + random_offset, 1.0f - jitter_factor); + } + return i; + } + + bool get_use_jitter() + { + return this->get_jitter_factor() != 0.0f; + } + + float get_jitter_factor() + { + return math::clamp(this->get_input("Jitter").get_single_value_default(0.0f), 0.0f, 1.0f); + } + /* ---------- * Glare Mix. * ---------- */