Synchronizing glTF Morph Target Deltas

In glTF 2.0, morph target animations synchronize delta positions, normals, and tangents by linking them under unified target indices governed by a shared array of scalar weights. Rather than animating spatial channels individually, glTF animation samplers drive a single weights array on the target node. Each weight simultaneously scales the corresponding position, normal, and tangent deltas for a given morph target, ensuring that geometric displacement and surface orientation remain perfectly aligned during deformation.

Structural Pairing via Primitive Targets

In the glTF asset structure, a mesh primitive defines its base geometry in its attributes dictionary (POSITION, NORMAL, TANGENT). Morph targets are defined within the targets array of that primitive. Each element in the targets array represents a distinct morph target state and contains accessors for the respective deltas:

  • POSITION: Stores the [x, y, z] displacement vector relative to the base position.
  • NORMAL: Stores the [x, y, z] normal vector shift relative to the base normal.
  • TANGENT: Stores a 3D [x, y, z] tangent vector shift relative to the base tangent, leaving the 4th component (the sign/handedness) to be inherited directly from the base TANGENT attribute.

Because the position, normal, and tangent deltas are bundled together inside the same index in the targets list, they represent a coupled set of modifications for that target index.

Single-Weight Modulation

glTF synchronizes these components during playback through animation channels that target the weights path of a node. An animation sampler outputs an array of floating-point values at each keyframe, where the length of the array matches the number of targets defined in the mesh.

For target index i, a single scalar value w[i] drives all associated delta accessors at the same time:

  • \(\Delta P_i = \text{Target}[i].\text{POSITION}\)
  • \(\Delta N_i = \text{Target}[i].\text{NORMAL}\)
  • \(\Delta T_i = \text{Target}[i].\text{TANGENT}\)

Because the runtime evaluation uses identical timing and interpolation for the target weight, none of the individual attribute layers can drift out of temporal phase.

Vertex Shader Blending Formulation

During vertex processing, the base attributes and delta values are accumulated linearly using the active weight values. The standard formula applied across all synchronized attributes is:

  • Position: \(P = P_{\text{base}} + \sum_{i} (w_i \cdot \Delta P_i)\)
  • Normal: \(N = \text{normalize}\left(N_{\text{base}} + \sum_{i} (w_i \cdot \Delta N_i)\right)\)
  • Tangent: \(T_{xyz} = \text{normalize}\left(T_{\text{base}.xyz} + \sum_{i} (w_i \cdot \Delta T_i)\right)\)

The accumulated normal and tangent vectors are re-normalized after blending to counteract any length distortion introduced by linear delta additions. The tangent's bitangent sign (\(T_{\text{base}.w}\)) is preserved to construct the orthonormal tangent-bitangent-normal (TBN) matrix for normal mapping.

GPU Implementation Strategies

Runtimes typically execute this synchronization on the GPU through two methods:

  1. Vertex Attributes: Engines supply target deltas as additional vertex attributes and pass the weight array as a uniform buffer to the vertex shader. The vertex shader performs parallel multiply-add operations for positions, normals, and tangents within the same invocation.
  2. Texture Buffers (Morph Textures): When the target count exceeds GPU attribute limits, engines pack all position, normal, and tangent deltas into 2D floating-point texture arrays. The vertex shader samples the texture coordinate for vertex ID across all active target slices, scales the fetched deltas by w[i], and aggregates the result in lockstep before transforming vertices into clip space.