# Why are normal maps blue?

> 2026-11-05 · Graphics · normal maps, shaders, textures, rendering
> https://hyna.dev/en/posts/why-normal-maps-are-blue

The first time you open a normal map texture, it is a little puzzling. Bricks, armour, tree bark: almost every normal map is the same lavender blue, with a touch of pink and cyan only along the edges of the details.

Below are three real game textures, freely released (Poly Haven, CC0). The top row holds the colour maps, the bottom row the normal maps. Brick, bark or metal plate: the normal maps are all the same colour.

![Brick, bark and metal plate textures. The top row is each one's colour map, the bottom row its normal map. The colour maps are red, brown and navy, but all three normal maps are the same lavender blue.](/images/posts/why-normal-maps-are-blue/game-textures.webp)

That colour is no accident. The blue is exactly `(128, 128, 255)`, and there is a reason it has to be. In this post I will follow that reason from the beginning. The short answer: **a normal map is not a picture but a table of directions, and directions written relative to the surface almost all point "up".**

## What a normal map stores

A normal is a vector of length 1 that stands perpendicular to a surface. Shaders use it to work out how squarely light hits the surface. The simplest diffuse light is the dot product of the normal and the light direction.

$$
\text{brightness} = \max(0,\; \mathbf{n} \cdot \mathbf{l})
$$

On a low-poly model the normals change only coarsely. So instead of modelling small bumps like rivets and seams with polygons, we write into a texture, pixel by pixel, "pretend the surface faces this way here". That is a normal map: one direction per texel.

![Two identical six-faced boxes wearing the same brick colour map. The left one has only the colour map, so the bricks look like a flat picture; the right one adds the normal map, and the bricks and mortar catch the light as if they stood out.](/images/posts/why-normal-maps-are-blue/cube-with-without.webp)

Both are the same six-faced box with the same brick colour map. Only the right one adds the brick normal map from above. Not a single polygon was added, yet the bricks and mortar catch the light and stand out.

The catch is that a texture is a container for colours. The direction has to be turned into a colour first.

## The rule that turns a direction into a colour

Each component of a unit vector lies between -1 and 1. A texture channel, on the other hand, holds only 0 to 1 (0 to 255 in 8 bits). So we halve it and shift it up by a half.

$$
\mathbf{c} = \tfrac{1}{2}\,\mathbf{n} + \tfrac{1}{2}, \qquad \mathbf{n} = 2\,\mathbf{c} - 1
$$

x goes into the red channel (R), y into green (G) and z into blue (B). A component of -1 becomes 0, 0 becomes 128, and 1 becomes 255.

![The x, y and z components moving into the R, G and B channels. -1, 0 and +1 map to 0, 128 and 255, and the flat normal (0, 0, 1) becomes R 128, G 128, B 255: a lavender blue. The lower half of the B channel is hatched.](/images/posts/why-normal-maps-are-blue/mapping.svg)

Now think of a perfectly flat spot. The surface faces straight up, so its normal is `(0, 0, 1)`.

$$
(0,\,0,\,1) \;\mapsto\; (0.5,\,0.5,\,1.0) \;\mapsto\; (128,\,128,\,255)
$$

Red and green sit exactly in the middle, blue is at its maximum. That is the blue of a normal map. Since 0.5 × 255 is 127.5, some tools write 127 instead. Either way it means the same thing: flat.

## Lean it by hand

This is easier to feel than to read, so I built the conversion as a machine. The tile resting on a ball is the surface, and the needle standing square to the tile is the normal. Two micrometer screws lean the tile along x and y, and the three columns behind read the needle's x, y and z as 0–255. The chip at the front is the colour those three values make.

Move the pointer over the figure and the needle leans that way. The arrow keys work too.

> Interactive demo: A bench that turns a normal into a colour: a needle standing square to a leaning tile is the normal, and three gauges read its x, y and z as R, G and B from 0 to 255. Flat reads 128, 128, 255 (blue), and since the needle can never point below the surface, B never drops under 128. — https://hyna.dev/en/posts/why-normal-maps-are-blue

Lean it a few times and two things stand out. The red and green gauges swing up and down around the middle (128). The blue gauge, though, always stays in the upper half. The blue column even has a stop at 128. The needle stands square to the tile, so however you lean the tile, the needle can never dip below it.

That stop is the heart of this post.

## Why blue is always at least half: tangent space

In the figure, the needle's x, y and z were measured **relative to the tile**, not to the bench. Normal maps do the same. At every point of the surface they set up a small coordinate frame and write the normal in that frame. This frame is called tangent space.

- **T** (tangent): the direction in which the texture's U increases. The normal map's x, red
- **B** (bitangent): the direction in which the texture's V increases. The normal map's y, green
- **N** (normal): the original mesh normal. The normal map's z, blue

In this frame, z means "out of the surface". However bumpy the surface gets, the normal of a bump never points into the original surface: that would mean seeing the surface from behind. So every tangent-space normal lies in the upper hemisphere, z is at least 0, and blue is at least 128.

![The directions a tangent-space normal can take. The arrows in the upper hemisphere are painted in their encoded colours, and the strip that unrolls those colours runs from teal through blue and lavender to pink. The olive and brown colours of the lower hemisphere are hatched out.](/images/posts/why-normal-maps-are-blue/hemisphere.svg)

Collect the colours of the upper hemisphere and they run from teal through blue and lavender to pink: every one of them at least half blue. Colours without blue, like olive or brown, would need a normal pointing below the surface, so they never appear in a tangent-space normal map.

One more thing adds to this. Real surfaces are mostly flat. Apart from a rivet here and a seam there, the rest is just plate. So most texels cluster around `(128, 128, 255)`, and the whole image reads as lavender blue. Below is one cell of the riveted-panel normal map used in this post's demo, split by channel.

![One cell of a normal map with a rivet, and its R, G and B channels as greyscale. R and G are mostly mid grey, lighter or darker only at the rims of the rivet and seams. B is almost entirely white.](/images/posts/why-normal-maps-are-blue/channels.png)

Red and green are mostly mid grey (128), getting lighter or darker only at the edges of the bumps. Blue is nearly white. The mean of this whole texture is `(128, 128, 251)`, and even its lowest blue is 228.

## The TBN matrix: from tangent space to the world

If the values in a normal map are relative to the surface, the shader has to turn them back into world space before it can light anything. It does that with a matrix whose columns are T, B and N, usually called the TBN matrix.

$$
\mathbf{n}_{\text{world}} = \begin{bmatrix} \mathbf{T} & \mathbf{B} & \mathbf{N} \end{bmatrix} \mathbf{n}_{\text{tangent}} = n_x\,\mathbf{T} + n_y\,\mathbf{B} + n_z\,\mathbf{N}
$$

It looks grand as an equation, but what it does is simple: go x along T, y along B and z along N, using the x, y and z read from the normal map. A flat texel `(0, 0, 1)` simply becomes N, the mesh's own normal.

![Three points on a curved surface, each with its T (red), B (green, out of the page) and N (blue) axes. All three read the same texel value (0.50, 0.00, 0.87), but because each point has its own axes, the resulting world normals point in different directions.](/images/posts/why-normal-maps-are-blue/tbn.svg)

The three points in the figure read exactly the same value from the normal map. Yet each has its own T, B and N, so in the world they all point different ways. This is why a tangent-space normal map can describe bumps with the same values regardless of the shape underneath. The same riveted-panel texture can go on a plane, a cylinder or a character's bending arm.

In a fragment shader it usually looks like this.

```glsl
vec3 n = texture(normalMap, uv).xyz * 2.0 - 1.0;   // [0,1] -> [-1,1]
mat3 TBN = mat3(normalize(vT), normalize(vB), normalize(vN));
vec3 worldNormal = normalize(TBN * n);
float diffuse = max(dot(worldNormal, lightDir), 0.0);
```

T and B are computed from the direction in which the mesh's UVs increase. If the baking tool and the engine compute them even slightly differently, the normal map looks wrong on the same mesh, so these days many tools and engines agree on one method called MikkTSpace.

## Object-space normal maps are a rainbow

Normals don't have to be written in tangent space. They can also be written in the model's own coordinate frame, object space. Put the same riveted panel on a sphere and bake it both ways, and the difference is plain.

![Two normal maps baked from the same riveted panel on a sphere. The tangent-space map on the left is an even lavender blue; the object-space map on the right is a rainbow: green at the top, purple at the bottom, blue in the middle and red on the right.](/images/posts/why-normal-maps-are-blue/tangent-vs-object.png)

In object space, the normals at the top of the sphere point to +y (green), on the right to +x (red) and at the front to +z (blue), so the texture becomes a rainbow. Some places, like the back of the sphere, point to -z, so blue goes all the way down to 0 (at both ends of the texture). The mean of this texture is `(128, 127, 128)`: just grey.

The two kinds differ like this.

| | Tangent space | Object space |
| --- | --- | --- |
| Colour | Lavender blue | Rainbow |
| Shader | Needs TBN | Uses the texel as is |
| Same texture on another mesh | Works (tiling, reuse) | Doesn't (one mesh only) |
| Mesh bent by skeletal animation | Just works | Must be rotated with the bend |
| Mirrored UVs | Work | Hard |
| Dropping the blue channel (compression below) | Works | Doesn't |

In the end, the normal maps you see in games are almost all blue because almost all of them are tangent-space maps. The gains in reuse and animation are simply too large.

## Try it yourself

The lab below lets you check everything so far in one place. On the left is the stored texture; on the right, a sphere wearing that texture, drawn by a shader.

- Switch **Space** and the same bumps bake into a blue texture or a rainbow one. The result looks the same either way.
- Set **Channel** to B. In tangent space it is almost white; in object space both ends (the back of the sphere) go dark.
- Point at the texture to read that texel's RGB and its decoded vector.
- Move the pointer over the sphere and the light follows.

> Interactive demo: The same riveted panel baked as a tangent-space and an object-space normal map, shown as the stored texture and as a lit sphere. View one channel at a time, and see the shading flip when a DirectX texture is read as OpenGL. — https://hyna.dev/en/posts/why-normal-maps-are-blue

## OpenGL and DirectX: one green channel apart

Move a normal map to another engine and sometimes the bumps look inverted: a raised rivet looks pressed in. The cause is almost always the green channel.

Everyone agrees that T is the U direction. But whether B, the direction in which V increases, points up or down the image splits into two conventions.

- **OpenGL convention (Y+)**: more green points further up the image. Unity, Blender and Godot use this one.
- **DirectX convention (Y-)**: more green points further down the image. Unreal Engine uses this one.

![The same rivet baked in both conventions. In the OpenGL one on the left, the rivet's top rim is greenish and its bottom rim purple. In the DirectX one on the right it is the other way round: the bottom rim is greenish.](/images/posts/why-normal-maps-are-blue/gl-vs-dx.png)

The two textures have identical red and blue; only green is flipped. That also gives you a way to tell them apart by eye: if the top rim of a raised bump is greenish, it is OpenGL; if the bottom rim is, it is DirectX. Switch Texture Y to DirectX in the lab above, and you can watch a shader that reads OpenGL shade the rivets upside down.

Real textures behave the same way. The brick texture from earlier ships with normal maps in both conventions. Zoom into the same spot and the left one (OpenGL) is greenish along the top edge of each brick, the right one (DirectX) along the bottom edge.

![The same spot of the same brick normal map, zoomed, in both conventions. In the OpenGL version on the left each brick's top edge is greenish; in the DirectX version on the right, its bottom edge.](/images/posts/why-normal-maps-are-blue/bricks-gl-vs-dx.webp)

The fix is simple: invert the green channel (`G' = 255 - G`). Most engines offer a flip-green option in their texture settings.

## Bonus: you can throw the blue channel away

The fact that z is never negative in tangent space has one more use. A normal has length 1, so once you know x and y, you can compute z.

$$
n_z = \sqrt{1 - n_x^2 - n_y^2}
$$

A square root has a positive and a negative answer, but since z can't be negative in tangent space, you take the positive one. That is why many engines store normal maps in two-channel compressed formats like BC5 and rebuild blue in the shader. In the same memory, R and G get more precision.

So the reason normal maps are blue leads somewhere neat: the blue is so predictable you don't even need to store it.

## Summary

- A normal map is a table of directions written as colours. Each component is moved into 0–255 with `n / 2 + 1/2`.
- The normal of a flat surface, `(0, 0, 1)`, becomes `(128, 128, 255)`: that lavender blue.
- In tangent space, z points out of the surface, so it is never negative. That keeps blue from dropping below 128.
- The shader turns tangent-space normals back into world space with the TBN matrix.
- Object-space normal maps are a rainbow, and much harder to reuse than tangent-space ones.
- The OpenGL and DirectX conventions differ only in the direction of the green channel.
- Since z is always positive, blue can be dropped and recomputed.
