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;
}
}
Noice
Nice code.
great! thanks
thanks.
[…] 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 […]
That’s exactly what i needed thank you so much 🙂
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
If I understand correctly: yes, by increasing the 10 at line 4!
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)
Took me a few hours but here it is! https://godotshaders.com/shader/2d-outline-inline-configured-for-sprite-sheets/
nice! thanks!
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:
Edit the “IF” in the fragment:
Thank you! I’ve just added a more general workaround to the description.
Great shader. I had to change hint_color to source_color in Godot 4.2 to make this work.
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!
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
I’ve added a workaround to the description for working with
modulate
in Godot 4 🙂 If you only replacetexture(TEXTURE, uv)
the outline itself will not be modulated.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.
It seems there is something wrong with lines 16 and 27?
Yes there was, sorry, should be fixed now!
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
I’ve had a look and to be honest this shader is very dependent on the presence of a texture. Without one you don’t even have access to
UV
. I could get this version working with a texture, by setting thenumber_of_images
uniform to the texture size divided by the polygon’s bounding box size, but even then only for a rectangular polygon without texture scaling or rotation…Looking at it online,
Polygon2D
outlines can only be done properly by rendering it to a CanvasGroup orSubViewport
, or by drawing a polyline or second polygon around/behind it manually.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
Actually the author may have found a solution in their sprite sheet shader:
Replacing:
With:
Seems to work.
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.
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;
}
}
This worked for me but I got error:”Unknown character #8211: ‘–’
I had to replace all of those with “-“
doesnt work with polygon2d i think?
Correct, see my reply to Poop Fart above.