Reconstruct Z from BC5 Normal Map in GLSL
BC5 compression optimizes texture memory by storing only two channels, typically representing the X and Y coordinates of a tangent-space normal vector. Because normalized 3D surface vectors have a constant unit length of one, the missing Z-component can be mathematically reconstructed in real time inside a fragment shader. This guide explains the core formula and provides the standard GLSL implementation used in modern graphics pipelines.
The Mathematical Formula
A normalized 3D vector satisfies the unit sphere equation:
\[x^2 + y^2 + z^2 = 1\]
Tangent-space normal maps assume that vectors point outward from the surface toward the positive Z direction, meaning \(z \ge 0\). By isolating \(z\), the equation becomes:
\[z = \sqrt{1 - (x^2 + y^2)}\]
Expressed using vector operations, the sum of squares \(x^2 + y^2\) is equivalent to the dot product of the 2D vector with itself (\(\mathbf{v}_{xy} \cdot \mathbf{v}_{xy}\)):
\[z = \sqrt{1 - (\mathbf{v}_{xy} \cdot \mathbf{v}_{xy})}\]
Texture Unpacking
BC5 textures store data in an unsigned normalized range of \([0.0, 1.0]\). Before deriving \(z\), the sampled red and green values must be remapped to signed tangent-space coordinates spanning \([-1.0, 1.0]\):
\[\mathbf{v}_{xy} = \text{sampled}_{rg} \times 2.0 - 1.0\]
GLSL Implementation
Texture filtering, mipmapping, and compression artifacts can
occasionally yield values where \(x^2 + y^2
> 1.0\). Computing the square root of a negative value
produces NaN, which creates visual black artifacts. To
prevent this, clamp the term inside the square root to zero:
// Sample the two-channel BC5 texture (stored in Red and Green)
vec2 normalXY = texture(u_NormalMap, v_TexCoord).rg;
// Remap from [0.0, 1.0] to [-1.0, 1.0]
normalXY = normalXY * 2.0 - 1.0;
// Reconstruct the Z-component
float normalZ = sqrt(max(0.0, 1.0 - dot(normalXY, normalXY)));
// Combine into the final tangent-space normal vector
vec3 normal = vec3(normalXY, normalZ);Using dot(normalXY, normalXY) computes \(x^2 + y^2\) in a single GPU instruction.
The max(0.0, ...) guard ensures mathematical stability
under all sampling conditions while keeping the reconstructed vector
normalized.