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;
	}
}
Tags
2d, border, configurable, inline, outline, sprite sheet, sprite sheets, 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

Related shaders

2D outline/inline, configured for CanvasGroup

2D outline/inline

2D Outline and Rainbow outline 2 in 1

Subscribe
Notify of
guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
sky94520
sky94520
3 months ago

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

drisicus
3 months ago

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);

Taylor Anderson
Taylor Anderson
1 month ago

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?

admin
Admin
1 month ago

This should be fixed now