Bleeding edges when artifically "zooming in" on texture atlas in the shaders.

I have a texture atlas with two blocks:

And I want to use this texture for my blocks. I don’t want each block to use the entire texture, just a part of it, to make the world look bigger, which is why I would’ve used something like this for the vertex shader:

v_uv = a_position;

Unfortunately since this isn’t a single texture, but an atlas, I had to improvise:

v_uv = vec2(0.5 * a_blocktype + mod(a_position.x, 0.5), -a_position.y * 0.5);

And this kinda works, except that it causes this bleeding:

I’m not sure if this is due to no half-pixel correction, since it seems to be a bit more than a pixel of bleeding.

I suspect that this is a result of the implicit derivatives used for mipmap selection. When the texture coordinates wrap around, you have a large difference in texture coordinates between adjacent fragments, which will result in a low-resolution mipmap level being selected.

To correct this, use one of the texture functions which take explicit derivatives or an explicit LoD parameter, e.g. textureGrad or textureLod.

Also: when using a texture atlas, set glTexParameter(GL_TEXTURE_MAX_LOD) so you never use mipmap levels where the tiles are smaller than a texel. Or use a 2D array texture where each tile is a separate layer.

I’m not using mipmapping, though? This is 2D. Also GLSL 1.2 doesn’t have textureLod, should’ve mentioned the version.

Well, it’s a bit hard to see exactly what’s going on due to JPEG artifacts. But if this is with magnification, mipmaps won’t be the issue. It probably is “no half-pixel correction”, i.e. interpolating between the two tiles. If the sample location falls between the centre of a tile-edge pixel and the edge itself, linear interpolation will interpolate between the right-most pixel in one tile and the left-most pixel in the next tile.

With a texture atlas, you can’t use the GL_WRAP_* modes to handle tile boundaries. Solutions include:

[li] Array textures (not available in OpenGL 2.1).
[/li][li] 3D textures (may be a suitable alternative to array textures if you don’t need mipmaps, although 2.1 doesn’t require support for more than 16 depth slices).
[/li][li] GL_NEAREST filtering.
[/li][li] Insetting texture coordinates by half a pixel (ugly; you’ll never get 1:1 reproduction at any zoom level).
[/li][li] Insetting texture coordinates by one pixel, adding a border to each tile.
[/li][li] Re-implementing linear filtering in the fragment shader so you can wrap around at tile boundaries (expensive).
[/li][li] Implementing clamp-to-edge in the fragment shader (less expensive than the previous option, but will result in smearing if tiles don’t have a plain border).

If all your atlas tiles have the same size you could clamp the texture coordinates in the fragment shader to not go over the center of your border pixels.

I am using GL_NEAREST filtering already.
Here’s a better screenshot: