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 baseTANGENTattribute.
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:
- 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.
- 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
IDacross all active target slices, scales the fetched deltas byw[i], and aggregates the result in lockstep before transforming vertices into clip space.