2D outline/inline, configured for sprite sheets
Extended version of my 2D outline/inline shader that supports sprite sheets as well. Adds one uniform for specifying the number of horizontal and vertical images in the sprite sheet. Might require some tweaking in the vertex shader when adding margins, as the code below assumes the texture to +- centered. If your images contain a few semi-transparent pixels from adjacent images, try increasing the 0.0 at lines 33 and 61 (I used 0.1 in the GIF).
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
uniform vec2 number_of_images = vec2(1.0); // number of horizontal and vertical images in the sprite sheet
void vertex() {
if (add_margins) {
VERTEX += sign(VERTEX) * width; // replace sign(VERTEX) by (sign(VERTEX) * 2.0 - 1.0) if not Centered
}
}
bool hasContraryNeighbour(vec2 uv, vec2 texture_pixel_size, vec2 image_top_left, vec2 image_bottom_right, 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, image_top_left, image_bottom_right) || texture(texture, xy).a <= 0.0) == inside) {
return true;
}
}
}
return false;
}
void fragment() {
vec2 uv = UV;
vec2 image_top_left = floor(uv * number_of_images) / number_of_images;
vec2 image_bottom_right = image_top_left + vec2(1.0) / number_of_images;
if (add_margins) {
vec2 texture_pixel_size = vec2(1.0) / (vec2(1.0) / TEXTURE_PIXEL_SIZE + vec2(width * 2.0) * number_of_images);
uv = (uv - texture_pixel_size * width - image_top_left) * TEXTURE_PIXEL_SIZE / texture_pixel_size + image_top_left;
if (uv != clamp(uv, image_top_left, image_bottom_right)) {
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, image_top_left, image_bottom_right, TEXTURE)) {
COLOR.rgb = inside ? mix(COLOR.rgb, color.rgb, color.a) : color.rgb;
COLOR.a += (1.0 - COLOR.a) * color.a;
}
}
Hey, Thank your shader, but I found it doesn’t work while I use modulate. I am new for godot shader, could you please help me what should to do?
Best regards
This is a general problem with Godot 4. Engine contributors are working on it, but meanwhile I’ve added a workaround to the description!
i just posted a solution in previous 2d outline/inline topic.
Nice work!
For people using Godot 4 replace
uniform vec4 color : hint_color = vec4(1.0);
with
uniform vec4 color : source_color = vec4(1.0);
Sorry, I don’t think I understand how this shader works. When I bring it in unchanged there’s tons of errors, a chunk of the shader missing, and I don’t see anywhere that actually uses this number_of_images variable. Did something happen to this shader?
Yeah it’s messed up and it happens again when I reupload the original code. I’ve notified the website creator, thank you for notifying me!
This should be fixed now
can we have the ability to configure what sides the outline is on like in asperite?
I guess you could try changing one of these values in
hasContraryNeighbour
to 0:-ceil(width)
,ceil(width)
,-ceil(offset)
orceil(offset)
🙂