How glTF Assigns Joint Indices Across Vertex Streams

This article explains how the glTF (GL Transmission Format) specification handles skeletal skinning when vertices are influenced by more than four bones. It covers the mechanics of partitioning joint data across multiple vertex attribute streams, how joint indices map directly to a model's skin hierarchy, and the requirements for corresponding weights to ensure proper hardware-accelerated deformation.

The 4-Component Attribute Stream Limit

In glTF, vertex attributes for skinning are defined using 4-component vectors (VEC4). A single stream set consists of:

  • JOINTS_0: Stores the indices of the bones influencing the vertex.
  • WEIGHTS_0: Stores the linear blend skinning weights corresponding to those bones.

Because standard graphics hardware traditionally processes vertex attributes in 4-component vectors, four bone influences per vertex are the default baseline. When a mesh requires more than four bone influences per vertex (e.g., 5 to 8 bones), glTF expands this system by adding secondary attribute streams: JOINTS_1 and WEIGHTS_1. If 9 to 12 influences are needed, JOINTS_2 and WEIGHTS_2 are introduced, continuing incrementally (JOINTS_n, WEIGHTS_n).

Joint Index Resolution

Each joint index inside every JOINTS_n stream directly references an index in the joints array of the associated skin object, not the global node array of the glTF document.

  1. The Skin Palette: A glTF skin defines a joints array containing references to node IDs (e.g., "joints": [ 10, 11, 12, 13, 14 ]).
  2. Direct Indexing: An index value of 3 in either JOINTS_0 or JOINTS_1 refers to the fourth entry in that specific skin.joints array (node ID 13 in this example).
  3. No Stream-Based Offsets: Joint indices in secondary streams (JOINTS_1, JOINTS_2) do not automatically offset based on previous streams. A joint index of 2 in JOINTS_1 points to index 2 of the skin.joints array, exactly as it would in JOINTS_0.

Pairing and Data Types

For skinning to function correctly, joint and weight attributes must follow strict pairing and formatting constraints:

  • Index-Weight Correspondence: The k-th component of JOINTS_n is strictly tied to the k-th component of WEIGHTS_n. For example, JOINTS_1.y is weighted by WEIGHTS_1.y.
  • Joint Data Types: Attributes under JOINTS_n must use either UNSIGNED_BYTE (0–255) or UNSIGNED_SHORT (0–65535). Floating-point formats are not permitted for joint indices.
  • Weight Data Types: WEIGHTS_n can be stored as FLOAT, normalized UNSIGNED_BYTE, or normalized UNSIGNED_SHORT.

Normalization and Unused Influences

When using multiple vertex streams, weights must be normalized across all active streams simultaneously:

  • Cumulative Sum: The sum of all weights across all streams for a single vertex (\(\sum \text{WEIGHTS\_0} + \sum \text{WEIGHTS\_1} + \dots\)) must equal 1.0.
  • Padding Unused Slots: If a vertex only uses 5 bones, it requires both JOINTS_0/WEIGHTS_0 (4 bones) and JOINTS_1/WEIGHTS_1 (1 bone). The remaining three unused components in JOINTS_1 and WEIGHTS_1 must be padded. The unused weight components must be set to 0.0. The unused joint indices are typically set to 0 or any valid index within the skin.joints range to avoid out-of-bounds errors during shader execution.