The problem with using texture atlases and adjacent texels leaking has to do with the way linear texture filtering works.
For any point in the texture that is not sampled exactly at the center of a texel, linear sampling will sample 4 adjacent texels and compute the value at the location you asked as the weighted (based on distance from the sample point) average of all 4 samples.
Here's a nice visualization of the problem:
Since you cannot use something like
GL_CLAMP_TO_EDGE in a texture atlas, you need to create border texels around the edge of each texture. These border texels will prevent neighboring samples from completely different textures in the atlas from altering the image through weighted interpolation explained above.
Note that when you use anisotropic filtering, you may need to increase the width of the border. This is because anisotropic filtering will increase the size of the sample neighborhood at extreme angles.
To illustrate what I mean by using a border around the edge of each texture, consider the various wrap modes available in OpenGL. Pay special attention to
CLAMP TO EDGE.
Despite there being a mode called "Clamp to Border", that is actually not what we are interested in. That mode lets you define a single color to use as a border around your texture for any texture coordinates that fall outside of the normalized [0.0-1.0] range.
What we want is to replicate the behavior of
CLAMP_TO_EDGE, where any texture coordinate outside the proper range for the (sub-)texture receives the value of the last texel center in the direction it was out of bounds in. Since you have almost complete control over the texture coordinates in an atlas system, the only scenario in which (effective) texture coordinates might refer to a location outside of your texture are during the weighted average step of texture filtering.
We know that
GL_LINEAR will sample the 4 nearest neighbors as seen in the diagram above, so we only need a 1-texel border. You may need a wider texel border if you use anisotropic filtering, because it increases the sample neighborhood size under certain conditions.
Here's an example of a texture that illustrates the border more clearly, though for your purposes you can make the border 1 texel or 2 texels wide.
(NOTE: The border I am referring to is not the black around all four edges of the image, but the area where the checkerboard pattern stops repeating regularly)
In case you were wondering, here is why I keep bringing up anisotropic filtering. It changes the shape of the sample neighborhood based on angle and can cause more than 4 texels to be used for filtering:
The larger the degree of anisotropy you use, the more likely you will have to deal with sample neighborhoods containing more than 4 texels. A 2 texel border should be adequate for most anisotropic filtering situations.
Last but not least, here is how a packed texture atlas would be built that would replicate
GL_CLAMP_TO_EDGE behavior in the presence of a
GL_LINEAR texture filter:
(Subtract 1 from X and Y in the black coordinates, I did not proof read the image before posting.)
Due to border storage, storing 4 256x256 textures in this atlas requires a texture with dimensions 516x516. The borders are color coded based on how you would fill them with texel data during atlas creation:
- Red = Replace with texel directly below
- Yellow = Replace with texel directly above
- Green = Replace with texel directly to the left
- Blue = Replace with texel directly to the right
Effectively in this packed example, each texture in the atlas uses a 258x258 region of the atlas, but you will generate texture coordinates that map to the visible 256x256 region. The bordering texels are only ever used when texture filtering is done at the edges of textures in the atlas, and the way they are designed mimics
In case you were wondering, you can implement other types of wrap modes using a similar approach --
GL_REPEAT can be implemented by exchanging the left/right and top/bottom border texels in the texture atlas and a little bit of clever texture coordinate math in a shader. That is a little more complicated, so do not worry about that for now. Since you're only dealing with sprite sheets limit yourself to