[SOLVED] 2D SHADER: How to apply a margin to a texture before applying an outline?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Erwin Broekhuis

Is it possible to create margins around a texture to ensure a sprite always has enough room for an effect like an outline?

I am currently using the outline shader by GDQuest but my assets don’t always have the room to display the outline.

shader_type canvas_item;

uniform vec4 line_color : hint_color = vec4(1.0);
uniform float line_thickness : hint_range(0, 10) = 1.0;

const vec2 OFFSETS[8] = {
	vec2(-1, -1), vec2(-1, 0), vec2(-1, 1), vec2(0, -1), vec2(0, 1), 
	vec2(1, -1), vec2(1, 0), vec2(1, 1)
};

void fragment() {
	vec2 size = TEXTURE_PIXEL_SIZE * line_thickness;
	float outline = 0.0;
	
	for (int i = 0; i < OFFSETS.length(); i++) {
		outline += texture(TEXTURE, UV + size * OFFSETS[i]).a;
	}
	outline = min(outline, 1.0);
	
	vec4 color = texture(TEXTURE, UV);
	COLOR = mix(color, line_color, outline - color.a);
}
:bust_in_silhouette: Reply From: Erwin Broekhuis

Via Mathichai Dechdee on our Facebook group I received this excellent answer with explanations:

shader_type canvas_item;

uniform vec4 line_color: hint_color = vec4(1.0);
uniform float width: hint_range(0,10)  = 2.0;

const vec2 OFFSETS[8] = {
	vec2(-1,-1), vec2(-1,0), vec2(-1,1), vec2(0,-1),
	vec2(0,1), vec2(1,-1), vec2(1,0), vec2(1,1)
};

void vertex(){
	// start with adding margin to the original sprite
	// this will scale up the sprite, will scale down later in fragment()
	VERTEX += (UV * 2.0 - 1.0) * width ;
}

void fragment(){
	// note that TEXTURE_PIXEL_SIZE is actually 1.0/vec2(WIDTH_OF_TEXTURE, HEIGHT_OF_TEXTURE)
	// so 1.0 / TEXTURE_PIXEL_SIZE is vec2(WIDTH, HEIGHT)
	vec2 real_texture_size = 1.0 / TEXTURE_PIXEL_SIZE;
	
	// This is texture size when add margin equal to 
	// width of the outline*2 (left and right / top and down)
	vec2 added_margin_texture_pixel_size = 1.0 / (real_texture_size + (width*2.0));
	
	// width in range (0,1), respected to the new texture size
	vec2 width_size = added_margin_texture_pixel_size * width;
	// shift the original uv bottom-right for 'width' unit 
	// Calculate how much bigger is the new size compared to the old one
	vec2 shifted_uv = UV - width_size;
	// Then scale the uv down to that ratio
	vec2 ratio = TEXTURE_PIXEL_SIZE / added_margin_texture_pixel_size;
	vec2 scaled_shifted_uv = shifted_uv * ratio;
	
	// sample the original texture with new uv to scale it down
	// to the original size
	vec4 color;
	color = texture(TEXTURE, scaled_shifted_uv);
	// This if is to remove artifacts outside the boundary of sprites
	if (scaled_shifted_uv != clamp(scaled_shifted_uv, vec2(0.0), vec2(1.0))) {
		color.a = 0.0;
	}
	
	float outline = 0.0;
	for (int i=0; i<OFFSETS.length(); i++){
		outline += texture(TEXTURE, scaled_shifted_uv + OFFSETS[i]*width_size).a;
	}
	outline = min(outline, 1.0);
	
	COLOR = mix(color, line_color, outline-color.a);
}
1 Like

I created a simple Node script for this

Screenshot 2025-02-11 at 5.01.08 PM

class_name AddTextureMargin
extends Node

@export var target: Sprite2D
@export_range(0, 50, 1, "or_greater") var margin_size: int = 15:
	set(ms):
		margin_size = ms
		if target and target.is_node_ready():
			add_margin(ms)


func _ready() -> void:
	if not target and get_parent() is Sprite2D:
		target = get_parent()
	else:
		push_error("Must set target for AddTextureMargin")
		return

	add_margin(margin_size)


func add_margin(size: int) -> void:
	if not target or not target.texture:
		return

	var original_texture: Texture2D = target.texture
	var image := original_texture.get_image()
	
	# Calculate new dimensions
	var new_width := image.get_width() + (2 * size)
	var new_height := image.get_height() + (2 * size)
	
	# Create new image with transparency
	var new_image := Image.create(new_width, new_height, false, Image.FORMAT_RGBA8)
	new_image.fill(Color(0, 0, 0, 0))  # Fill with transparent color

	# Copy original image to new position with offset
	new_image.blit_rect(image, Rect2i(0, 0, image.get_width(), image.get_height()),
		Vector2i(size, size))

	# Create and assign new texture
	var new_texture := ImageTexture.create_from_image(new_image)
	target.texture = new_texture
1 Like