mutter/clutter/clutter/clutter-blur.c
Georges Basile Stavracas Neto 49653b0b0a clutter/blur: Select a better n_steps
The n_steps variable corresponds to the number of *pairs* of
texture lookups that the blur shader does. For example, when
n_steps = 1, the for-loop reads 1 pixel before and 1 pixel
after the current one.

Our blur shader is heavily inspired in WebRender's blur shader,
the biggest difference being that we calculate the gaussian
samples in the fragment shader itself, and not in the vertex
shader. (This could be an improvement in performance for the
future though!)

WebRender's blur shader calculates n_steps differently than what
we currently do, though. It calculates n_step in such a way that
at least 2 steps are performed for evey non-zero sigma value.

Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1646>
2020-12-17 13:21:50 +00:00

432 lines
14 KiB
C

/*
* Copyright (C) 2020 Endless OS Foundation, LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "clutter-blur-private.h"
#include "clutter-backend.h"
/**
* SECTION:clutter-blur
* @short_description: Blur textures
*
* #ClutterBlur is a moderately fast gaussian blur implementation.
*
* # Optimizations
*
* There are a number of optimizations in place to make this blur implementation
* real-time. All in all, the implementation performs best when using large
* blur-radii that allow downscaling the texture to smaller sizes, at small
* radii where no downscaling is possible this can easily halve the framerate.
*
* ## Multipass
*
* It is implemented in 2 passes: vertical and horizontal.
*
* ## Downscaling
*
* #ClutterBlur uses dynamic downscaling to speed up blurring. Downscaling
* happens in factors of 2 (the image is downscaled either by 2, 4, 8, 16, …)
* and depends on the blur radius, the texture size, among others.
*
* The texture is drawn into a downscaled framebuffer; the blur passes are
* applied on the downscaled texture contents; and finally, the blurred
* contents are drawn
* upscaled again.
*
* ## Hardware Interpolation
*
* This blur implementation cuts down the number of sampling operations by
* exploiting the hardware interpolation that is performed when sampling between
* pixel boundaries. This technique is described at:
*
* http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
*
* ## Incremental gauss-factor calculation
*
* The kernel values for the gaussian kernel are computed incrementally instead
* of running the expensive calculations multiple times inside the blur shader.
* The implementation is based on the algorithm presented by K. Turkowski in
* GPU Gems 3, chapter 40:
*
* https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html
*
*/
static const char *gaussian_blur_glsl_declarations =
"uniform float sigma; \n"
"uniform float pixel_step; \n"
"uniform vec2 direction; \n";
static const char *gaussian_blur_glsl =
" vec2 uv = vec2 (cogl_tex_coord.st); \n"
" \n"
" vec3 gauss_coefficient; \n"
" gauss_coefficient.x = 1.0 / (sqrt (2.0 * 3.14159265) * sigma); \n"
" gauss_coefficient.y = exp (-0.5 / (sigma * sigma)); \n"
" gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y; \n"
" \n"
" float gauss_coefficient_total = gauss_coefficient.x; \n"
" \n"
" vec4 ret = texture2D (cogl_sampler, uv) * gauss_coefficient.x; \n"
" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
" \n"
" int n_steps = int (ceil (1.5 * sigma)) * 2; \n"
" \n"
" for (int i = 1; i <= n_steps; i += 2) { \n"
" float coefficient_subtotal = gauss_coefficient.x; \n"
" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
" coefficient_subtotal += gauss_coefficient.x; \n"
" \n"
" float gauss_ratio = gauss_coefficient.x / coefficient_subtotal; \n"
" \n"
" float foffset = float (i) + gauss_ratio; \n"
" vec2 offset = direction * foffset * pixel_step; \n"
" \n"
" ret += texture2D (cogl_sampler, uv + offset) * coefficient_subtotal; \n"
" ret += texture2D (cogl_sampler, uv - offset) * coefficient_subtotal; \n"
" \n"
" gauss_coefficient_total += 2.0 * coefficient_subtotal; \n"
" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
" } \n"
" \n"
" cogl_texel = ret / gauss_coefficient_total; \n";
#define MIN_DOWNSCALE_SIZE 256.f
#define MAX_SIGMA 6.f
enum
{
VERTICAL,
HORIZONTAL,
};
typedef struct
{
CoglFramebuffer *framebuffer;
CoglPipeline *pipeline;
CoglTexture *texture;
int orientation;
} BlurPass;
struct _ClutterBlur
{
CoglTexture *source_texture;
float sigma;
float downscale_factor;
BlurPass pass[2];
};
static CoglPipeline*
create_blur_pipeline (void)
{
static CoglPipelineKey blur_pipeline_key = "clutter-blur-pipeline-private";
CoglContext *ctx =
clutter_backend_get_cogl_context (clutter_get_default_backend ());
CoglPipeline *blur_pipeline;
blur_pipeline =
cogl_context_get_named_pipeline (ctx, &blur_pipeline_key);
if (G_UNLIKELY (blur_pipeline == NULL))
{
CoglSnippet *snippet;
blur_pipeline = cogl_pipeline_new (ctx);
cogl_pipeline_set_layer_null_texture (blur_pipeline, 0);
cogl_pipeline_set_layer_filters (blur_pipeline,
0,
COGL_PIPELINE_FILTER_LINEAR,
COGL_PIPELINE_FILTER_LINEAR);
cogl_pipeline_set_layer_wrap_mode (blur_pipeline,
0,
COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE);
snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP,
gaussian_blur_glsl_declarations,
NULL);
cogl_snippet_set_replace (snippet, gaussian_blur_glsl);
cogl_pipeline_add_layer_snippet (blur_pipeline, 0, snippet);
cogl_object_unref (snippet);
cogl_context_set_named_pipeline (ctx, &blur_pipeline_key, blur_pipeline);
}
return cogl_pipeline_copy (blur_pipeline);
}
static void
update_blur_uniforms (ClutterBlur *blur,
BlurPass *pass)
{
gboolean vertical = pass->orientation == VERTICAL;
int sigma_uniform;
int pixel_step_uniform;
int direction_uniform;
pixel_step_uniform =
cogl_pipeline_get_uniform_location (pass->pipeline, "pixel_step");
if (pixel_step_uniform > -1)
{
float pixel_step;
if (vertical)
pixel_step = 1.f / cogl_texture_get_height (pass->texture);
else
pixel_step = 1.f / cogl_texture_get_width (pass->texture);
cogl_pipeline_set_uniform_1f (pass->pipeline,
pixel_step_uniform,
pixel_step);
}
sigma_uniform = cogl_pipeline_get_uniform_location (pass->pipeline, "sigma");
if (sigma_uniform > -1)
{
cogl_pipeline_set_uniform_1f (pass->pipeline,
sigma_uniform,
blur->sigma / blur->downscale_factor);
}
direction_uniform =
cogl_pipeline_get_uniform_location (pass->pipeline, "direction");
if (direction_uniform > -1)
{
gboolean horizontal = !vertical;
float direction[2] = {
horizontal,
vertical,
};
cogl_pipeline_set_uniform_float (pass->pipeline,
direction_uniform,
2, 1,
direction);
}
}
static gboolean
create_fbo (ClutterBlur *blur,
BlurPass *pass)
{
CoglContext *ctx =
clutter_backend_get_cogl_context (clutter_get_default_backend ());
float scaled_height;
float scaled_width;
float height;
float width;
g_clear_pointer (&pass->texture, cogl_object_unref);
g_clear_object (&pass->framebuffer);
width = cogl_texture_get_width (blur->source_texture);
height = cogl_texture_get_height (blur->source_texture);
scaled_width = floorf (width / blur->downscale_factor);
scaled_height = floorf (height / blur->downscale_factor);
pass->texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx,
scaled_width,
scaled_height));
if (!pass->texture)
return FALSE;
pass->framebuffer =
COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (pass->texture));
if (!pass->framebuffer)
{
g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
return FALSE;
}
cogl_framebuffer_orthographic (pass->framebuffer,
0.0, 0.0,
scaled_width,
scaled_height,
0.0, 1.0);
return TRUE;
}
static gboolean
setup_blur_pass (ClutterBlur *blur,
BlurPass *pass,
int orientation,
CoglTexture *texture)
{
pass->orientation = orientation;
pass->pipeline = create_blur_pipeline ();
cogl_pipeline_set_layer_texture (pass->pipeline, 0, texture);
if (!create_fbo (blur, pass))
return FALSE;
update_blur_uniforms (blur, pass);
return TRUE;
}
static float
calculate_downscale_factor (float width,
float height,
float sigma)
{
float downscale_factor = 1.f;
float scaled_width = width;
float scaled_height = height;
float scaled_sigma = sigma;
/* This is the algorithm used by Firefox; keep downscaling until either the
* blur radius is lower than the threshold, or the downscaled texture is too
* small.
*/
while (scaled_sigma > MAX_SIGMA &&
scaled_width > MIN_DOWNSCALE_SIZE &&
scaled_height > MIN_DOWNSCALE_SIZE)
{
downscale_factor *= 2.f;
scaled_width = width / downscale_factor;
scaled_height = height / downscale_factor;
scaled_sigma = sigma / downscale_factor;
}
return downscale_factor;
}
static void
apply_blur_pass (BlurPass *pass)
{
CoglColor transparent;
cogl_color_init_from_4ub (&transparent, 0, 0, 0, 0);
cogl_framebuffer_clear (pass->framebuffer,
COGL_BUFFER_BIT_COLOR,
&transparent);
cogl_framebuffer_draw_rectangle (pass->framebuffer,
pass->pipeline,
0, 0,
cogl_texture_get_width (pass->texture),
cogl_texture_get_height (pass->texture));
}
static void
clear_blur_pass (BlurPass *pass)
{
g_clear_pointer (&pass->pipeline, cogl_object_unref);
g_clear_pointer (&pass->texture, cogl_object_unref);
g_clear_object (&pass->framebuffer);
}
/**
* clutter_blur_new:
* @texture: a #CoglTexture
* @sigma: blur sigma
*
* Creates a new #ClutterBlur.
*
* Returns: (transfer full) (nullable): A newly created #ClutterBlur
*/
ClutterBlur *
clutter_blur_new (CoglTexture *texture,
float sigma)
{
ClutterBlur *blur;
unsigned int height;
unsigned int width;
BlurPass *hpass;
BlurPass *vpass;
g_return_val_if_fail (texture != NULL, NULL);
g_return_val_if_fail (sigma >= 0.0, NULL);
width = cogl_texture_get_width (texture);
height = cogl_texture_get_height (texture);
blur = g_new0 (ClutterBlur, 1);
blur->sigma = sigma;
blur->source_texture = cogl_object_ref (texture);
blur->downscale_factor = calculate_downscale_factor (width, height, sigma);
if (G_APPROX_VALUE (sigma, 0.0, FLT_EPSILON))
goto out;
vpass = &blur->pass[VERTICAL];
hpass = &blur->pass[HORIZONTAL];
if (!setup_blur_pass (blur, vpass, VERTICAL, texture) ||
!setup_blur_pass (blur, hpass, HORIZONTAL, vpass->texture))
{
clutter_blur_free (blur);
return NULL;
}
out:
return g_steal_pointer (&blur);
}
/**
* clutter_blur_apply:
* @blur: a #ClutterBlur
*
* Applies the blur. The resulting texture can be retrieved by
* clutter_blur_get_texture().
*/
void
clutter_blur_apply (ClutterBlur *blur)
{
if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
return;
apply_blur_pass (&blur->pass[VERTICAL]);
apply_blur_pass (&blur->pass[HORIZONTAL]);
}
/**
* clutter_blur_get_texture:
* @blur: a #ClutterBlur
*
* Retrieves the texture where the blurred contents are stored. The
* contents are undefined until clutter_blur_apply() is called.
*
* Returns: (transfer none): a #CoglTexture
*/
CoglTexture *
clutter_blur_get_texture (ClutterBlur *blur)
{
if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
return blur->source_texture;
else
return blur->pass[HORIZONTAL].texture;
}
/**
* clutter_blur_free:
* @blur: A #ClutterBlur
*
* Frees @blur.
*/
void
clutter_blur_free (ClutterBlur *blur)
{
g_assert (blur);
clear_blur_pass (&blur->pass[VERTICAL]);
clear_blur_pass (&blur->pass[HORIZONTAL]);
cogl_clear_object (&blur->source_texture);
g_free (blur);
}