7
\$\begingroup\$

All normal maps I've seen are pinkish or bluish. It seems like only a small color range is used. Why do game developers give away so much precision?

\$\endgroup\$
12
\$\begingroup\$

The colors in a normal map represent the normals at each point. If a normal is (x, y, z), the corresponding pixel in the normal map will have each of x, y, and z mapped from the range (-1,1) to (0,255) to get the red, green, and blue components respectively.

Now the z-axis is typically used as the direction away from the surface. A perfectly flat surface, with normals (0, 0, 1) all over, will have x and y both mapped to 128, and z mapped to 255. That means the resulting color is (128, 128, 255), which is the sky blue you see often in normal maps.

The bumps in a surface deviate away from this normal, removing a little bit of the blue component and giving it to the red and/or green components (depending on which direction the bump is facing). Generally, the bumps in surfaces are not particularly exaggerated, so the normal map remains mostly blue. You will see magentas and cyans for the bumps that face along the x- and y-axes.

If you found the normal map for a surface with harsh bumps in it, such as a brick normal map, you'll see that the edges are very much red-ish and green-ish.

\$\endgroup\$
8
  • \$\begingroup\$ So the main reason for not using the available color depth of each channel is that the normals are stored normalized? Wouldn't it result in better precision to use unnormalized normals instead? \$\endgroup\$
    – danijar
    Aug 24 '13 at 18:07
  • \$\begingroup\$ Then you'd have too little precision left for actual data. All we care about is direction, not magnitude, for normal maps. \$\endgroup\$ Aug 24 '13 at 18:38
  • \$\begingroup\$ Is a blank normal map deep blue or light blue in the way most normal maps look like? That would help me understand a lot. \$\endgroup\$
    – danijar
    Aug 24 '13 at 18:56
  • 1
    \$\begingroup\$ @danijar What do you mean by blank? There's no such thing as a blank normal. A normal is always pointing in some direction. When the normal is pointing directly away from the surface (i.e. all normals in a flat surface), you get the color (128, 128, 255), which is the light blue you're probably referring to. It's half way between full blue (0, 0, 255) and white (255, 255, 255). See the corners of this image for an example. \$\endgroup\$ Aug 24 '13 at 19:04
  • 1
    \$\begingroup\$ @danijar It's because the values in the normal vector can range from -1 to +1. If -1 maps to 0, and +1 maps to 255, then 0 maps to 128. So (0, 0, 1) maps to (128, 128, 255). \$\endgroup\$ Aug 25 '13 at 0:11
6
\$\begingroup\$

It's true that many tangent-space normal maps have only a limited range of colors. Object-space or world-space normal maps would have a wider range of colors, but if we're only using tangent-space normal maps in our game (as most developers do), it's a quite legitimate question to ask why we don't choose an encoding that trades off the range that we're not using to gain more precision.

For example, instead of mapping [-1, 1] to [0, 1] for each component, we could decide we're going to map [-0.5, 0.5] to [0, 1] for the XY components, and [0.5, 1] to [0, 1] for the Z component. That would give us twice the precision.

The answer is we usually don't need more precision, and using the standard mapping gives us the range to handle any conceivable normals.

However, sometimes we do need more precision. On surfaces that are nearly smooth but not quite and have very subtle bumps, the combination of the [-1, 1] range plus DXT compression can certainly yield visible banding due to poor precision of the normals. In games I've worked on we added a per-material "normal map strength" parameter that would let you effectively adjust the encoding for a specific texture, so you could use a smaller range like [-0.5, 0.5] or [-0.1, 0.1] if you wanted, and thereby get much more precision.

An example of the kind of surface we'd use this on is a car hood, or a glass window on the side of a building (which is not quite flat, but very slightly warped). Both of these are hard, shiny surfaces with quite subtle curvature, which means you can easily see any imperfection in the normals due to their effect on cubemap reflections. Adjusting the normal map encoding for more precision vastly improved the appearance of these surfaces.

Nevertheless, they were the exception rather than the rule, and the default [-1, 1] encoding was quite sufficient for almost all surfaces in the game.

\$\endgroup\$
5
\$\begingroup\$

Who says we do not? You are limiting your thought process to Tangent Space Normal Maps, which (generally) always point away from the surface they are mapped onto. There are also World / Object Space Normal Maps, which contain every color of the rainbow because they are not related to the surface's coordinate space.


As a fun experiment, you should multiply a tangent space normal map by the TBN matrix and output the resulting color instead of normal lighting. Notice how the colors dramatically change, since they are now in object space (or view space depending on how you created your TBN matrix)?

You're not done though, now that the normals are in object/view space you will probably have quite a few vectors that have negative values. You need to bias and scale the colors so that negative values occupy the lower half of the color space and positive values occupy the higher (if you actually want to visualize the normal map correctly, that is).


If you do not want to do the experiment yourself, you can see the difference between world space and tangent space in the image below:

Tangent Space vs. World Space

Tangent space normal maps are in a special coordinate space that ensures positive Z is always normal to the surface. Since XYZ is usually encoded into RGB and most surfaces are relatively flat (e.g. the dot product between the normal the texture provides and a vector tangent to the surface at any point is pretty close to 0) B dominates the color space in tangent space.

In world/object space, there is no dominant color, but these normal maps do not like to be deformed. Ultimately when tangent space normal maps are used, either lighting is done in tangent space or the normal map is transformed into world/view space before lighting. By deferring this transformation (or avoiding it altogether) until render time, objects can be rotated and texture coordinates can be deformed without invalidating the normal map. World space normal maps are usually reserved for static world geometry, and may be used for the normal G-Buffer in deferred shading.

\$\endgroup\$
1
\$\begingroup\$

You can store just two components of a normalized normal map in an RG texture and trivially recompute the third component. I don't know why this isn't more common other than the performance hit of the square root operation in doing this.

1 = sqrt(x^2 + y^2 + z^2)
z^2 = 1 - x^2 - y^2
z = sqrt(1 - x^2 - y^2)
\$\endgroup\$
6
  • \$\begingroup\$ My question is not about OpenGL textures, but about texture image files. For OpenGL textures in memory, I saw storing only X and Y components in some implementations. I am sorry that I wasn't clear about this. \$\endgroup\$
    – danijar
    Aug 24 '13 at 18:54
  • \$\begingroup\$ You can do the same thing for on-disk files. Source assets are just usually in formats that don't support RG textures. \$\endgroup\$ Aug 24 '13 at 18:59
  • \$\begingroup\$ I know, but my question was about the reason of using just a small range of color depth for normal map files. Or is just my assumption about the color range usage untrue? \$\endgroup\$
    – danijar
    Aug 24 '13 at 19:03
  • 1
    \$\begingroup\$ Yes, your assumption is completely untrue. The full range is used. The problem is you, and everyone else in this thread, are thinking of tangent space normal maps. These normal maps are in a special coordinate space that ensures positive Z is always normal to the surface. Since XYZ is usually encoded into RGB and most surfaces are relatively flat ("bump mapping" not "fold mapping" =P), B dominates the color space in tangent space. In world/object space, there is no dominant color, but these normal maps do not like to be deformed so they are not used often. \$\endgroup\$ Aug 25 '13 at 0:32
  • 1
    \$\begingroup\$ @SeanMiddleditch I think it's fairly common to store normals normalized to z = 1 - aka "derivative map", since the normal's XY components are then the (negated) partial derivatives of the surface. Then you can decode it in the pixel shader by setting z = 1 and normalizing, which might well be faster than calculating sqrt(1 - x^2 - y^2). \$\endgroup\$ Aug 25 '13 at 0:36
1
\$\begingroup\$

We actually do use the full colour-depth.

First of all, normals should be unit-length, otherwise we need to do an expensive (and unnecessary) renormalization in the fragment shader.

Let's assume that you're looking at a texture face-on, as if in an image editor.

  • A normal of {0,0,1} is the unit-length normal that's pointing towards you, which translates to a normal map colour of {128, 128, 255}.
  • A normal of {0,0,-1} on the other hand points away from you, and that's a colour of {128, 128, 0}.
  • Normals of {1,0,0}, {-1,0,0}, {0,1,0}, and {0,-1,0} are at right-angles to your view direction, and you can work out the colours yourself.

So already, for just the six cardinal directions (up/down/left/right/front/back), we need values of 0, 128 and 255 in each of the R, G and B channels. They're obviously not the only 6 directions something can be pointing in, so we also need intermediate values. That's the full colour-depth.

The reason why most normal maps are chalky-blue is that most of the time they encode normals that are pointing towards the viewer (my first bullet-point above), and most of the rest of the time they encode normals that are pointing more-or-less towards the viewer. But they don't have to, and without using the full colour-depth they'd lose the ability to point in any arbitrary direction.

This is actually nothing to do with any notion of "precision", it just comes from the direction normals tend to point in when encoded in a normal map.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ In practice I think it is necessary to renormalize in the fragment shader, as the sampled normal can end up non-unit-length due to texture compression and bilinear filtering issues. It's also not that expensive - vector normalization is such a common operation that I'd be surprised if any GPU these days doesn't have a hardware fast-path for it. \$\endgroup\$ Aug 25 '13 at 0:39
  • \$\begingroup\$ Definitely, re-normalization is an unavoidable evil in most cases. However, long gone are the days when normalization is so expensive that a cubemap lookup would be quicker. I kind of miss those days :) \$\endgroup\$ Aug 25 '13 at 1:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.