This post will go over more details about the construction of the texture atlas mentioned in this post, for which I am using an XNA content pipeline extension. There are some subtleties that bear mentioning.
First, we’ll assume we’re creating a 4 x 4 atlas of 512 x 512 textures, such as this one:
I have these defined in an XML file as external texture file references which get loaded and “processed” into the final atlas.
The first trick is maximizing the usable resolution of the atlas. We unfortunately can’t get our entire 512 x 512 worth. Here are some constraints:
- the sub-textures are intended to wrap when rendered in the game
- for performance and visual reasons we need mipmaps
Since the sub-textures are intended to wrap, we need at least a 1 pixel gutter surrounding each one. Since the shader will be sampling from 2 mipmaps using the same texture coordinates, it stands to reason that the gutter will need to be “relatively” the same size at each mip level (gutter shown in red below). Otherwise, we would get noticeable blurring when the GPU blends between mipmaps.
We need at least a 1 pixel gutter at any mip level. This means at small mip levels the gutter will dominate the texture. A 2 x 2 image will still need a 1 pixel gutter. Working back up, this means the top level 512 x 512 mip level will consist of 256 x256 of usable data surrounded by a 128 pixel gutter. We’ve lost most of our usable resolution!
To address this, my atlas generator lets you specify the maximum number of mip levels for which you want an “accurate” representation of the textures. For instance, if it is 5, then we need a 1 pixel gutter for the 32 x 32 image (the smallest of (512, 256, 128, 64, 32)). Extrapolating back up, this means a 16 pixel gutter at 512 x 512, resulting in an effective 480 X 480 sub-texture:
The next step for each sub-texture is to scale it from its original size (512 x 512) to size it will occupy inside its gutter (480 x 480):
Once this is done, we can begin filling in the (512 x 512) space it will occupy in the atlas. We copy the (480 x 480) center area. Follwing that we can copy over the 8 remaining edges and corners from opposite sides of the original. These form the gutter of the final sub-texture. A picture with some of these edges color-coded is best at explaining (apologies to the colorblind):
Seen at its resting place in the atlas:
Another thing to note is that you should not generate atlas mipmaps from top level of the atlas itself since this will re-introduce texture bleeding. Instead, each mip level needs to be generated from scratch using scaled down versions of the original images (with according changes to gutter size, etc…).
There is one final subtle detail. In the step where we scale the original image from (512 x 512) to (480 x 480) we actually introduce some artifacts at the edges. The scaling code I’m using isn’t aware that this texture is intended for wrapping. The result is that the pixel on the far left won’t include any information from its “neighbor” on the extreme right of the image – but it needs to since this represents the gutter/texture boundary which we’ll be sampling from in-game. The inaccuracy tends to be subtle, but you may notice that something “intangible” looks wrong – a barely visible seam where the texture wraps.
The “lazy” solution here is to make a temporary texture that is a (3 x 3) grid of the original texture (so (1536 x 1536)), and scale that down to the gutterized size (1440 x 1440), and then copy data out of the (480 x 480) center of the (1440 x 1440) downsize. The edges pixels will then have been properly filtered along with their neighbors on the “opposite” side of the texture:
This last step does increase compile time (where the content is processed) a fair amount since we’re moving so much data around. But the result is worth it if you’re into perfection.