KHR_texture_transform UV Mapping in glTF
The KHR_texture_transform extension in glTF provides a
standardized way to modify UV texture coordinates directly within
material definitions without altering the underlying mesh geometry. By
allowing runtime adjustments to texture scale, offset, and rotation,
this extension reduces file size, enables texture reuse across different
materials, and avoids duplicating vertex data for simple texture
variations.
Core Properties of KHR_texture_transform
The extension is added directly to a glTF textureInfo
block (such as baseColorTexture or
metallicRoughnessTexture). It introduces four main
properties:
offset: An array of two floating-point numbers[u, v]defining the translation of the texture coordinates. Defaults to[0.0, 0.0].scale: An array of two floating-point numbers[u, v]defining the scaling factor (tiling/repetition) along each axis. Defaults to[1.0, 1.0].rotation: A single floating-point number representing the rotation angle in radians, applied counter-clockwise around the UV origin[0, 0]. Defaults to0.0.texCoord: An integer indicating which set of texture coordinates (TEXCOORD_0,TEXCOORD_1, etc.) to use. If omitted, the texture defaults to thetexCoordindex defined on the parenttextureInfoobject.
Transformation Order and Mathematics
To apply the transform correctly, runtime engines construct a 3×3 affine transformation matrix. The operations must be calculated in a specific order: Scale, followed by Rotation, followed by Translation (Offset).
Expressed as matrix multiplication applied to a column vector
[u, v, 1]:
\[\begin{bmatrix} u' \\ v' \\ 1 \end{bmatrix} = T \times R \times S \times \begin{bmatrix} u \\ v \\ 1 \end{bmatrix}\]
Expanding the matrices:
- Scale (\(S\)): \[\begin{bmatrix} \text{scale}_u & 0 & 0 \\ 0 & \text{scale}_v & 0 \\ 0 & 0 & 1 \end{bmatrix}\]
- Rotation (\(R\)): \[\begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}\]
- Translation (\(T\)): \[\begin{bmatrix} 1 & 0 & \text{offset}_u \\ 0 & 1 & \text{offset}_v \\ 0 & 0 & 1 \end{bmatrix}\]
Multiplying these matrices produces the final 3×3 transformation matrix:
\[\begin{bmatrix} \text{scale}_u \cos(\theta) & -\text{scale}_v \sin(\theta) & \text{offset}_u \\ \text{scale}_u \sin(\theta) & \text{scale}_v \cos(\theta) & \text{offset}_v \\ 0 & 0 & 1 \end{bmatrix}\]
Handling Specific Transformations
UV Scaling
Increasing the scale values compresses the texture
mapping across the UV domain, resulting in repetitive tiling if the
texture sampler is set to REPEAT. Non-uniform scaling
(e.g., [2.0, 1.0]) stretches or squashes the texture along
individual axes. Negative values invert the coordinates across the
respective axis.
UV Offset
The offset shifts the sampling window across the
texture. An offset of [0.5, 0.0] shifts the texture
horizontally by half its width. Because translation occurs last in the
transformation pipeline, the offset vector directly positions the
already scaled and rotated coordinate space.
UV Rotation and Origin Handling
Rotation is evaluated about the UV coordinate system's origin
[0, 0] (typically the top-left or bottom-left corner
depending on API coordinate conventions; glTF defines
[0, 0] as the upper-left corner of the texture).
Because rotation pivots around [0, 0], rotating around a
custom point, such as the texture center [0.5, 0.5],
requires calculating a compensatory offset:
- Translate the center to the origin (
-0.5, -0.5). - Apply rotation and scale.
- Translate the center back (
+0.5, +0.5).
The resulting net translation must be stored in the
offset property to achieve centered rotation.
glTF JSON Structure Example
Below is a standard glTF material snippet implementing
KHR_texture_transform:
{
"materials": [
{
"name": "TransformedMaterial",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0,
"extensions": {
"KHR_texture_transform": {
"offset": [0.2, 0.0],
"scale": [2.0, 2.0],
"rotation": 1.57079632679,
"texCoord": 0
}
}
}
}
}
]
}In modern shaders, this 3×3 matrix can either be precomputed on the CPU and passed as a uniform, or computed directly in vertex or fragment stages, allowing efficient texture manipulation with minimal performance overhead.