Sub-Pixel Accurate Pixel-Sprite Filtering

Intended to fix the issue mentioned here:

(In the preview, the top is without filtering, the bottom is with filtering.)

Tested with Godot 3.2.4-rc5.

Usage notes are in the shader code.

  • To be used as a ShaderMaterial on a Sprite node.
  • Make sure “Filtering” is enabled in the texture import settings, otherwise it won’t have an effect!
Shader code
shader_type canvas_item;
render_mode blend_mix;

// *** Sub-pixel Accurate Pixel-Sprite Filtering ***
// (Effectively implements analytical anti-aliasing for point-filtered textures.)
// A new uv-coordinate is computed that can be used to look up a texture with
// linear filtering enabled. Therefore only one texture lookup is required.
// This means you have to check the "Filter" option, when importing the texture.
// A side effect is, that rotated sprites also look smooth.
// The only use is if you require non-integer scale or rotation. Otherwise, the
// result will be identical to point-filtering.
// Possible Issues:
// - If a sprite has non-integer coordinates (this can be caused by using 
//   the "centered" option), or by manually moving it to a non-integer 
//   coordinate, it will appear more blurry than without filtering.
//   This is simply an effect of the algorithm.
// - There is still aliasing on the outermost edges of the sprite.
//   This cannot be solved within the fragment shader. It is due to lack of 
//   real anti-aliasing in the rasterizer.

// Additional smoothing factor. Should usually be left at 1.0
// Lower values cause a stronger smoothing.
uniform float smoothing_factor : hint_range(0.1, 1.0) = 1.0;

void fragment() {
	// compute the new uv
	vec2 uv = UV;
	vec2 uv_width = fwidth(UV);
	vec2 sprite_screen_resolution = smoothing_factor / uv_width;
	vec2 uv_pixel_src = floor(uv / TEXTURE_PIXEL_SIZE + 0.499);
	vec2 edge = uv_pixel_src;
	edge = edge * TEXTURE_PIXEL_SIZE * sprite_screen_resolution;

	vec2 uv_pixel = uv * sprite_screen_resolution;
	vec2 uv_factor = clamp(uv_pixel - edge + 0.5, 0.0, 1.0);

	uv = (mix(uv_pixel_src - 1.0, uv_pixel_src, uv_factor) + 0.5) * TEXTURE_PIXEL_SIZE;

	// now we can use the uv as always...
	COLOR = texture(TEXTURE, uv).rgba;
pixel, smooth, sprite
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.

Related shaders

Smooth 3D pixel filtering

Accurate FNAF “Panorama”

2D Sprite “Cartridge Tilting Glitch”


1 Comment
Newest Most Voted
Inline Feedbacks
View all comments
1 year ago

I am trying to convert this from a GLES3 shader to GLES2.

The only function I see that is GLES3 only is fwidth(uv) which can be broken down into
abs(dFdx(uv)) + abs(dFdy(uv))

But dFdx() and dFdy() are also GLES3 only. So what does the dFdx function actually do to uv and is there any way of implementing it for a GLES2 shader? I have tried reading about it but the math jargon about partial derivatives is going over my head! :S