BC5 Normal Map Compression in Modern DirectX

BC5 texture compression optimizes tangent-space normal maps in modern DirectX rendering by dedicating two independent 8-bit compression channels exclusively to the X and Y normal vectors. Because tangent-space normal vectors are normalized to unit length, the Z component can be omitted entirely from the texture asset and reconstructed computationally in the pixel shader. This approach cuts texture memory usage in half relative to uncompressed formats, eliminates cross-channel compression crosstalk, and provides superior gradient fidelity compared to legacy block-compression schemes like BC1 or BC3.

Eliminating the Z-Axis Redundancy

A standard tangent-space normal map represents three-dimensional vector offsets \((X, Y, Z)\) relative to a surface's geometric normal. Because these vectors represent directions, they have a normalized length of 1:

\[\sqrt{X^2 + Y^2 + Z^2} = 1\]

In tangent space, normals almost universally point outward from the surface, meaning the \(Z\) component (the surface normal axis) is strictly non-negative (\(Z \ge 0\)). With this constraint, storing \(Z\) is mathematically redundant. The hardware only needs to sample \(X\) and \(Y\) from the texture, allowing the graphics pipeline to derive \(Z\) dynamically:

\[Z = \sqrt{1 - X^2 - Y^2}\]

Discarding the third channel instantly frees up memory without losing directional data, setting the foundation for two-channel compression.

The BC5 Data Structure

In the DirectX Graphics Infrastructure (DXGI), BC5 is exposed primarily via DXGI_FORMAT_BC5_UNORM and DXGI_FORMAT_BC5_SNORM. Architecturally, BC5 consists of two BC4 compression blocks joined together:

  • Footprint: A single BC5 block covers a 4x4 texel footprint and consumes 128 bits (16 bytes), yielding an effective rate of 8 bits per pixel (bpp).
  • Channel Independence: The first 64 bits encode the red channel (\(X\) vector), and the remaining 64 bits encode the green channel (\(Y\) vector).
  • Endpoint Interpolation: For each channel, the block stores two 8-bit endpoint values followed by a 4x4 lookup table of 3-bit indices. The GPU interpolates either six or eight intermediate values between the two endpoints depending on their numerical relationship.

Because the \(X\) and \(Y\) axes are handled by completely separate 64-bit sub-blocks, there is zero mathematical interference between the horizontal and vertical surface slopes.

Overcoming Legacy Compression Artifacts

Earlier rendering pipelines commonly packed normal maps into BC1 (DXT1) or BC3 (DXT5) formats, both of which introduce severe visual defects:

  1. Shared Color Palettes (BC1): BC1 compresses RGB channels together along a single 3D vector line in RGB color space. If a surface requires variations along the X axis that do not correlate directly with the Y axis, BC1 attempts to find an intermediate compromise, causing severe blockiness, color bleeding, and directional errors.
  2. DXT5nm Workarounds (BC3): A common legacy trick placed \(X\) in the BC3 Alpha channel (which has dedicated 8-bit endpoints similar to BC4) and \(Y\) in the Green channel, while discarding Red and Blue. While an improvement, the Green channel was still tied to the shared 5:6:5 RGB endpoint system, creating an imbalance in precision between axes.

BC5 completely resolves these issues. By granting both \(X\) and \(Y\) their own dedicated 8-bit precision endpoints and 3-bit index tables, gradients remain smooth, precision is evenly distributed across both dimensions, and lighting calculations remain stable under grazing view angles.

Reconstruction in the HLSL Pipeline

Modern DirectX pixel shaders sample BC5 textures using standard sampler states and perform the vector reconstruction using fast arithmetic logic unit (ALU) instructions.

When sampling DXGI_FORMAT_BC5_UNORM, values are read in the \([0, 1]\) range and must be remapped to the \([-1, 1]\) range. When using DXGI_FORMAT_BC5_SNORM, the hardware delivers values directly in the signed \([-1, 1]\) range, skipping manual remapping.

A standard HLSL implementation using an unsigned BC5 texture proceeds as follows:

Texture2D normalMap : register(t0);
SamplerState defaultSampler : register(s0);

float3 ReconstructNormal(float2 uv)
{
    // Sample X and Y from the two-channel BC5 texture
    float2 normalXY = normalMap.Sample(defaultSampler, uv).rg;

    // Unpack from [0, 1] to [-1, 1]
    normalXY = normalXY * 2.0f - 1.0f;

    // Reconstruct the Z component
    float normalZ = sqrt(saturate(1.0f - dot(normalXY, normalXY)));

    return float3(normalXY, normalZ);
}

Modern GPU architectures execute the dot, saturate, and sqrt (or reciprocal square root rsqrt) instructions in a fraction of a clock cycle. Offloading the storage of the third channel to the ALU represents an optimal trade-off: memory bandwidth—historically the primary bottleneck in real-time rendering—is conserved at the cost of a virtually unnoticeable compute footprint.