2D outline/inline, configured for CanvasGroup

This shader allows you to add an outline/inline to your Canvas Groups. You can learn more about Canvas Groups here.

The base for the material was taken from Juulpowers shader, which you can get here.

The shader is unmodified, other than to change from UV coordinate space into screen coordinate space.

 

Shader code
shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

uniform vec4 color : source_color = vec4(1.0);
uniform float width : hint_range(0, 10) = 1.0;
uniform int pattern : hint_range(0, 2) = 0; // diamond, circle, square
uniform bool inside = false;
uniform bool add_margins = true; // only useful when inside is false

void vertex() {
	if (add_margins) {
		VERTEX += (UV * 2.0 - 1.0) * width;
	}
}

bool hasContraryNeighbour(vec2 uv, vec2 texture_pixel_size, sampler2D texture) {
	for (float i = -ceil(width); i <= ceil(width); i++) {
		float x = abs(i) > width ? width * sign(i) : i;
		float offset;
		
		if (pattern == 0) {
			offset = width - abs(x);
		} else if (pattern == 1) {
			offset = floor(sqrt(pow(width + 0.5, 2) - x * x));
		} else if (pattern == 2) {
			offset = width;
		}
		
		for (float j = -ceil(offset); j <= ceil(offset); j++) {
			float y = abs(j) > offset ? offset * sign(j) : j;
			vec2 xy = uv + texture_pixel_size * vec2(x, y);
			
			if ((xy != clamp(xy, vec2(0.0), vec2(1.0)) || texture(texture, xy).a == 0.0) == inside) {
				return true;
			}
		}
	}
	
	return false;
}

void fragment() {
	vec2 uv = SCREEN_UV;
	
    vec4 base_color = vec4(0.15);
	
   	if (base_color.a > 0.0001) {
        base_color.rgb /= base_color.a;
    }
	
	vec4 base_color2 = base_color;
	
	if (add_margins) {
		vec2 texture_pixel_size = vec2(1.0) / (vec2(1.0) / SCREEN_PIXEL_SIZE + vec2(width * 2.0));
		
		uv = (uv - texture_pixel_size * width) * SCREEN_PIXEL_SIZE / texture_pixel_size;
		
		if (uv != clamp(uv, vec2(0.0), vec2(1.0))) {
			base_color.a = 0.0;
		} else {
			base_color = textureLod(screen_texture, uv, 0.0);
		}
	} else {
		base_color = textureLod(screen_texture, uv, 0.0);
	}
	
	if ((base_color.a > 0.0) == inside && hasContraryNeighbour(uv, SCREEN_PIXEL_SIZE, screen_texture)) {
		base_color.rgb = inside ? mix(base_color.rgb, color.rgb, color.a) : color.rgb;
		base_color.a += (1.0 - base_color.a) * color.a;
	}
	
	COLOR = base_color;
}
Tags
CanvasGroup, godot4, outline
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

2D outline/inline, configured for sprite sheets

2D outline/inline

2D Outline and Rainbow outline 2 in 1

Subscribe
Notify of
guest

5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Matt
Matt
9 months ago
Reply to  SirLich

Did you ever figure out how to make the outline a consistent width? Having it change on different monitors or if the game is windowed or fullscreen is definitely making this hard to rely on.

Liyaowhen
Liyaowhen
1 year ago

thank you for posting this shader, you really helped me out. It made my game more aesthetic by using a white outline to make it look like paper cutout :). However, it impacts performance severely, is there a way to improve performance?

ArtcadeDev
11 months ago

Thank you so much, this is exactly what I needed.

pavibear
pavibear
3 months ago

I can confirm that it still works after the shader changes in Godot 4.3. I had the issue that it didn’t work when y-sorting was enabled in the CanvasGroup. A workaround is to disable it on the CanvasGroup and add an additional Node2D below the CanvasGroup where y-sorting is enabled.