How glTF Stores Relative Deltas for Morph Targets

glTF (GL Transmission Format) handles morph targets, commonly known as shape keys, by storing additive displacement vectors rather than full duplicate geometry. Instead of redefining entire meshes for each target pose, glTF defines relative positional deltas (\(\Delta\)) that measure the distance and direction vertices move from their base rest pose. This article explains how glTF structures these relative positional deltas, how accessors and sparse data structures optimize storage, and how real-time rendering engines compute the final deformed vertex positions.

The Mechanism of Additive Deltas

In 3D deformation, storing full vertex arrays for every target pose drastically inflates asset size and memory consumption. glTF 2.0 solves this by using additive morph targets. Each shape key stores only the difference between the base state and the deformed state:

\[\Delta P = P_{\text{target}} - P_{\text{base}}\]

During rendering, the graphics engine calculates the final position (\(P_{\text{final}}\)) of each vertex by multiplying each target's delta by its corresponding weight (\(w\)) and adding the sum to the original base position:

\[P_{\text{final}} = P_{\text{base}} + \sum_{i=0}^{n-1} (w_i \cdot \Delta P_i)\]

This approach allows multiple shape keys—such as blinking, smiling, and brow-raising—to blend simultaneously without conflicting vertex specifications.

The glTF Schema Structure

In a glTF file, morph targets are organized inside the mesh.primitives object under the targets array.

Each entry in the targets array is a dictionary mapping vertex attribute semantics to specific accessor objects:

{
  "meshes": [
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 0,
            "NORMAL": 1
          },
          "targets": [
            {
              "POSITION": 2,
              "NORMAL": 3
            },
            {
              "POSITION": 4
            }
          ]
        }
      ],
      "weights": [0.0, 0.0]
    }
  ]
}

In this structure:

  • The base primitive defines its rest geometry via attributes.POSITION (accessor 0).
  • Each dictionary in targets represents a distinct shape key.
  • The POSITION property inside a target (e.g., accessor 2) does not store absolute coordinates. Instead, it points to raw buffer data containing 3-component floating-point vectors (VEC3) that represent the \((dx, dy, dz)\) offsets relative to accessor 0.
  • The vertex count in each target accessor must exactly match the vertex count of the base attribute accessor, maintaining a direct 1:1 index alignment.

Data Types and Quantization

Positional deltas are typically stored as single-precision 32-bit floats (5126 / FLOAT). However, glTF 2.0 also allows signed integers to be used for deltas:

  • 5120 (BYTE)
  • 5122 (SHORT)

When signed integer types are used, the accessor must enable normalization ("normalized": true). This maps the minimum and maximum displacement range directly to \([-1.0, 1.0]\), offering a quantized data format that significantly reduces binary buffer size without requiring external extensions.

Optimizing Localized Deformations with Sparse Accessors

Most shape keys modify only a localized cluster of vertices. For instance, an eyelid blink leaves thousands of body and clothing vertices completely stationary with a delta of \((0, 0, 0)\).

To prevent storing extensive arrays of zeroed vectors, glTF supports sparse accessors (accessor.sparse). A sparse accessor constructs the target array virtually by combining:

  1. A default initialization filled with zeros.
  2. An indices buffer identifying only the specific vertex indices that move.
  3. A values buffer containing the corresponding \((dx, dy, dz)\) displacement deltas for those active indices.

When a client engine unpacks or streams the glTF primitive, it expands the sparse data into full GPU buffers or applies the sparse indices directly during compute skinning passes, keeping network transfer footprints and disk storage minimal.