How glTF Handles Rest Poses and Animation Keyframes
In the glTF (GL Transmission Format) specification, rest poses and animated states are decoupled into distinct data structures to optimize runtime performance and memory efficiency. This article details how glTF separates resting geometry and joint hierarchies using node transforms and inverse bind matrices, explains how animation channels dynamically alter these properties, and outlines how the runtime skinning pipeline evaluates final vertex transformations relative to the original bind pose.
The Rest Pose: Node Transforms and Inverse Bind Matrices
A model's rest state (often called the bind pose) in glTF is not stored as a separate animation track. Instead, it is defined through two complementary structures: the default transforms in the node hierarchy and the inverse bind matrices assigned to the mesh's skin.
Default Node Hierarchy Transforms
Every bone in a glTF skeleton is represented as a standard
node. When no animation is playing, a node's baseline
transform is defined explicitly by its translation,
rotation, and scale (TRS) properties, or by a
single 4x4 transformation matrix.
These properties define the local rest transform of each joint relative to its parent node. Traversing this hierarchy from the skeleton's root to the leaf joints yields the global rest pose of the skeleton in model space.
Inverse Bind Matrices
While the node hierarchy defines how joints relate to each other, the
skin element defines how joints relate to the mesh
vertices. A glTF skin references:
- A
jointsarray: An indexed list of node IDs acting as bones. - An
inverseBindMatricesproperty: An accessor pointing to an array of 4x4 floating-point matrices, one for each joint.
The inverse bind matrix (IBM) transforms vertex positions from the
mesh’s local coordinate space into the local coordinate space of a given
joint at the exact moment the mesh was bound to the skeleton. Because
vertex attributes (POSITION, NORMAL) are
stored in mesh space, the IBM acts as the static reference point for all
subsequent deformation.
Animated Keyframe States
Animations in glTF do not alter vertex data or inverse bind matrices directly. Instead, they target the local TRS properties of the skeleton's nodes over time.
An entry in the animations array contains two primary
structures:
- Samplers: These store the raw keyframe data. A
sampler references an input accessor (an array of timestamps in seconds)
and an output accessor (the corresponding values for translation,
rotation, scale, or morph weights), along with an interpolation method
(
LINEAR,STEP, orCUBICSPLINE). - Channels: These connect a sampler's output to a
target. A channel specifies the target node index and the exact property
being modified (
translation,rotation,scale, orweights).
When an animation is evaluated at time \(t\), the runtime interpolates the keyframes defined in the sampler and overwrites or replaces the target node's default local TRS values. The rest pose values on the node are simply the initial states used when no animation channel is targeting that specific property.
Runtime Evaluation and Skinning Pipeline
To deform a skinned mesh during animation, a glTF runtime evaluates how far each joint has moved from its initial bind state. The calculation proceeds as follows:
- Evaluate Local Transforms: For the current playback time, active animation channels update the local TRS values of the skeleton nodes. Unanimated channels retain their default rest pose TRS values.
- Compute Global Joint Transforms: The engine computes the world (or model-space) matrix for every joint by multiplying local transforms down the hierarchy from the skeleton root: \[\text{GlobalTransform}_{\text{joint}} = \text{GlobalTransform}_{\text{parent}} \times \text{LocalTransform}_{\text{joint}}\]
- Compute the Final Joint Matrix: The runtime calculates the deformation matrix (\(M_{\text{joint}}\)) by multiplying the animated global transform by the static inverse bind matrix: \[M_{\text{joint}} = \text{GlobalTransform}_{\text{joint}} \times \text{InverseBindMatrix}_{\text{joint}}\]
- Vertex Blending: In the vertex shader, each vertex
is transformed by a weighted sum of the joint matrices assigned to it
via the mesh's
JOINTS_0andWEIGHTS_0vertex attributes: \[\text{Position}_{\text{skinned}} = \sum_{i} \left( \text{Weight}_i \times (M_{\text{joint}_i} \times \text{Position}_{\text{mesh}}) \right)\]
If a skeleton is rendered in its rest pose—meaning the current global joint transforms match the bind pose—the product of \(\text{GlobalTransform}_{\text{joint}}\) and \(\text{InverseBindMatrix}_{\text{joint}}\) yields an identity matrix (assuming uniform coordinate spaces). This results in zero net deformation, rendering the mesh in its original, authored shape.