If you apply DXT compression to a normal map texture, the results are usually pretty awful. But there are some simple things you can do to improve the quality.

First off, notice how in a tangent space normalmap, the Z component always faces roughly outwards (which is why tangent space normalmaps have a bluish tint). And because the texture contains normal vectors, we know the combined length of the X, Y, and Z channels must always be 1.

This means there is no need to explicitly store the Z value. We can throw this away, replacing the blue channel of our texture with zeros, then reconstruct the missing Z value in the pixel shader.

The normal is a unit vector, therefore:

x*x + y*y + z*z = 1

After looking up the values of x and y from the texture, our shader can rearrange this to compute:

z = sqrt(1 - (x*x + y*y))

But how can throwing away and then reconstructing the blue channel improve compression quality?

Remember that DXT compression works by choosing just two base colors for each 4x4 block of the image. If all the colors in a block lie along a line between these two end points, compression quality will be good. The worst artifacts occur when a single block contains colors that are scattered through RGB space, so a single line cannot be fit through them.

By discarding the blue channel of the texture, we collapse the three dimensional RGB colorspace into a two dimensional red/green space. This increases the odds of a single line being a good fit for all the colors in a block, and thus reduces compression artifacts.

For even better quality, you can discard the blue channel as described above, move the red data into the alpha channel (replacing red with zeros), then compress using DXT5. This leaves only a single color dimension for the compression to worry about, which guarantees every block will fit along a single line.

When using only one of the color channels, it is better to choose green rather than red or blue, because DXT uses a 5.6.5 format for the end point colors, and so has slightly better precision in the green channel.

> Are GPUs at the point where calling sqrt on each pixel isn't that expensive relative to the quality of the final output?

It depends on the card. If you're targeting slower/older hardware like in laptops, this could be a problem, but for high end desktop cards or Xbox it will most likely be ok.

I've tried this with a couple of my normal maps and I'm not getting good results.

I get strong bands of colours at points where the normal directions change between the x and z axis.

I'm simply reading the normal data from my normal map into 2 floats (x and y), the computing z on the GPU as you've shown above and displaying it as the pixel colour.

Do I need to setup my normal map differently? They've been generated using the NVidia Photoshop tool, then I've removed the blue channel and moved the red into the alpha.

Nivix

4 Jan 2011 2:51 PM

>> Are GPUs at the point where calling sqrt on each pixel isn't that expensive relative to the quality of the final output?

If you're worried about the number of instruction calls you can always use the partial derivative normal map technique.

It is really easy to setup. In your texture processor convert X to (-X/Z) and Y to (-Y/Z). Then drop the Z component and move the X component to W as Shawn stated above if you like. Just remember to either ConvertBitmapType to typeof(Dxt1BitmapContent) or typeof(Dxt5BitmapContent) depending if you moved X to W or not.

When reconstructing all you have to do is set normal.xy = -tex2D(NormalMapSampler, input.texCoord).xy for Dxt1 and normal.xy = -tex2D(NormalMapSampler, input.texCoord).wy for Dxt5. Then set normal.z = 1 and call normal = normalize(normal).

No sqrt to worry about and fewer total instructions, supposedly around 1/3. However, "it yields a different error distribution" which isn't necessarily worse, it just looks different.

>> I get strong bands of colours at points where the normal directions change between the x and z axis.

I personally get better results on Shawn's technique above by using sqrt(1 - x*x - y*y) instead of sqrt(1 - (x*x + y*y)). The bands of color could be caused by not expanding the normal. You can either expand it in the texture processor (which is a better approach) or expand it in the shader. The expansion formula is 2 * normal - 1 which increases the normal range from (0 to 1) to (-1 to 1).

Hope this information helps expand on one of Shawn's many brilliant posts.

Luke

30 Sep 2011 5:06 PM

In case it's helpful to anyone, here's a simple texture content processor that alters RGBA textures to the DXT5 scheme Shawn described: