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 to 0.0.
  • texCoord: An integer indicating which set of texture coordinates (TEXCOORD_0, TEXCOORD_1, etc.) to use. If omitted, the texture defaults to the texCoord index defined on the parent textureInfo object.

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:

  1. Translate the center to the origin (-0.5, -0.5).
  2. Apply rotation and scale.
  3. 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.