2D outline/inline

Adds an outer or inner stroke to a texture. Just attach the shader to a material and the material to a canvas item 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 <= 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 = 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

24 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
10 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
9 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
6 months ago
Reply to  Juulpower

nice! thanks!

Alberto
6 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
5 months ago

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

PikacKukac
PikacKukac
4 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
3 months 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 3 months ago by elvisish
yourFAN
yourFAN
2 months 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
1 month ago

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

Poop Fart
24 days ago

Do you have any idea why this may not be working with a polygon 2d? If I use a black color, even with the lowest possible width above 0, the entire polygon becomes black. Thanks

kcfos
kcfos
7 days ago

When outlining with add_margins:
Setting a texture_rect’s “flip_H” to true will mess up the sprite, it doesn’t correctly expand the width.
Not sure why flipH is doing this, I don’t know what flipH is actually doing behind the scenes

kcfos
kcfos
7 days ago
Reply to  kcfos

Actually the author may have found a solution in their sprite sheet shader:
Replacing:

VERTEX += (UV * 2.0 - 1.0) * width;

With:

VERTEX += (sign(VERTEX) * 2.0 1.0) * width;

Seems to work.