Getting Screen Size of Pixels in Shader

Godot Version

Godot 4.3

Question

I’m trying to make a shader that creates a border around the shaded object, with a specific width in pixels. Here’s my shader right now:

shader_type canvas_item;

uniform float border_width:hint_range(0.0, 64, 1.0) = 0.0;
uniform vec3 border_color:source_color = vec3(0.0);

void vertex()
{
	//Called for every vertex the material is visible on.
}

void fragment()
{
	//Multiply desired border thickness by the UV size of a single pixel
	vec2 border_uv = SCREEN_PIXEL_SIZE * border_width;
	//We want to paint a border around the edges
	if (UV.x <= border_uv.x
		|| UV.y <= border_uv.y
		|| 1.0 - UV.x <= border_uv.x
		|| 1.0 - UV.y <= border_uv.y)
	{
		COLOR.rgb = border_color;
	}
}

For some reason, this doesn’t quite work. This is what I’m getting in the game:

example

There are two problems:

  • The border isn’t the right size; border_width is set to 64, so I’d expect it to be 64 pixels thick, but this is a full screen grab and the borders are far less than 64 pixels in thickness.
  • The border isn’t the same size on all sides. I’d expect the X and Y borders to match, but instead for example the left border is 13 pixels wide, and the top border is 10 pixels tall.

I figure I’m misunderstanding SCREEN_PIXEL_SIZE here. How should I go about creating a border that’s measured in exact screen pixels?

For reference, this is how SCREEN_PIXEL_SIZE is described in Godot Docs:

As an example, a resolution of 1920x1080 will produce a value of:

SCREEN_PIXEL_SIZE.x = 1 / 1920 = 0.00052
SCREEN_PIXEL_SIZE.y = 1 / 1080 = 0.00092

The value of the built-in SCREEN_PIXEL_SIZE is relative to the size of the screen (i.e. window or viewport). This means that the aspect ratio of SCREEN_PIXEL_SIZE is the same as that of the viewport (e.g. 16:9). Using the UVs of a 2d sprite that does not share the aspect ratio of the screen will result in distortion – in your case, distorted borders.

You can try to use the more specific TEXTURE_PIXEL_SIZE. Although, without having tried it myself, I believe this will prohibit you from scaling the object as that changes the frame of reference which also causes distortion.

I’m not sure what the best approach is for a per-object outline shader. The outlines I’ve seen in other projects and tutorials are mostly produced via a post-processing shader, or they use the fresnel of the mesh which is not applicable here.

There is a technique which involves using the alpha channel of the sprite and a kernel to produce an outline. However, this technique requires that your sprites contain a margin of transparency i.e. the sprite cannot be touching the edge of the image. Should you choose to use this approach, it would require the use of the UV built-in and the SCREEN_PIXEL_SIZE built-in which you’re already using.
(Video tutorial example, with timestamp)

I’ll have to do some research and get back to you.