Gaussian blur

Whether you need your protagonist to be drunk or disoriented, a transition effect, or just to stylize a part of your scene, a blur is a good way to create a hazy and indistinct image.

You will learn to:

There are different blur methods. When it comes to games, the two most common are box and Gaussian:

The Gaussian blur is also the base for the common unsharp mask image sharpening filter. You can produce it by subtracting two blurred version of your image, making the edges pop.

In short

When blurring a scene, the pixels surrounding the current point of the image are also sampled then averaged together.

The Gaussian function or distribution was discovered by Carl Friedrich Gauss. It’s a way of sampling those pixels using a bell curve, where the most important pixels are those in the middle of the ‘sampling area.’

Setting up the scene

First, we need a scene to blur. A 3D scene with a sphere or a 2D scene with a sprite will serve our purposes. Then, we need to create the scene where the blurring happens.

As of Godot 3.2, there are two ways of setting up a multi-pass pipeline: Nesting ViewportContainers, or using Viewports and Viewport Textures inside TextureRect nodes. In this tutorial, we’ll use nested ViewportContainers as they are the easiest to make work functionally and the recommended method by Godot’s documentation.

  1. Create a new User Interface scene.
  2. Add a ViewportContainer and name it Blur2.
  3. Enable its Stretch property, and set its Right and Bottom anchors to 1 each.
  4. Assign a new Shader Material to its Material property, and assign a new Shader to that material’s Shader property.
  5. Right click on the new shader and save it to disk. I called mine Gaussian_blur.shader.
  6. Add a Viewport as a child of Blur2.
  7. Add a ViewportContainer as a child of that Viewport and name Blur1.
  8. Enable its Stretch property, and set its Right and Bottom anchors to 1 each.
  9. Assign a new Shader Material to its Material property, and drag gaussian_blur.shader to that material’s Shader property.
  10. Add a Viewport as a child of Blur1.
  11. Instance the scene you want to blur as a child of that Viewport.

Save the scene. Then, from the Scene Menu, click on Reload Saved Scene. This will reload the scene and force all the viewport containers to resize their viewport children. You should now see your instanced scene in the scene view.

Hint: Since Godot 3.2, you can assign a keyboard shortcut to the Reload Saved Scene command to do this without the menus.

We use two blurs instead of one for performance and accuracy. A single pass doing all the blurring has one of two issues:

Two viewports doing the work keeps the performance cost down and gives full accuracy; though it makes your scene more complex and can resist modification and refactor down the line. At the end of the day, it’s up to your preference which compromise you go with.

Coding the shader

Gaussian function

A Gaussian or normal distribution is an equation that produces numbers between two deviations that favor the middle - the mean - by 68%. Only 0.3% will fall at the extreme ends. All we need to do is sample the pixels and average them based on where they fall on this distribution. Thankfully, there’s a simple mathematical equation we can follow for this:

With that done…

Just kidding! It’s not a gentle mathematical concept, but thankfully we don’t need to understand it all that deeply to use it. If you really want, you can learn about it from other sources such as this YouTube video for a lightweight overview, or this article for a more thorough breakdown of its probabilities.

For the purposes of making a Gaussian blur shader, understanding the nitty gritty of the math is unnecessary. We will write a function that will do the job perfectly well.

We create two constants: PI2 which is PI*2 and is the width of our Gaussian deviation, and SAMPLES which is how many pixels will be looked at and used to average the final blurred color. Both are used in the Gaussian equation.

const float SAMPLES = 71.0;
const float PI2 = 6.28319;

float gaussian(float x) {
    float x_squared = x*x;
    float width = 1.0 / sqrt(PI2 * SAMPLES);

    return width * exp((x_squared / (2.0 * SAMPLES)) * -1.0);
}

Two pass blurring

Blurring a scene in one pass can be either expensive or inaccurate, as mentioned above. But we can get the best of both worlds by doing two passes. To control the direction and the amount of blur, we add a vec2 uniform that we’ll call blur_scale.

uniform vec2 blur_scale = vec2(1, 0);

To figure out how far away each sample is from the current pixel, we’ll use TEXTURE_PIXEL_SIZE to get the size of a pixel in UV coordinates and multiply it by our scale.

void fragment() {
    vec2 scale = TEXTURE_PIXEL_SIZE * blur_scale;
}

Now comes the expensive part. We need to get the color information in the texture at each pixel that is scale away from the current pixel, get its contribution based on our Gaussian distribution, and finally average all of those pixels together. We need to keep track of the total weight calculated and the final color. The total weight will be used to get the average of all the pixels.

float weight = 0.0;
float total_weight = 0.0;
vec4 color = vec4(0.0);

Now, we iterate over our samples with half going in the negative direction and half going in the positive direction. We get the weight at the current offset, add its color contribution to our final color, and keep track of how much weight we’ve gathered so far so we can average the total.

for(int i=-int(SAMPLES)/2; i < int(SAMPLES)/2; ++i) {
    weight = gaussian(float(i));
    color.rgb += texture(TEXTURE, UV + scale * vec2(float(i))).rgb * weight;
    total_weight += weight;
}

The final output is the average of all those color samples.

COLOR.rgb = color.rgb / total_weight;

Which leaves the final code:

shader_type canvas_item;

const float SAMPLES = 71.0;
const float PI2 = 6.28319;

uniform vec2 blur_scale = vec2(1, 0);

float gaussian(float x) {
    float x_squared = x*x;
    float width = 1.0 / sqrt(PI2 * SAMPLES);

    return width * exp((x_squared / (2.0 * SAMPLES)) * -1.0);
}

void fragment() {
    vec2 scale = TEXTURE_PIXEL_SIZE * blur_scale;

    float weight = 0.0;
    float total_weight = 0.0;
    vec4 color = vec4(0.0);

    for(int i=-int(SAMPLES)/2; i < int(SAMPLES)/2; ++i) {
        weight = gaussian(float(i));
        color.rgb += texture(TEXTURE, UV + scale * vec2(float(i))).rgb * weight;
        total_weight += weight;
    }

    COLOR.rgb = color.rgb / total_weight;
}

Set Blur1’s Blur Scale parameter to (1, 0) and Blur2’s to (0, 1) to get a nicely uniform, smooth blurred scene.