A shader that creates a double outline?

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

I’m trying to create a dark outline outside and a brighter inside for a polygon2D, for this I’m using nested viewports each containing a shader, one for the inline, other for the outline, and the last for pixealizing the polygon.

Here’s my node setup:

Setup

And here’s the final result:
Result

But having 2 of these for each polygon and outline color is becoming expensive, how could I compress the two outline shaders into a single, double-color outline?

This is the outline shader:

shader_type canvas_item;

const vec4  outline_color = vec4(0.10, 0.01, 0.01, 1.00);
const float outline_scale = 1.0;

bool has_contrary_neighbour(vec2 uv, vec2 texture_pixel_size, sampler2D texture)
{
	for (float i = -ceil(outline_scale); i <= ceil(outline_scale); i++)
	{
		float x = abs(i) > outline_scale ? outline_scale * sign(i) : i;
		float offset = outline_scale - abs(x);
		
		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)) { return true; }
		}
	}
	return false;
}

void fragment()
{
	vec2 uv = UV;
	
	COLOR = texture(TEXTURE, uv);
	
	if ((COLOR.a > 0.0) && has_contrary_neighbour(uv, TEXTURE_PIXEL_SIZE, TEXTURE))
	{
		COLOR.rgb = outline_color.rgb;
		COLOR.a  += (1.0 - COLOR.a) * outline_color.a;
	}
}

And this is the inline shader:

shader_type canvas_item;

const vec4  primary_color   = vec4(0.8, 0.2, 0.08, 1.0);
const vec4  secondary_color = vec4(0.6, 0.06, 0.15, 1.0);
const float outline_scale   = 2.0;

bool has_contrary_neighbour(vec2 uv, vec2 texture_pixel_size, sampler2D texture)
{
	for (float i = -ceil(outline_scale); i <= ceil(outline_scale); i++)
	{
		float x = abs(i) > outline_scale ? outline_scale * sign(i) : i;
		float offset = floor(sqrt(pow(outline_scale + 0.5, 2) - x * x));
		
		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)) { return true; }
		}
	}
	return false;
}

void fragment()
{
	vec2 uv = UV;
	
	COLOR = texture(TEXTURE, uv);
	
	if ((COLOR.a > 0.0) && has_contrary_neighbour(uv, TEXTURE_PIXEL_SIZE, TEXTURE))
	{
			if (UV.y < 0.5)
			{
				COLOR.rgb = mix(COLOR.rgb, secondary_color.rgb, secondary_color.a);
				COLOR.a += (1.0 - COLOR.a) * secondary_color.a;
			}
			else if (UV.y > 0.5)
			{
				COLOR.rgb = mix(COLOR.rgb, primary_color.rgb, primary_color.a);
				COLOR.a += (1.0 - COLOR.a) * primary_color.a;
			}
	}
}
:bust_in_silhouette: Reply From: Niko

For anyone who may search for this later, I come with this triple shader that has outline and inline in the same shader, you can tweak the colors from both in the inspector, this shader also comes with a flash effect for convenience.
Shader

Polygon

The pixel art aspect comes from the low-resolution viewports, combined with the polygons you can get some fake 2D mesh’s, the shader needs some optimizations but for me the performance was great, here’s the code:

shader_type canvas_item;

uniform float brightness : hint_range(0.00, 1.00) = 0.00;
uniform vec4  highlight  : hint_color = vec4(0.80, 0.20, 0.08, 1.00);
uniform vec4  shadow     : hint_color = vec4(0.60, 0.06, 0.15, 1.00);
uniform vec4  outline    : hint_color = vec4(0.10, 0.01, 0.01, 1.00);

vec3 first_pass(vec2 uv, vec2 pixel_size, sampler2D sprite, vec3 input_color)
{
	vec3  target_color  = uv.y < 0.5   ? shadow.rgb : highlight.rgb;
	float color_scale   = 2.0;
	
	for (float i = -ceil(color_scale); i <= ceil(color_scale); i++)
	{
		float x = abs(i) > color_scale ? color_scale * sign(i) : i;
		float offset = color_scale - abs(x);
		
		for (float j = -ceil(offset); j <= ceil(offset); j++)
		{
			float y = abs(j) > offset ? offset * sign(j) : j;
			vec2 xy = uv + pixel_size * vec2(x, y);
			
			if ((texture(sprite, xy).a == 0.0)) { return mix(input_color, target_color, 1.0); }
		}
	}
	return input_color;
}

vec3 second_pass(vec2 uv, vec2 pixel_size, sampler2D sprite, vec3 input_color)
{
	for (float i = -ceil(1.0); i <= ceil(1.0); i++)
	{
		float x = abs(i) > 1.0 ? 1.0 * sign(i) : i;
		float offset = 1.0 - abs(x);
		
		for (float j = -ceil(offset); j <= ceil(offset); j++)
		{
			float y = abs(j) > offset ? offset * sign(j) : j;
			vec2 xy = uv + pixel_size * vec2(x, y);
			
			if ((texture(sprite, xy).a == 0.0)) { return mix(input_color, outline.rgb, 1.0); }
		}
	}
	return input_color;
}

vec3 third_pass(vec3 color)
{
	return mix(color.rgb, vec3(1.0), brightness);
}

void fragment()
{
	COLOR     = texture(TEXTURE, UV);
	COLOR.rgb = first_pass(UV, TEXTURE_PIXEL_SIZE, TEXTURE, COLOR.rgb);
	COLOR.rgb = second_pass(UV, TEXTURE_PIXEL_SIZE, TEXTURE, COLOR.rgb);
	COLOR.rgb = third_pass(COLOR.rgb);
}

Flash