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

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

Noice

Apprentice
3 years ago

Nice code.

Muhannad
Muhannad
3 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
1 year 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
1 year 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
1 year ago
Reply to  Juulpower

nice! thanks!

Alberto
1 year 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
1 year ago

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

PikacKukac
PikacKukac
1 year 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
11 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 11 months ago by elvisish
yourFAN
yourFAN
10 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
9 months ago

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

Poop Fart
8 months 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 months 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 months 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.

lmao
lmao
7 months ago
Reply to  kcfos

Thanks for the tip! It works for me, not sure if this a best solution for everyone tho, since it scales up the sprite a little bit.

lmao
lmao
7 months ago

Fixed version for godot 4, now it works with modulate:
(it’s the same script with 3 new code strings, all new code has a comment //NEW CODE)

shader_type canvas_item;

varying flat vec4 modulate; // NEW CODE
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() {
   modulate = COLOR; // NEW CODE
   if (add_margins) {
      VERTEX += (sign(VERTEX) * 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) * modulate; // NEW CODE
      }
   } 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;
   }
}

Last edited 7 months ago by lmao
csf
csf
5 months ago
Reply to  lmao

This worked for me but I got error:”Unknown character #8211: ‘–’

I had to replace all of those with “-“

pawlogates
pawlogates
3 months ago

doesnt work with polygon2d i think?