In my previous article about texture filtering I mentioned that scaling down images using point sampling, or scaling to less than half size using linear filtering, looks bad because some source pixels are discarded.
Why exactly is this a problem?
Try this thought experiment:
We have a 256x256 texture, which is being scaled down by a factor of 1/256, so the entire image covers just a single screen pixel. What color should this pixel be?
If we use point sampling, only one pixel of the 256x256 image will be used, and the others discarded. This can give pretty random results depending on which specific pixel happens to be chosen!
Consider what happens if we tile our texture, so the scaled down image is repeated 100 times. If each repetition lines up exactly with one screen pixel, they will all choose the same source pixel, but if repetitions and screen pixels do not line up (maybe you are scaling down by 1/255 or 1/257 rather than 1/256, or using a 3D perspective projection), each repetition will end up choosing a different random location in the source texture. This is basically the same thing as:
for each destination pixel: destination = sourceTexture.GetPixel(random.GetNext(), random.GetNext());
which is not going to give the best looking results 🙂
It would obviously be better if we could average all the pixels from the source texture, rather than having to choose just one of them. But averaging 256x256 color values is too slow to do in realtime.
When you can't afford to compute something on the fly, it is time to precalculate...
A mipmap is a precalculated copy of an image that has been shrunk to a lower resolution using a more accurate, higher quality filtering algorithm than would be possible in realtime on the GPU. Mipmaps are arranged in a chain where each image is half the size of the previous one, for instance:
- Original = 256x256
- Mip 1 = 128x128
- Mip 2 = 64x64
- Mip 3 = 32x32
- Mip 4 = 16x16
- Mip 5 = 8x8
- Mip 6 = 4x4
- Mip 7 = 2x2
- Mip 8 = 1x1
The GPU will automatically choose the appropriate image depending on how much the texture is being shrunk down.
To draw with mipmaps, you need two things:
- You must include mipmap data when creating your textures (for instance by setting the Generate Mipmaps content processor parameter)
- You must set SamplerState.MipFilter
- TextureFilter.None = do not use mipmaps
- TextureFilter.Point = low quality (but sometimes faster) mipmapping
- TextureFilter.Linear = higher quality mipmapping
- If you draw using BasicEffect, this will automatically set MipFilter = Linear
The difference between Point and Linear mip filtering is how the mipmap level is chosen:
- MipFilter = Point
- Chooses the appropriate mip level depending on how much the texture is being shrunk
- If this is a fractional value, rounds up to the next larger mip level
- This means the mip image may still need to be shrunk by a small amount
- But linear filtering is good for shrinking as long as we don't go below half size
- To shrink below half size, we'd just choose a smaller mip level instead
- There can sometimes be visible artifacts when switching between mip levels
- MipFilter = Linear
- Samples from both the next larger and next smaller mip levels
- Interpolates between them based on how close the current image scale is to the two mip levels
- Avoids artifacts along boundaries between mip levels
- But we must now sample 8 rather than 4 source pixels for each destination, so this can cost more if your app is texture fetch limited
Let's see this in action. Here is the linear filtered terrain from my previous post:
And now using mipmaps:
Note how the distant hills appear smooth and free from noisy aliasing.
Popular urban myth #1
"Mipmaps take up too much memory"
Actually, mipmaps use little extra memory, thanks to the power of powers:
- Mip 1 = 1/4 the original size
- Mip 2 = 1/16 the original size
- Mip 3 = 1/64 the original size
- Mip 4 = 1/256 the original size
If you continue this sequence, you'll see that an entire mipmap chain all the way down to 1x1 takes up just 1/3 more memory than the original texture. So it's a negligible overhead, especially once you get past that first mip.
Popular urban myth #2
"Mipmaps are slower for the GPU to render"
In fact, they are often much faster! If you aren't scaling down a texture, having mipmaps won't cost anything. But when you are scaling down, mipmaps can save crazy amounts of memory bandwidth.
Remember our example of a tiled 256x256 texture, where each repeat is being scaled down to 1x1 (a common situation in things like terrain rendering). Without mipmaps, every destination pixel will sample a radically different location in the source texture, so the GPU must jump around fetching colors from different areas of memory. GPUs typically have very small texture caches, relying on the fact that textures tend to be accessed sequentially, so this access pattern will thrash the cache and can bring even a high end card to its knees. But with mipmaps, the GPU can simply load a small mip level which will easily fit in the cache, and can then render many destination pixels without having to go back to main memory.
So mipmaps are not just for reducing aliasing: they can actually speed up rendering, too.
Shawn's Recommendation™: if you are going to draw a texture in 3D, or planning to scale it down to less than half size, you should always use mipmaps.