1 /*
2 * Copyright (C) 2020 Endless OS Foundation, LLC
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "clutter-blur-private.h"
19
20 #include "clutter-backend.h"
21
22 /**
23 * SECTION:clutter-blur
24 * @short_description: Blur textures
25 *
26 * #ClutterBlur is a moderately fast gaussian blur implementation.
27 *
28 * # Optimizations
29 *
30 * There are a number of optimizations in place to make this blur implementation
31 * real-time. All in all, the implementation performs best when using large
32 * blur-radii that allow downscaling the texture to smaller sizes, at small
33 * radii where no downscaling is possible this can easily halve the framerate.
34 *
35 * ## Multipass
36 *
37 * It is implemented in 2 passes: vertical and horizontal.
38 *
39 * ## Downscaling
40 *
41 * #ClutterBlur uses dynamic downscaling to speed up blurring. Downscaling
42 * happens in factors of 2 (the image is downscaled either by 2, 4, 8, 16, …)
43 * and depends on the blur radius, the texture size, among others.
44 *
45 * The texture is drawn into a downscaled framebuffer; the blur passes are
46 * applied on the downscaled texture contents; and finally, the blurred
47 * contents are drawn
48 * upscaled again.
49 *
50 * ## Hardware Interpolation
51 *
52 * This blur implementation cuts down the number of sampling operations by
53 * exploiting the hardware interpolation that is performed when sampling between
54 * pixel boundaries. This technique is described at:
55 *
56 * http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
57 *
58 * ## Incremental gauss-factor calculation
59 *
60 * The kernel values for the gaussian kernel are computed incrementally instead
61 * of running the expensive calculations multiple times inside the blur shader.
62 * The implementation is based on the algorithm presented by K. Turkowski in
63 * GPU Gems 3, chapter 40:
64 *
65 * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html
66 *
67 */
68
69 static const char *gaussian_blur_glsl_declarations =
70 "uniform float sigma; \n"
71 "uniform float pixel_step; \n"
72 "uniform vec2 direction; \n";
73
74 static const char *gaussian_blur_glsl =
75 " vec2 uv = vec2 (cogl_tex_coord.st); \n"
76 " \n"
77 " vec3 gauss_coefficient; \n"
78 " gauss_coefficient.x = 1.0 / (sqrt (2.0 * 3.14159265) * sigma); \n"
79 " gauss_coefficient.y = exp (-0.5 / (sigma * sigma)); \n"
80 " gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y; \n"
81 " \n"
82 " float gauss_coefficient_total = gauss_coefficient.x; \n"
83 " \n"
84 " vec4 ret = texture2D (cogl_sampler, uv) * gauss_coefficient.x; \n"
85 " gauss_coefficient.xy *= gauss_coefficient.yz; \n"
86 " \n"
87 " int n_steps = int (ceil (1.5 * sigma)) * 2; \n"
88 " \n"
89 " for (int i = 1; i <= n_steps; i += 2) { \n"
90 " float coefficient_subtotal = gauss_coefficient.x; \n"
91 " gauss_coefficient.xy *= gauss_coefficient.yz; \n"
92 " coefficient_subtotal += gauss_coefficient.x; \n"
93 " \n"
94 " float gauss_ratio = gauss_coefficient.x / coefficient_subtotal; \n"
95 " \n"
96 " float foffset = float (i) + gauss_ratio; \n"
97 " vec2 offset = direction * foffset * pixel_step; \n"
98 " \n"
99 " ret += texture2D (cogl_sampler, uv + offset) * coefficient_subtotal; \n"
100 " ret += texture2D (cogl_sampler, uv - offset) * coefficient_subtotal; \n"
101 " \n"
102 " gauss_coefficient_total += 2.0 * coefficient_subtotal; \n"
103 " gauss_coefficient.xy *= gauss_coefficient.yz; \n"
104 " } \n"
105 " \n"
106 " cogl_texel = ret / gauss_coefficient_total; \n";
107
108 #define MIN_DOWNSCALE_SIZE 256.f
109 #define MAX_SIGMA 6.f
110
111 enum
112 {
113 VERTICAL,
114 HORIZONTAL,
115 };
116
117 typedef struct
118 {
119 CoglFramebuffer *framebuffer;
120 CoglPipeline *pipeline;
121 CoglTexture *texture;
122 int orientation;
123 } BlurPass;
124
125 struct _ClutterBlur
126 {
127 CoglTexture *source_texture;
128 float sigma;
129 float downscale_factor;
130
131 BlurPass pass[2];
132 };
133
134 static CoglPipeline*
create_blur_pipeline(void)135 create_blur_pipeline (void)
136 {
137 static CoglPipelineKey blur_pipeline_key = "clutter-blur-pipeline-private";
138 CoglContext *ctx =
139 clutter_backend_get_cogl_context (clutter_get_default_backend ());
140 CoglPipeline *blur_pipeline;
141
142 blur_pipeline =
143 cogl_context_get_named_pipeline (ctx, &blur_pipeline_key);
144
145 if (G_UNLIKELY (blur_pipeline == NULL))
146 {
147 CoglSnippet *snippet;
148
149 blur_pipeline = cogl_pipeline_new (ctx);
150 cogl_pipeline_set_layer_null_texture (blur_pipeline, 0);
151 cogl_pipeline_set_layer_filters (blur_pipeline,
152 0,
153 COGL_PIPELINE_FILTER_LINEAR,
154 COGL_PIPELINE_FILTER_LINEAR);
155 cogl_pipeline_set_layer_wrap_mode (blur_pipeline,
156 0,
157 COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE);
158
159 snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP,
160 gaussian_blur_glsl_declarations,
161 NULL);
162 cogl_snippet_set_replace (snippet, gaussian_blur_glsl);
163 cogl_pipeline_add_layer_snippet (blur_pipeline, 0, snippet);
164 cogl_object_unref (snippet);
165
166 cogl_context_set_named_pipeline (ctx, &blur_pipeline_key, blur_pipeline);
167 }
168
169 return cogl_pipeline_copy (blur_pipeline);
170 }
171
172 static void
update_blur_uniforms(ClutterBlur * blur,BlurPass * pass)173 update_blur_uniforms (ClutterBlur *blur,
174 BlurPass *pass)
175 {
176 gboolean vertical = pass->orientation == VERTICAL;
177 int sigma_uniform;
178 int pixel_step_uniform;
179 int direction_uniform;
180
181 pixel_step_uniform =
182 cogl_pipeline_get_uniform_location (pass->pipeline, "pixel_step");
183 if (pixel_step_uniform > -1)
184 {
185 float pixel_step;
186
187 if (vertical)
188 pixel_step = 1.f / cogl_texture_get_height (pass->texture);
189 else
190 pixel_step = 1.f / cogl_texture_get_width (pass->texture);
191
192 cogl_pipeline_set_uniform_1f (pass->pipeline,
193 pixel_step_uniform,
194 pixel_step);
195 }
196
197 sigma_uniform = cogl_pipeline_get_uniform_location (pass->pipeline, "sigma");
198 if (sigma_uniform > -1)
199 {
200 cogl_pipeline_set_uniform_1f (pass->pipeline,
201 sigma_uniform,
202 blur->sigma / blur->downscale_factor);
203 }
204
205 direction_uniform =
206 cogl_pipeline_get_uniform_location (pass->pipeline, "direction");
207 if (direction_uniform > -1)
208 {
209 gboolean horizontal = !vertical;
210 float direction[2] = {
211 horizontal,
212 vertical,
213 };
214
215 cogl_pipeline_set_uniform_float (pass->pipeline,
216 direction_uniform,
217 2, 1,
218 direction);
219 }
220 }
221
222 static gboolean
create_fbo(ClutterBlur * blur,BlurPass * pass)223 create_fbo (ClutterBlur *blur,
224 BlurPass *pass)
225 {
226 CoglContext *ctx =
227 clutter_backend_get_cogl_context (clutter_get_default_backend ());
228 float scaled_height;
229 float scaled_width;
230 float height;
231 float width;
232
233 g_clear_pointer (&pass->texture, cogl_object_unref);
234 g_clear_object (&pass->framebuffer);
235
236 width = cogl_texture_get_width (blur->source_texture);
237 height = cogl_texture_get_height (blur->source_texture);
238 scaled_width = floorf (width / blur->downscale_factor);
239 scaled_height = floorf (height / blur->downscale_factor);
240
241 pass->texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx,
242 scaled_width,
243 scaled_height));
244 if (!pass->texture)
245 return FALSE;
246
247 pass->framebuffer =
248 COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (pass->texture));
249 if (!pass->framebuffer)
250 {
251 g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
252 return FALSE;
253 }
254
255 cogl_framebuffer_orthographic (pass->framebuffer,
256 0.0, 0.0,
257 scaled_width,
258 scaled_height,
259 0.0, 1.0);
260 return TRUE;
261 }
262
263 static gboolean
setup_blur_pass(ClutterBlur * blur,BlurPass * pass,int orientation,CoglTexture * texture)264 setup_blur_pass (ClutterBlur *blur,
265 BlurPass *pass,
266 int orientation,
267 CoglTexture *texture)
268 {
269 pass->orientation = orientation;
270 pass->pipeline = create_blur_pipeline ();
271 cogl_pipeline_set_layer_texture (pass->pipeline, 0, texture);
272
273 if (!create_fbo (blur, pass))
274 return FALSE;
275
276 update_blur_uniforms (blur, pass);
277 return TRUE;
278 }
279
280 static float
calculate_downscale_factor(float width,float height,float sigma)281 calculate_downscale_factor (float width,
282 float height,
283 float sigma)
284 {
285 float downscale_factor = 1.f;
286 float scaled_width = width;
287 float scaled_height = height;
288 float scaled_sigma = sigma;
289
290 /* This is the algorithm used by Firefox; keep downscaling until either the
291 * blur radius is lower than the threshold, or the downscaled texture is too
292 * small.
293 */
294 while (scaled_sigma > MAX_SIGMA &&
295 scaled_width > MIN_DOWNSCALE_SIZE &&
296 scaled_height > MIN_DOWNSCALE_SIZE)
297 {
298 downscale_factor *= 2.f;
299
300 scaled_width = width / downscale_factor;
301 scaled_height = height / downscale_factor;
302 scaled_sigma = sigma / downscale_factor;
303 }
304
305 return downscale_factor;
306 }
307
308 static void
apply_blur_pass(BlurPass * pass)309 apply_blur_pass (BlurPass *pass)
310 {
311 CoglColor transparent;
312
313 cogl_color_init_from_4ub (&transparent, 0, 0, 0, 0);
314
315 cogl_framebuffer_clear (pass->framebuffer,
316 COGL_BUFFER_BIT_COLOR,
317 &transparent);
318
319 cogl_framebuffer_draw_rectangle (pass->framebuffer,
320 pass->pipeline,
321 0, 0,
322 cogl_texture_get_width (pass->texture),
323 cogl_texture_get_height (pass->texture));
324 }
325
326 static void
clear_blur_pass(BlurPass * pass)327 clear_blur_pass (BlurPass *pass)
328 {
329 g_clear_pointer (&pass->pipeline, cogl_object_unref);
330 g_clear_pointer (&pass->texture, cogl_object_unref);
331 g_clear_object (&pass->framebuffer);
332 }
333
334 /**
335 * clutter_blur_new:
336 * @texture: a #CoglTexture
337 * @sigma: blur sigma
338 *
339 * Creates a new #ClutterBlur.
340 *
341 * Returns: (transfer full) (nullable): A newly created #ClutterBlur
342 */
343 ClutterBlur *
clutter_blur_new(CoglTexture * texture,float sigma)344 clutter_blur_new (CoglTexture *texture,
345 float sigma)
346 {
347 ClutterBlur *blur;
348 unsigned int height;
349 unsigned int width;
350 BlurPass *hpass;
351 BlurPass *vpass;
352
353 g_return_val_if_fail (texture != NULL, NULL);
354 g_return_val_if_fail (sigma >= 0.0, NULL);
355
356 width = cogl_texture_get_width (texture);
357 height = cogl_texture_get_height (texture);
358
359 blur = g_new0 (ClutterBlur, 1);
360 blur->sigma = sigma;
361 blur->source_texture = cogl_object_ref (texture);
362 blur->downscale_factor = calculate_downscale_factor (width, height, sigma);
363
364 if (G_APPROX_VALUE (sigma, 0.0, FLT_EPSILON))
365 goto out;
366
367 vpass = &blur->pass[VERTICAL];
368 hpass = &blur->pass[HORIZONTAL];
369
370 if (!setup_blur_pass (blur, vpass, VERTICAL, texture) ||
371 !setup_blur_pass (blur, hpass, HORIZONTAL, vpass->texture))
372 {
373 clutter_blur_free (blur);
374 return NULL;
375 }
376
377 out:
378 return g_steal_pointer (&blur);
379 }
380
381 /**
382 * clutter_blur_apply:
383 * @blur: a #ClutterBlur
384 *
385 * Applies the blur. The resulting texture can be retrieved by
386 * clutter_blur_get_texture().
387 */
388 void
clutter_blur_apply(ClutterBlur * blur)389 clutter_blur_apply (ClutterBlur *blur)
390 {
391 if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
392 return;
393
394 apply_blur_pass (&blur->pass[VERTICAL]);
395 apply_blur_pass (&blur->pass[HORIZONTAL]);
396 }
397
398 /**
399 * clutter_blur_get_texture:
400 * @blur: a #ClutterBlur
401 *
402 * Retrieves the texture where the blurred contents are stored. The
403 * contents are undefined until clutter_blur_apply() is called.
404 *
405 * Returns: (transfer none): a #CoglTexture
406 */
407 CoglTexture *
clutter_blur_get_texture(ClutterBlur * blur)408 clutter_blur_get_texture (ClutterBlur *blur)
409 {
410 if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
411 return blur->source_texture;
412 else
413 return blur->pass[HORIZONTAL].texture;
414 }
415
416 /**
417 * clutter_blur_free:
418 * @blur: A #ClutterBlur
419 *
420 * Frees @blur.
421 */
422 void
clutter_blur_free(ClutterBlur * blur)423 clutter_blur_free (ClutterBlur *blur)
424 {
425 g_assert (blur);
426
427 clear_blur_pass (&blur->pass[VERTICAL]);
428 clear_blur_pass (&blur->pass[HORIZONTAL]);
429 cogl_clear_object (&blur->source_texture);
430 g_free (blur);
431 }
432