How glTF Encodes Normal Maps in RGB Channels
This article provides an overview of how the glTF (GL Transmission Format) 2.0 specification encodes surface normal perturbation maps into RGB color textures. In glTF, normal maps simulate micro-surface detail by mapping tangent-space 3D vector coordinates into standard 2D image channels. The format uses an OpenGL-standard coordinate convention, storing normalized directional components across the red, green, and blue color channels, which runtime shaders decode into perturbed surface normals.
glTF normal maps are defined strictly in tangent space. In this local coordinate system, the surface normal points directly along the positive Z-axis, perpendicular to the polygon face, while the X and Y axes align with the tangent and bitangent vectors that run across the surface geometry along the UV coordinates.
The individual RGB color channels map directly to these spatial axes:
- Red Channel (R): Encodes the X component of the normal vector, representing horizontal perturbations along the tangent vector (left-to-right).
- Green Channel (G): Encodes the Y component of the normal vector, representing vertical perturbations along the bitangent vector. glTF explicitly follows the OpenGL convention, meaning the positive Y-axis points upward (UV V-coordinate increases upward). This contrasts with the DirectX convention, which uses a flipped, downward-pointing Y-axis.
- Blue Channel (B): Encodes the Z component of the normal vector, representing depth pointing outward from the surface geometry along the geometric normal. Because these vectors mostly point away from the surface, the B channel contains predominantly high values, giving normal maps their characteristic purplish-blue appearance.
Because image channels store values in an unsigned normalized range
of [0.0, 1.0], but directional vectors require signed
values in the range of [-1.0, 1.0], shaders must unpack the
sampled RGB values using linear scaling. The baseline conversion from
sampled color values (\(RGB\)) to the
tangent-space normal vector (\(N\)) is
calculated as:
\[N_{xyz} = 2.0 \times \text{Texture}_{RGB} - 1.0\]
The glTF specification also supports an optional scale
property on the normalTextureInfo object. This
floating-point multiplier modifies the intensity of the surface
perturbation without altering the base texture. When the
scale property is present, it is applied exclusively to the
X and Y channels prior to vector normalization:
\[N_x = (2.0 \times R - 1.0) \times \text{scale}\] \[N_y = (2.0 \times G - 1.0) \times \text{scale}\] \[N_z = 2.0 \times B - 1.0\] \[N_{\text{final}} = \text{normalize}([N_x, N_y, N_z])\]
Normal map textures in glTF must always be treated as linear data. Unlike base color textures, which are typically stored in sRGB space and converted to linear values upon sampling, normal textures contain mathematical vector data and must not undergo sRGB-to-linear transfer curve transformations.