Fast way to blur image

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
User avatar
Bobble68
Party member
Posts: 162
Joined: Wed Nov 30, 2022 9:16 pm
Contact:

Fast way to blur image

Post by Bobble68 »

Hi all!

I recently implemented a bloom effect into my game, which for the most part works pretty well. The way it works is by making a canvas, drawing the glow layer of sprites onto it and then subtracting away parts that should be blocked by things in front of it, before finally gaussian blurring the whole canvas and drawing it on top. It looks like so:
example.png
example.png (59.73 KiB) Viewed 2958 times

It has a few issues though:

1) Induvidual entities can't have different levels of bloom. It's not really a huge issue, but I would prefer greater control of how the bloom looks

2) I suspect it will look different on different machines. I'm still pretty new to shaders, but I'm pretty sure that the bloom will look different depending on the resoultion you display it as (I tried to implement this shader https://learnopengl.com/Advanced-Lighting/Bloom)

3) The blur hates the edge of the screen. I'm pretty sure I can fix this, but if there's a better solution that doesn't have that issue that would be nice.

4) It's slow. In order to get a larger radius, I have to apply the shader to the canvas multiple times, and that really slows it down. The amount I have at the moment is alright, but it's too subtle for my liking.

This is my current shader code:

Code: Select all

extern vec2 texture_size; 
    extern bool horizontal;
    uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
    
    vec4 effect( vec4 colour, Image texture, vec2 texture_coords, vec2 screen_coords )
    {
      vec4 pixel = Texel(texture, texture_coords) * colour ;
      vec2 tex_offset = 1.0 / texture_size;
      
      vec4 result = pixel*weight[0]; 
     
      if (horizontal) 
      {
        for(int i = 1; i < 5; ++i)
        {
          result += Texel(texture, texture_coords + vec2(tex_offset.x*i, 0), 0) * weight[i];
          result += Texel(texture, texture_coords - vec2(tex_offset.x*i, 0), 0) * weight[i];
        }
      }
      else
      {
        for(int i = 1; i < 5; ++i)
        {
          result += Texel(texture, texture_coords + vec2(0, tex_offset.y*i), 0) * weight[i];
          result += Texel(texture, texture_coords - vec2(0, tex_offset.y*i), 0) * weight[i];
        }
      }
      return result * colour;
    }
I've read that box blurs are supposed to be faster, but I can't really work out why that would be since it's basically the same thing but with equal weights.
Dragon
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: Fast way to blur image

Post by Bigfoot71 »

To adjust the blur radius in your case, you will need to change the pre-calculated kernel that you are using (weight), either by using a larger matrix or by using a shader that does not use a pre-calculated matrix, which will be slower. Here's an example I made based on yours and one I made before. I tried to optimize it so that it has roughly the same performance as the one you presented. If it doesn't suit your needs, I have others in stock ^^

Code: Select all

    extern vec2 tex_size;
    uniform float blur_radius;

    vec4 effect(vec4 c, Image tex, vec2 tc, vec2 sc)
    {
        vec2 offset = 1.0 / tex_size;
        float sigma = blur_radius / 3.0;
        int num_samples = int(blur_radius * 2.0);

        vec4 sum = vec4(0.0);
        vec4 blurred_pixel = vec4(0.0);

        // Combined horizontal and vertical pass
        for (int i = -num_samples; i <= num_samples; i++) {
            vec2 offset_i = vec2(float(i) * offset.x, float(i) * offset.y);
            float weight = exp(-0.5 * pow(length(offset_i) / sigma, 2.0));
            sum += Texel(tex, tc + offset_i) * weight;
        }
        blurred_pixel = sum / (float(num_samples) * 2.0 + 1.0);

        return blurred_pixel * c;
    }
After the performances with this type of shader it's clear that it's sensitive, for example this shader and yours both run at ~20 fps on my machine, after it at 15 years old, ok I overdo it a little and i have a lot of stuff open :ultrahappy:

Otherwise theoretically if you only work with UV (texture coordinates) there should be no differences. Calculations on normalized coordinates are used precisely to be independent of the resolution.

As far as performance differences are concerned, Box Blur is faster than Gaussian Blur because it uses a simple arithmetic average to blur pixels in an image, while Gaussian Blur uses a Gaussian function that needs to be calculated for each pixel in the blur area, so no it's not really the same thing.

You can try moonshine in case you don't know, try the different effects and see what you think ^^

Btw, if you only make the shader work on the resolution of the screen you have the `love_ScreenSize.xy` variable (it's a vec4), this will avoid an additional uniform which can slow down the shader (well that's fine but that's good to know)
My avatar code for the curious :D V1, V2, V3.
User avatar
pgimeno
Party member
Posts: 3691
Joined: Sun Oct 18, 2015 2:58 pm

Re: Fast way to blur image

Post by pgimeno »

Bigfoot71 wrote: Sat Apr 08, 2023 1:21 am As far as performance differences are concerned, Box Blur is faster than Gaussian Blur because it uses a simple arithmetic average to blur pixels in an image, while Gaussian Blur uses a Gaussian function that needs to be calculated for each pixel in the blur area, so no it's not really the same thing.
The thing with Box Blur is that when applied repeatedly, it quickly converges to Gaussian Blur. It will never be the same but it will be close enough to make them undistinguishable.

The problem is the number of passes. Three passes will probably be decent for this purpose. Even two passes might suffice, it's a question of trying.

See http://nghiaho.com/?p=1159
User avatar
Sasha264
Party member
Posts: 131
Joined: Mon Sep 08, 2014 7:57 am

Re: Fast way to blur image

Post by Sasha264 »

Bobble68 wrote: Thu Apr 06, 2023 9:17 am 4) It's slow. In order to get a larger radius, I have to apply the shader to the canvas multiple times, and that really slows it down. The amount I have at the moment is alright, but it's too subtle for my liking.
If you want a larger visual radius for the Gaussian blur effect, I can think of three ways to achieve that (plus 4th method to just increase performance with the same blur radius):
  1. Direct method: Calculate another set of weights for a larger Gaussian radius, so that the weights for farther pixels will be greater. But you will also need to increase the number of precalculated weights, so that the last weight will be less than 0.01 or something similar. The performance will be inversely proportional to the number of weights. This will produce perfect results in terms of quality.
  2. Apply the same blur multiple times: As pgimeno mentioned earlier, the resulting blur will eventually converge to a Gaussian blur anyway, so it may be better to use a box blur as a single operation. This is not because it is faster than Gaussian blur (in your implementation it is almost the same), but because with the same radius in pixels, it will "blur more". Applying 2-3 (maybe max 4) times box blur will visually look almost the same as a decent Gaussian blur.
  3. Resize the input image down before blurring: One possible way to do this quickly is to draw one canvas to another with smaller sizes. However, there is a simple method to achieve the same effect as image resizing with your shader. You can just change

    Code: Select all

    vec2 tex_offset = 1.0 / texture_size;
    to something like

    Code: Select all

    vec2 tex_offset = radius_multiplier / texture_size;
    This will be generally the same as resizing your image down by a factor of radius_multiplier using nearest neighbor method. A value of radius_multiplier between 1.5 and 5.0 may be appropriate. There is a sweet point when the radius_multiplier is equal to the size of your pixel graphics, which appears to be 3.0 in your image. So, basically, you can be satisfied by just changing 1.0 to 3.0 in your shader :3
  4. On the other hand, you can reduce the size of the resulting blurred image by 2-4 times. This will further increase performance by about 4-16 times. And then bilinearly sample the result downscaled image up to the original size when applying it as a bloom effect source.
You can implement any combination of these methods. For example, if you use a combination of the third method with a factor of 3, and the forth method with a factor of 3, then the resulting performance can be improved up to 81 times (3x3 x 3x3). Of course, it may not be achieved in practice, but with a large enough incoming image size, it will not be far from that. And I think the corresponding visual defects will be minimal.
Last edited by Sasha264 on Sun Apr 09, 2023 12:36 am, edited 6 times in total.
User avatar
Sasha264
Party member
Posts: 131
Joined: Mon Sep 08, 2014 7:57 am

Re: Fast way to blur image

Post by Sasha264 »

Bobble68 wrote: Thu Apr 06, 2023 9:17 am 3) The blur hates the edge of the screen. I'm pretty sure I can fix this, but if there's a better solution that doesn't have that issue that would be nice.
It's hard to say without a visual explanation of the mentioned bug, but I'm 99% sure it can be solved by changing the WrapMode of the input image or canvas.
Bobble68 wrote: Thu Apr 06, 2023 9:17 am 2) I suspect it will look different on different machines. I'm still pretty new to shaders, but I'm pretty sure that the bloom will look different depending on the resoultion you display it as (I tried to implement this shader https://learnopengl.com/Advanced-Lighting/Bloom)
I'm not sure how you're handling different resolutions in terms of scaling your pixel graphics, but the main rule here is that your blur radius should be proportional to your "visible pixel size". For example, if you're scaling your game in such a way that there are always 200 visual pixels in the height of a window, then your visible_pixel_size can be defined as windowHeight (in actual monitor pixels) / 200, which would be something like 1080/200 = 5.4 for a common display. It would be better to round it to 5.0 in order to preserve even sizes of every visual pixel in your graphics. Then you just change your blur radius to something like 15 * 5.0 (which would be 15 * 10.0 for a 4k display, for example). Than it should look the same.

Another way to achieve a similar effect is by rendering your entire game in your visible pixel resolution. For example, you can fix your target height to something like 200 pixels (and calculate your target width based on the chosen height and the actual display proportions), draw all pixel graphics in that resolution, apply all effects in that resolution, and only then scale the result to 1920x1080 or whatever actual display resolution you have. However, this approach comes with several complications:
  • Rounding issue: It's best to scale up images only with integer scales, like 5.0 in the previous example instead of 5.4. This leads to somewhat complicated calculations of the target height depending on the actual window height. Something like

    Code: Select all

    local displayHeight = 1080 -- actual display height
    local targetHeight = displayHeight / math.round(displayHeight / 200)
    may be appropriate, but it's debatable, because 1) displays not only differs in terms of pixels, they differs in terms of inches also 2) your game will have slightly different visible area on different monitors, which can be hard to handle.
  • The same visual pixel size for blur/bloom/other shader effects. This means that your bloom effect won't have a smooth gradient but will consist of pixels with the size of your graphic pixels. However, this could be even considered a good thing from an artistic point of view.
  • The problem with moving objects (or full-screen slow movement). This is perhaps the biggest issue. When there are smoothly moving objects in pixel graphics, especially with some small angle of movement, it looks much better if they move with display-pixel-size granularity, not target-pixel-size granularity. That way, they will move smoothly, not jump by ladders times to times, like "right-right-right-up-right-right-up-right-right-right...".
RNavega
Party member
Posts: 418
Joined: Sun Aug 16, 2020 1:28 pm

Re: Fast way to blur image

Post by RNavega »

There's an optimization that reduces the number of sampling taps required, making use of the hardware bilinear filtering of textures. If you need a linear mix between two texels, instead of sampling each texel then doing the mix() manually, you can sample the UV point between them:
https://www.rastergrid.com/blog/2010/09 ... -sampling/
User avatar
Bobble68
Party member
Posts: 162
Joined: Wed Nov 30, 2022 9:16 pm
Contact:

Re: Fast way to blur image

Post by Bobble68 »

Ah I had kinda forgotten about this post I've been a bit busy with an assignment 😅 (man I hate C++), thank you all for the helpful advice! I'll probably look back at it when I've got time, sorry for not replying at all.
Dragon
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Bing [Bot], Google [Bot] and 7 guests