2D outline/inline

Adds an outer or inner stroke to a texture. Just attach the shader to a material and the material to a CanvasItem, like a Sprite. This shader is robust and configurable (color, width, pattern, placement, and the ability to add margins to the texture to make room for an outline). However, for textures with anti-aliased edges this shader might provide a better result.

For Godot 3, replace source_color at line 3 by hint_color.

For Godot 4, if you also want to use the modulate property, you can incorporate this solution for now and then replace texture(TEXTURE, uv) by texture(TEXTURE, uv) * modulate twice, color.rgb (case-sensitive) by color.rgb * modulate.rgb twice, and color.a (case-sensitive) by color.a * modulate.a twice.

 

Shader code
shader_type canvas_item;

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  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  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 = UV;
	
	if (add_margins) {
		vec2 texture_pixel_size = vec2(1.0) / (vec2(1.0) / TEXTURE_PIXEL_SIZE + vec2(width * 2.0));
		
		uv = (uv - texture_pixel_size * width) * TEXTURE_PIXEL_SIZE / texture_pixel_size;
		
		if (uv != clamp(uv, vec2(0.0), vec2(1.0))) {
			COLOR.a = 0.0;
		} else {
			COLOR = texture(TEXTURE, uv);
		}
	} else {
		COLOR = texture(TEXTURE, uv);
	}
	
	if ((COLOR.a > 0.0) == inside && hasContraryNeighbour(uv, TEXTURE_PIXEL_SIZE, TEXTURE)) {
		COLOR.rgb = inside ? mix(COLOR.rgb, color.rgb, color.a) : color.rgb;
		COLOR.a += (1.0 - COLOR.a) * color.a;
	}
}
Tags
2d, border, configurable, inline, outline, stroke
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.

More from Juulpower

2D outline/inline, configured for sprite sheets

Related shaders

2D outline/inline, configured for sprite sheets

2D outline/inline, configured for CanvasGroup

2D Outline and Rainbow outline 2 in 1

Subscribe
Notify of
guest

19 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ubX
ubX
3 years ago

Noice

Apprentice
3 years ago

Nice code.

Muhannad
Muhannad
2 years ago

great! thanks

morningking
morningking
2 years ago

thanks.

trackback

[…] to start in creating a shader to do that so I did a quick search on the ol’ Googs and found this shader that did exactly what I needed! So giving credit where credit is due, thank you Juulpower for your […]

PudimLaranja
PudimLaranja
1 year ago

That’s exactly what i needed thank you so much 🙂

Gavin
Gavin
9 months ago

I know it’s been awhile since you made this post, but might I ask if there’s a way to make the shader enclose around the sprite to a greater extent, thanks

Alberto
Alberto
8 months ago

nice!. Just one question, Is it possible to make it compatible with a sprite sheet? I notice that if you enable add_margins, the sprite aren’t show correctly. (At least in godot 4)

Alberto
4 months ago
Reply to  Juulpower

nice! thanks!

Alberto
4 months ago
Reply to  Juulpower

i made a little modification. I use this shader when the player is near to an item in the map, and then i apply the outline effect to indicate it’s ‘selected’ to pick up. But, when the item is over the player, i modify the modulate of the item to see the player again. With this shader it’s not possible, but i made it possible with this modification.

Add a new shader parameter:

uniform float global_alpha_color: hint_range(0.0, 1.0, 0.01) = 1.0; 

Edit the “IF” in the fragment:

	if ((COLOR.a > 0.0) == inside && hasContraryNeighbour(uv, TEXTURE_PIXEL_SIZE, TEXTURE)) {
		COLOR.rgb = inside ? mix(COLOR.rgb, color.rgb, color.a) : color.rgb;
		COLOR.a += (1.0 - COLOR.a) * (color.a * global_alpha_color);
	}
	else if (COLOR.a > 0.0)
	{
		COLOR.a = COLOR.a * global_alpha_color;
	}
jamieyello
3 months ago

Great shader. I had to change hint_color to source_color in Godot 4.2 to make this work.

PikacKukac
PikacKukac
3 months ago

Great work! The issue is when I put an image that is smaller, for example rod, the rectangle at the middle is still visible. How would I remove this rectangle? Thanks!

elvisish
1 month ago

This is an amazing shader, is it possible to keep the outline and set the alpha of the sprite?

Edit: I managed to somehow make this do it: https://godotshaders.com/shader/2d-outline-inline/#comment-1060

Last edited 1 month ago by elvisish
yourFAN
yourFAN
1 month ago

You are my God and savior! 
I’m modifying the original sprite to be clear and using it as an outline to mark the other side of the obstacle. I wish you prosperity.

north
north
2 days ago

It seems there is something wrong with lines 16 and 27?