How to write a bloom shader?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Diet Estus

I am working on a 2D game and want to implement a simple bloom shader, like the one described here. The built-in glow setting in the World Environment doesn’t work for 2D in Godot, which means I need to create my own bloom shader.

I know that I should use shader_type canvas_item.

I know that I need to sample my scene’s current texture, probably using texture(TEXTURE, UV).

I also need to create a new texture from this one, isolating only the brightest parts. To do this, I probably need to use sampler2D(), though I never used it before.

Then I need to blur this new texture and combine it with my original texture.

So, I understand the process in general.

The problem is that I don’t know how to implement it. I have only ever made a simple outline shader and a simple vignette shader.

I don’t even know to which node I should attach the shader. Maybe I need to use Godot’s built-in screen-reading capability, described here?

Any help on implementing bloom?

:bust_in_silhouette: Reply From: Zylann

Have you check the HDR 2D demo? https://github.com/godotengine/godot-demo-projects/tree/master/2d/hdr
It shows working glow and bloom in 2D.

Thanks, I will check it out! That said, I would still like to learn how to do it with a shader, just to learn.

Diet Estus | 2018-07-23 14:17

So you can create a fullscreen quad (ColorRect with anchors set to wide), and put a shader material on it.
With a shader, blur is done by looking 4 neighbors of the current pixel and averaging them:

shader_type canvas_item;

void fragment() {
	vec2 ps = SCREEN_PIXEL_SIZE;
	vec4 col0 = texture(SCREEN_TEXTURE, SCREEN_UV + vec2(-ps.x, 0));
	vec4 col1 = texture(SCREEN_TEXTURE, SCREEN_UV + vec2(ps.x, 0));
	vec4 col2 = texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0, -ps.y));
	vec4 col3 = texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0, ps.y));
	COLOR = 0.25 * (col0 + col1 + col2 + col3);
}

To achieve glow, instead of just sampling SCREEN_TEXTURE colors, you would subtract a threshold to them, called hdr_threshold (negative result must be clamped to zero). The lower this threshold, the more glow will happen on less bright objects.
Then you add that to the normal pixel color instead of replacing it:

shader_type canvas_item;

vec4 sample_glow_pixel(sampler2D tex, vec2 uv) {
	float hdr_threshold = 1.0; // Pixels with higher color than 1 will glow
	return max(texture(tex, uv) - hdr_threshold, vec4(0.0));
}

void fragment() {
	vec2 ps = SCREEN_PIXEL_SIZE;
	// Get blurred color from pixels considered glowing
	vec4 col0 = sample_glow_pixel(SCREEN_TEXTURE, SCREEN_UV + vec2(-ps.x, 0));
	vec4 col1 = sample_glow_pixel(SCREEN_TEXTURE, SCREEN_UV + vec2(ps.x, 0));
	vec4 col2 = sample_glow_pixel(SCREEN_TEXTURE, SCREEN_UV + vec2(0, -ps.y));
	vec4 col3 = sample_glow_pixel(SCREEN_TEXTURE, SCREEN_UV + vec2(0, ps.y));
	
	vec4 col = texture(SCREEN_TEXTURE, SCREEN_UV);
	vec4 glowing_col = 0.25 * (col0 + col1 + col2 + col3);
	
	COLOR = vec4(col.rgb + glowing_col.rgb, col.a);
}

However this alone is too subtle, as the blur only spans the neighbor pixels in a radius of 1, which is not much. We could sample more pixels around, but that would become very expensive.
So what we can do is to exploit textureLod on a lower mipmap. The advantage here is speed, because in that case the mipmaps of the texture combined with the bilinear filter work on hardware level in a single sample, effectively blurring the result.
So we can replace the little function above by this, for example using LOD 2:

vec4 sample_glow_pixel(sampler2D tex, vec2 uv) {
	float hdr_threshold = 0.1; // Exagerated, almost everything will glow
	return max(textureLod(tex, uv, 2.0) - hdr_threshold, vec4(0.0));
}

And you will notice quite a big difference :slight_smile:
You could even get rid of the 4-sampling, since that’s also working in a single call.
Then you can also compose multiple LOD levels to tweak how glow is spreading.

And you will notice… that Godot may be using a very similar approach, since some glow parameters I talked about are present in WorldEnvironment :wink:

Zylann | 2018-07-23 22:03

I appreciate this so much! You just taught me a bunch. Thanks again.

Diet Estus | 2018-07-24 03:50