If you ever programmed a 3D game, you probably came along this problem: You have a texture with alpha channel and lots of small details on them, which disappear when the object is far away or viewed at a sharp angle. This happens for example with foliage, trees, bushes, grass or chain-link fences. Here is an extreme example showing what I mean:
For most objects like the leaves in trees, this isn't a serious problem, most people won't even notice. But for important, fine grained stuff like wire mesh fence as above, this is certainly not wanted. There are some simple workarounds to this problem:
- Edit the image and make the alpha channel broader and sharper. But this probably makes your texture look much uglier.
- Adjust the alpha testing reference value for your material. This might work, but if you are unlucky, it doesn't look that nice.
- Disable mipmapping. This looks usually quite nice, but it impacts performance quite a lot
What I did instead for
my game was to compute the mip maps slightly different. Instead of calculating the weighted average of the alpha value of each sample as normally when computing the next mip map level, I also compute the maximum alpha value and select a value between the maximum and normal alpha value. And the result was looking a lot nicer:
The result could probably be adjusted a bit, but for me, it looks ok for now. There is no performance impact when rendering this and it is also not slower to generate the mipmaps at all.
For a very simple and badly filtered mipmap, I was originally calculating a pixel for the next level like this:
a /= 4;
r /= 4;
g /= 4;
b /= 4;
newColor.set(a, r, g, b);
(Where a, r, g, and b is the total sum for all 4 input samples)
But now I'm doing it this way:
const float refValue = 0.65f;
a = (int)( (amax*(1.0f - refValue)) + ((a / 4.0f) * refValue) );
r /= 4;
g /= 4;
b /= 4;
newColor.set(a, r, g, b);
(Where amax is the maximal value alpha had in all samples). refValue is a value which you can adjust to your texture (or even compute it for each texture, if you like), but I figured a value of 0.65 worked nicely for most input images, although it probably is not a perfect value in all cases.
Maybe I'll add this feature into my game engine as well, so that other people will profit from this as well in the future.
Another approach might be using a slightly larger downsampling kernel, kind of like (1 3 3 1)^2, or some other more bicubic-like variant (as this will likely be similar in effect to max, but possibly more well-behaved for iterated application). (1 3 3 1)^2 would have a sum of 64, so could be computed without divisions (and even without multiplications) likewise.