glTF Multiple UV Sets for Multi-Layered Materials
The glTF (GL Transmission Format) specification handles complex, multi-layered materials by separating texture coordinates at the vertex level and mapping them individually at the material level. By defining indexed UV channels in mesh primitives and referencing them through specific material property objects, glTF allows different texture layers—such as base color, ambient occlusion, lightmaps, and extension-driven coatings—to utilize independent UV channels and transformations. This decoupled architecture enables detailed, multi-layered surface rendering without duplicating underlying mesh geometry.
Mesh Primitive UV Definitions
In glTF, texture coordinates are stored as vertex attributes within
the primitives array of a mesh. They follow a strict naming
convention formatted as TEXCOORD_n, where n is
an integer index starting from 0:
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2,
"TEXCOORD_1": 3
}TEXCOORD_0: Typically assigned to the primary UV unwrap, used for the main surface maps (base color, normal, metallic-roughness).TEXCOORD_1and beyond: Assigned to secondary layouts, often utilized for tiled detail textures, baking ambient occlusion, or decoupled lightmapping.
The glTF specification requires client implementations to support at
least two UV sets (TEXCOORD_0 and TEXCOORD_1),
though modern engines often support more depending on GPU hardware
limits.
Material Property
Mapping via texCoord
glTF links a texture to a specific UV set via the
textureInfo object. Every texture slot in a glTF
material—including base color, metallic-roughness, normal maps,
occlusion maps, and emissive maps—contains an optional
texCoord integer property.
If omitted, texCoord defaults to 0.
Specifying an explicit integer tells the renderer which
TEXCOORD_n attribute to read for that specific texture:
"materials": [
{
"name": "MultiUV_Material",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0
}
},
"occlusionTexture": {
"index": 1,
"texCoord": 1
}
}
]In this setup, the base color evaluates using the unique layout in
TEXCOORD_0, while the baked ambient occlusion evaluates
using the non-overlapping lightmap unwrap in
TEXCOORD_1.
Multi-Layered Materials and glTF Extensions
Standard PBR in glTF core is often enhanced by extensions to
represent multi-layered materials like car paint, textiles, or lacquered
wood. Extensions such as KHR_materials_clearcoat,
KHR_materials_sheen, and
KHR_materials_transmission follow the exact same
textureInfo pattern.
For example, a clearcoat layer can use a secondary UV coordinate to drive scratches or dust independently of the underlying base paint:
"extensions": {
"KHR_materials_clearcoat": {
"clearcoatFactor": 1.0,
"clearcoatRoughnessTexture": {
"index": 2,
"texCoord": 1
}
}
}Each material layer can sample whichever UV set best preserves its resolution and mapping needs.
Independent Tiling
via KHR_texture_transform
To further enhance UV management without adding additional geometry
data, glTF utilizes the KHR_texture_transform extension.
This extension allows per-texture scaling, offset, and rotation.
When applied, it can also override the target UV channel using its
own texCoord property. This allows two different texture
layers to share TEXCOORD_0 at the vertex level, while
sampling at completely different scales and orientations in the
shader:
"baseColorTexture": {
"index": 0,
"texCoord": 0,
"extensions": {
"KHR_texture_transform": {
"scale": [10.0, 10.0]
}
}
}Technical Considerations and Best Practices
- Attribute Overhead: Each additional
TEXCOORD_nadds aVEC2(typically 8 bytes per vertex using 32-bit floats, or 4 bytes using normalized 16-bit integers). Keep UV sets to the minimum necessary for runtime performance. - Fallback Behavior: If a material requests a
texCoordindex that does not exist in the mesh'sattributesmap, glTF conforming engines must default toTEXCOORD_0. - Shader Variants: Varying the
texCoordindices across different textures can increase the number of shader permutations in some game engines, as shaders must compile dynamically to route inputs to specific UV channels.