Short version:
Is there a general approach in OpenGL that will allow pixel-perfect 2D drawing of textures from an atlas when drawn at native size (including edge pixels), and good quality scaling when filtering to non-native size?
More detail on the problem:
Suppose you’re writing some kind of system for drawing sprites from a texture atlas, such that the user can specify the sprite, and the size and location at which it is to be drawn. Additionally, they may want only to draw a sub-rectangle of the sprite.
For instance, here is a 64x64 checkerboard:
...and alone in its texture atlas:
Note: We’ll assume that the viewport is set up to map 1:1 to device pixels.
Approaches
Edit for clarity: note that the first 2 approaches below do not give the desired result, and are there specifically to outline what doesn’t work.
1. To draw the sprite at native size, simply use GL_NEAREST
- set OpenGL to use GL_NEAREST min/mag filtering
- to draw at position (x,y), place vertices from (x,y) to (x+64,y+64)
- use texture coordinates (0,0) -> (64/atlas_size,64/atlas_size)
This is fine for drawing at native size, but NEAREST filtering gives poor results when the user draws the object at a size other than 64x64. It also forces texels to align to the pixel grid, giving poor results when the object is drawn at non-integer pixel locations:
2. Enable GL_LINEAR
- With linear filtering, we need to shift our uv coords to the centre of the texels:
(0.5/atlas_size,0.5/atlas_size)
to(63.5/atlas_size,63.5/atlas_size)
. Otherwise, for the outermost pixels, linear filtering will sample the sprite’s neighbours in the atlas. - We then also need to modify our vertices, since continuing to use vertices from (0,0) to (64,64) will then stretch the texture by 1px in both directions, like so:
- we therefore need use vertices from (0.5,0.5) to (63.5,63.5). Generally, when the texture is drawn at a ratio of its native size (a,b) we will need to shift our vertices “inwards” by, I believe, (a/2,b/2). The gives the following result (on a purple background):
Note that we get pixel accurate drawing, except for edge pixels, which are blended with the background, since the vertex boundary falls halfway between pixels.
Edit: also note, this behaviour also depends on whether antialiasing is enabled. If not, the previous approach does in fact give pixel perfect rendering, but doesn’t provide a good transition when the sprite moves from a pixel-aligned to a sub-pixel position. Plus antialiasing is a must in many cases.
3. Pad sprites in the texture atlas
The two obvious solutions to the edge pixel problem involve padding the edge of the sprite with a 1px border in the texture atlas. You could either:
- pad with 1 layer of transparent pixels, and expand vertices by (0.5a,0.5b) rather than contracting them
- pad with a second copy of the sprite's outermost pixels, and go back to sampling the texture from (0,0) to (64/atlas_size,64/atlas_size)
This basically gives us pixel accurate drawing with linear scaling. However, if we then allow the user to draw only a sub-rect of a sprite, either of these approaches fails because the sub-rect obviously does not have the required padding.
Have I missed anything? Is there a general solution to this problem?