I figured out what was going on. The problem was not floating point precision, and not that I needed to make my own mipmaps to blend each level correctly. There were actually 2 problems:
- I was using the Max Rects packer algorithm with non-power-of-2 sized images to create the texture atlas.
- I wasn't setting
GL_TEXTURE_MAX_LEVEL
What I was doing:
- Use Max Rects on non-power-of-two packing to create a texture atlas. Textures are aligned at pixel boundaries.
glGenerateMipmap(GL_TEXTURE_2D);
making blending the texture atlas textures into each other even on level 1
What I'm doing now:
- All textures are power of 2. 128x128 is the smallest sized texture in a 4096x4096 texture atlas.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 7);
- This is new. Don't allow anything below mipmap level 7, because 4096 / (2^7=128) = 32. This makes the code stop generating mipmaps once the smallest texture in the atlas becomes 1 pixel by 1 pixel.
glGenerateMipmap(GL_TEXTURE_2D);
- Now scales down the atlas correctly because all the textures are aligned to 1/32 of the width or height of the atlas rather than 1/width or 1/height.
And now the min filter can use 2 modes without texture bleeding:
GL_NEAREST_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
The following 2 min filter modes still have bleeding issues, but I'm not planning on using them anyway:
GL_LINEAR_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_LINEAR
The reason is because they are interpolating the mipmaps to sizes that are not powers of two, which makes the textures in the texture atlas aligned to points in between pixels, causing the bleeding. The only way I think this can be solved is by adding a border around each texture in the texture atlas. The only problem with that is that the border would have to be 128 or 64 pixels, the size of the smallest texture or half the size of the smallest texture (if you use half pixel offset). And that's a huge waste of video RAM.
So I went with GL_NEAREST_MIPMAP_LINEAR
:

Code
uint8_t* pixels = /* Fixed number of pre-generated mipmaps */;
size_t pixels_len = /* length of pixels */;
size_t width = /* width of first mipmap */;
size_t height = /* height of first mipmap */;
assert(pixels_len >= width * height * 4);
unsigned int new_texture;
glGenTextures(1, &new_texture);
glBindTexture(GL_TEXTURE_2D, new_texture);
// Rendered smaller than texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_NEAREST_MIPMAP_LINEAR);
// Rendered bigger than texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA,
width,
height,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
pixels
);
// Generate mipmaps
int mipmap_level = 0;
int offset = width * height * 4;
while(width > 1 && height > 1 && offset != pixels_len) {
// Divide width & height.
width >>= 1;
height >>= 1;
// Increase mipmap level.
mipmap_level += 1;
glTexImage2D(
GL_TEXTURE_2D,
mipmap_level,
GL_RGBA,
width,
height,
0,
GL_RGBA,
0x1401,
pixels + offset,
);
offset += width * height * 4;
}
glTexParameteri(
GL_TEXTURE_2D,
GL_TEXTURE_MAX_LEVEL,
mipmap_level,
);