10
\$\begingroup\$

I have a custom sprite routine (openGL 2.0) which uses a simple sprite sheet (my textures are arranged horizontally next to each other).

So, for example, here is a test sprite sheet with 2 simple textures:

enter image description here

Now, what I do when creating my openGL sprite object is specify the total number of frames in its atlas and when drawing, specify which frame I want to draw.

It then works out where to grab the texture from by:

Dividing the required frame number by the total number of frames (to get the left coordinate)

And then diving 1 by the total number of frames and adding the result to the left hand coordinate calculated above.

This does seem to work but sometimes I get problems. Say for example, I want to draw the X below and I get...........

enter image description here

I've heard about putting a 'padding' of 1 px between each texture but could someone explain exactly how this works? I mean if I do this it will surely throw off the calculations for getting the texture.

If I simple include the padding in texture picked up (so the sprite is drawn with a blank border), then surely this will cause problem with collision detection? (ie sprites may appear to collide when using bounding boxes when the transparent parts collide).

Would appreciate if someone could explain.

\$\endgroup\$
2
  • \$\begingroup\$ Are you using GL_NEAREST or GL_LINEAR for rendering the texture? \$\endgroup\$
    – House
    Commented Sep 5, 2013 at 21:25
  • \$\begingroup\$ I'm using GL_Linear @Byte56 \$\endgroup\$ Commented Sep 5, 2013 at 23:08

1 Answer 1

16
\$\begingroup\$

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.

  http://lucera-project.com/blog/wp-content/uploads/2010/06/wrap.png

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:

  http://www.arcsynthesis.org/gltut/Texturing/ParallelogramDiag.svg

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 GL_CLAMP_TO_EDGE behavior.

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 GL_CLAMP_TO_EDGE :)

\$\endgroup\$
6
  • \$\begingroup\$ Thanks @AndonMColeman, could you show the border in a diagram as I'm still not sure I understand how this works - again will it mean that when the texture is applied to my object it will include the border? I want the actual texture to go right to the edges of my quad - sorry if I'm not understanding this correctly - would appreciate some more details - thanks \$\endgroup\$ Commented Sep 5, 2013 at 23:26
  • \$\begingroup\$ @user22241 Certainly, the way this border works is by duplicating the texel at the edge of the texture. \$\endgroup\$ Commented Sep 5, 2013 at 23:35
  • \$\begingroup\$ @user22241: No, this border will not be visible under normal circumstances. You will treat your packed textures as having the same dimensions for texture coordinate calculation, you just apply an offset to them to skip past the border. The whole point of the border is to prevent linear texture sampling from over-reaching and sampling texels that belong to separate images in your sprite sheet. If you use nearest neighbor sampling none of this is necessary, but then you get nasty aliased sprites. \$\endgroup\$ Commented Sep 5, 2013 at 23:59
  • \$\begingroup\$ Brilliantly detailed answer - thanks! Just one last thing if I could. How would I work out the 'offset' to add to my texture coordinates? Am I right in saying it would simply be 1/widthOfTexture? \$\endgroup\$ Commented Sep 6, 2013 at 13:14
  • \$\begingroup\$ @user22241: Yes, the start of the texture image would be +1/widthOfTexture in the X direction and +1/heightOfTexture in the Y direction. You will have a border that goes all the way around each texture. By the time you want to calculate the texture coordinates for the 3rd horizontal texture, the packed location for this texture is actually +5 texels (+2 texels for the first texture's border, +2 for the second texture's border and +1 for the start of this texture) in addition to the width of the other 2 textures. It seems complicated just writing it; I can draw you a diagram if need be :) \$\endgroup\$ Commented Sep 6, 2013 at 13:28

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .