Packing/Unpacking normals - Looking for indepth info.

I am trying to understand what Unity does when it imports a normal map.
As per unity docs - in order to make a normal map I need a gray scale image (Not sure that being 8 bit is mandatory). When importing the texture as a normal map created from grey scale, Unity constructs a normal map which as far as I can see is actually a 4 channel image - each channel being 8 bits.
Here is the first question - could any one direct me to what is actually done in this conversion - how are the x,y and z components of the normal calculated from a single 8 bit channel grey scale?

Now looking into the UnityCG.cginc file - unpacking normals uses this code:

inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
   fixed3 normal;
   normal.xy = packednormal.wy * 2 - 1;
#if defined(SHADER_API_FLASH)
   // Flash does not have efficient saturate(), and dot() seems to require an extra register.
   normal.z = sqrt(1 - normal.x*normal.x - normal.y * normal.y);
#else
   normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
#endif
   return normal;
}

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)) && defined(SHADER_API_MOBILE)
   return packednormal.xyz * 2 - 1;
#else
   return UnpackNormalDXT5nm(packednormal);
#endif
}

So here we can clearly see that for mobile unity just uses the xyz components for calculating the normal, but for desktop/flash the X component of the normal is taken from the w/alpha of the normal texture, the Y component from the y and the Z component in actually calculated from the previous two.
So here is the second question - would I be correct assuming that the X component of the normal is copied into the w/alpha channel of the normal map texture during it’s creation?

My third question - As you can also import any texture as a normal map (without creating it from a grey scale) - can I assume that for mobile platforms I just need a 3 channel texture from which the normal will be extracted, while for desktop and/flash I would need a 4 channel textures with the x component of the normal copied into the alpha channel?

Fourth question:
Would it be correct to assume that for desktops/flash the 4th channel is no longer available for usage for things such as height maps while for mobile platform I could use the 4th channel for extra info?

This seems like a convoluted implementation by Unity for storing/calculating normals. What do you think about the notion of creating cginc with a custom consistent implementation for extracting normals? For this to happen though I really need to understand how the normal components are calculated before they are packed into the texture channels.

So there’s two ways to get a normal map in Unity;
You can import a greyscale image (essentially an old-school bump map) and tell Unity to create a normal map from that.
Or you can import a proper tangent space normal map.

You’ll see once you set the texture to a Normal Map in the Inspector, that you’ve got a tickbox for “generate from greyscale”. If that’s ticked, Unity will attempt to treat it as a bump map and create a normal map from that. If it’s unticked Unity will treat it as if it’s already a regular tangent space normal map with R/G/B channels containing the X/Y/Z vector information.

If you’re doing it from a greyscale map, it generates the normals simply by working out the difference in value between the pixel and the pixels next to it. Bigger difference = steeper surface slope. The closer to white the pixel is, the higher it is assumed to be raised above the surface, the closer to black the pixel is, the lower it is assumed to be embossed into the surface. In general, I don’t recommend using this method.

Compression
As for compression, desktop platforms will attempt to store it as DXT5nm format. That means that it takes the red component of the texture and stores it in the alpha channel, then wipes the red and the blue channel.

Normal maps are pretty sensitive so you want the best compression you can get. DXT5 compression stores it’s R/G/B/A channels with 5/6/5/8 bits of accuracy, respectively. Meaning that the Green and Alpha channels store values with higher precision than the Red and Blue channels.

However, because a tangent space normal map should be a normalized/unit vector (a vector with a magnitude - length - of 1.0) and the Z component (blue channel) is never negative, you can use the code defined in UnpackNormalDXT5nm() to reconstruct the Z component (blue channel) if you know the values of the other two. Which means you can store the 3 component vector using just 2 components in the texture map, meaning you use the two highest precision channels in the texture to store it.

This is pretty standard behaviour for desktop/console normal map storage.

For mobile platforms, that code’s a little too expensive to run just for the sake of higher quality normal maps, so for mobile platforms the normal map gets stored as a regular RGB texture, without the channel swapping. That makes unpacking it a lot simpler (simply unpacking it from 0.0 - 1.0 range into -1.0 - 1.0 range by doing fixed3 normal = normalMap.rgb * 2 - 1;).

Unity performs these two compression methods automatically depending on the target platform you’re building for, you simply feed it a regular normal map. Which is why the UnpackNormal() function is there - it will unpack the normal map for you using the correct method for whichever platform you’re building on and you don’t have to think any more about it.

Channel Loss
So, yeah, if you’re using Unity’s built in normal map compression/unpacking, you’ll lose the extra channel information from the alpha and blue channels of your normal map.

The way around that would be to roll your own unpacking method and not set the normal map texture to be a Normal Map type in the Inspector (so Unity doesn’t store it as DXT5nm). Then you can uncompress the normal map as you please and take whatever information from your texture’s channels however you want.

1 Like

Thank you Farfarer for enlightning me further on this subject especially in regards to the use of the DXT5nm format.

So my final question is if I supply an rgb texture (not a grayscale) with the information for the normal components - will Unity do the right conversion according to the required platform when i mark the texture as a normal map?

Yep, that’s all handled automatically with the target platform and UnpackNormal(). Just specify that it’s a normal map and Unity will handle the rest.