How glTF Represents Blend Shapes for Deformation

In glTF (GL Transmission Format), non-rigid mesh deformations such as facial expressions and organic flexing are achieved using morph targets, commonly known as vertex blend shapes. Rather than storing full duplicates of the geometry for every variation, glTF handles blend shapes by storing relative displacement vectors alongside the base mesh attributes, which runtime engines combine using a series of scalar weights. This article explains the data structure, math, and animation mechanisms glTF uses to efficiently encode and render these non-rigid deformations.

The Base Primitive and Morph Targets

In the glTF JSON schema, 3D geometry is organized under meshes, where each mesh contains one or more primitives. A standard primitive holds core vertex attributes—such as POSITION, NORMAL, and TANGENT—defined by accessors that point to the underlying binary buffer data.

To support blend shapes, a primitive includes an optional targets array. Each entry in the targets array represents a distinct morph target and contains an object mapping attribute names to accessor IDs. The attributes supported in morph targets are:

  • POSITION: Stores 3D displacement vectors (offsets).
  • NORMAL: Stores normal displacement vectors.
  • TANGENT: Stores XYZ tangent displacement vectors.

Additive Delta Storage

A critical aspect of glTF's representation is that morph target data is strictly stored as deltas (displacements) rather than absolute coordinates. For example, if a vertex at position \((0, 5, 0)\) moves to \((0, 6, 0)\) in a given expression, the morph target stores \((0, 1, 0)\).

Storing relative differences provides significant advantages:

  • It simplifies blending, allowing multiple morph targets to be applied simultaneously.
  • It improves compression efficiency because many target vertices remain stationary, yielding displacement values of zero.

To optimize bandwidth further, glTF implementations frequently use sparse accessors (accessor.sparse). Sparse accessors only record vertices that deviate from the neutral state, omitting untouched vertices entirely.

The Blending Formulation

The deformation state at runtime is governed by an array of scalar float values called weights. These weights can be defined globally on the mesh as default values or overridden at the node level via node.weights.

The final transformed attribute is computed using additive blending:

\[\text{Attribute}_{\text{final}} = \text{Attribute}_{\text{base}} + \sum_{i=0}^{n-1} (w_i \times \Delta\text{Attribute}_i)\]

Where:

  • \(\text{Attribute}_{\text{base}}\) is the neutral vertex position, normal, or tangent.
  • \(w_i\) is the scalar weight for target \(i\) (typically clamped between \(0.0\) and \(1.0\), though negative values and overshooting values above \(1.0\) are mathematically valid).
  • \(\Delta\text{Attribute}_i\) is the displacement vector for target \(i\).

After computing final normals and tangents, shaders typically re-normalize the vectors to correct for non-linear interpolation distortions.

Animating Blend Shapes

glTF integrates blend shape control directly into its core animation system. To animate a morph target over time:

  1. An animation channel designates a target pointing to a specific node.
  2. The channel's path is set to "weights".
  3. An animation sampler provides the timeline inputs and the corresponding scalar array outputs (one weight per morph target at each keyframe).
  4. Engines interpolate these weights (via LINEAR, STEP, or CUBICSPLINE interpolation) and apply the resulting values to the target node during the frame update.