Normal map compression

Started by
17 comments, last by GameDev.net 19 years, 7 months ago
Thanks for all the comments.

Quote:Original post by Anonymous Poster
Another way of storing a normalized vector with two values are to just store X and Z, then calculate Y using sqrt(1 - X * X - Z * Z).
This assumes that the Y component of the normal is always positive (true if normals were generated from a heightfield).
The creation of the map is faster and easier than the previous method since no cos/sin etc are needed, You just drop one component.
You could use a look up here to using X and Z (2D texture) or use X * X + Z * Z (1D texture) which I belive is faster (less memory to access).


I've read about this method. Currently, my normal map is stored in (planet) object coordinates, so basically all orientations of X, Y and Z are possible. On the other hand, I don't think the method is much faster since you have to compute a sqrt which is as bad as sin/cos I guess. Lookup is possible, though. But it doesn't solve the compression problem.

Quote:
Why not just store your normal maps as a heightfield, calculate the normals on the fly and only use 1 height value per pixel?

OK, that would cost least memory but it would require at least 3 texture lookups in the pixel shader to get the dx and dy differences along two directions of the heightmap.

Scoob Droolins:
Thanks for the link. I am using mipmapping also for the normals.
Why do the QIII need the R component? Do they store phi and theta in RGB somehow and R in alpha? What do they do in the pixel shader? Covert (theta,phi,R) back to (n1,n2,n3)?


Advertisement
Quote:
Lutz> "I've got an idea how to compress normal maps"
this technique was already used in the md3 format to store normals :)
have you tryed it on the GPU yet to see the performance of the whole thing compared to regular normal maps?


Ah, good to know! I couldn't imagine it was new anyway.
I haven't tried it yet. I guess I won't see any performance change (RadeOn 9800 Pro 128MB) since the normal map stuff is only a small part of my pixel shader and the work load is dominated by other stuff. Maybe I'll temporarily disable the other stuff.

Anyway, I think I'll post some results once it works.
Dag. Yeah, i meant Doom III (always get those confused.)

Quote:Original post by Lutz
Scoob Droolins:
Thanks for the link. I am using mipmapping also for the normals.
Why do the QIII need the R component? Do they store phi and theta in RGB somehow and R in alpha? What do they do in the pixel shader? Covert (theta,phi,R) back to (n1,n2,n3)?


I think they use standard XYZ pixel normals, the R component is the pixel normal's X value, doesn't matter if it 's object or tangent space. Move R to Alpha, then zero the R. Now DXT5 compress, which does alpha separately. This gives more accurate compression of both the GB(YZ) channels, and the A(X) channel. In the pixel shader (now that the normal map has been uncompressed by HW), move A back to R to restore the proper XYZ normal, the proceed as usual.

joe
image space
Quote:Original post by Scoob Droolins
- (offline) move the R component into the alpha channel
- (offline) DXT5 compress the normal map
- (runtime) move alpha back into R channel in the pixel shader.

Just a quick correction. Point 1 and 2 are done at level-load time. So it's not really off-line. Or is that just different terminology?


Quote:Original post by Anonymous Poster
Another way of storing a normalized vector with two values are to just store X and Z, then calculate Y using sqrt(1 - X * X - Z * Z).
This assumes that the Y component of the normal is always positive (true if normals were generated from a heightfield).
The creation of the map is faster and easier than the previous method since no cos/sin etc are needed, You just drop one component.
You could use a look up here to using X and Z (2D texture) or use X * X + Z * Z (1D texture) which I belive is faster (less memory to access).

This is how nVidias HILO textures work. They store X and Z component with 8 or 16 per component and then calculate Y in "pixel shader". But their point is not increased compression but increased percision.
You should never let your fears become the boundaries of your dreams.
Quote:Original post by _DarkWIng_
Just a quick correction. Point 1 and 2 are done at level-load time. So it's not really off-line. Or is that just different terminology?


You're right, due to the A/R swap, no current offline compressor (like the nVidia Photoshop plugin) could do this. I'm thinking this feature may show up in the plugin soon.

Thanks _DarkWing_
I've tested the theta-phi-method now. It works, BUT:

1) It's slower.
I've used sin/cos functions in my pixel shader (no texture lookups) (the combined sincos function didn't work for some reason). Frame rate drops from +200FPS down to 150FPS (RadeOn 9800 Pro, 4xAA, 8xAF, 800x600, planet fills whole screen, ~20000 polys)

2) There's a problem when theta wraps from 2PI down to 0.
Could be solved, though, I guess.

3) It doesn't look pretty (most important!)
There are two things important to me: The normal map can be compressed and it should still look good. My GPU didn't want to compress luminance/alpha, so I wrote theta/phi into the RG channels and set BA to zero (just for testing) and compressed the texture. As a result, it produced (almost) the same artifacts as the (old) XYZ method.

So what I did is change my normal mapping system from object space to tangent space (everything is stored in tangent space now). And voila - it looked good even with compressed normal maps (XYZ)!

Sorry, this was me.
This 2d representation has been exploited eg. in photon mapping. You can store the spherical coordinates as bytes, leading two only two bytes per pixel/photon/whatever, and then you can use a lookup table (of size 65536 to directly grab the cartesian direction, or two ones, one for sin and one for cos, of size 256 and then do the math. the latter can read to more efficient cache usage).

-- Mikko
One of the problem with using the theta phi representation as you figured already is that you cannot rely on the filtering of the texture to smooth your normals around the poles.
But let's just imagine that you choose a representation that does not rely on the poles (hemisphere in tangent space), then what you do is converting your angles in -pi/2, pi/2 to a position in [-1,1][-1,1][0,1] by using a two monotonic functions and you can deduce the last component from the first two.

You will soon realize that you can save the first process by storing instead the first two components and still deduce the last one (it's straightforward on a single hemisphere).

Saving the sin/cos calculation is a big performance step usually and additionnally, depending where you want your precision, you should note that storing components directly makes the precision higher around the "zero" position which should help smooth gradients which is what matters visually.

Of course this "hemisphere" thing only works in tangent space, that means is only viable on per pixel normal calculations in traditional techniques.

This topic is closed to new replies.

Advertisement