# How Godot's Animation System Processes Keyframes: AnimationPlayer and AnimationTree Deep Dive

> Discover how Godot's animation system processes keyframes. Learn how AnimationPlayer and AnimationTree interpolate and blend animations to bring your scenes to life.

- Repository: [Godot Engine/godot](https://github.com/godotengine/godot)
- Tags: deep-dive
- Published: 2026-02-26

---

**Godot's animation system processes keyframes by interpolating between stored values in the `Animation` resource, then blending results through `AnimationPlayer` for single tracks or `AnimationTree` for complex graph-based mixing, ultimately applying transformed values to scene nodes each frame.**

The Godot Engine (godotengine/godot) handles **Godot animation system keyframes** through a sophisticated three-layer pipeline that transforms raw track data into smooth scene updates. Whether you're using `AnimationPlayer` for straightforward playback or `AnimationTree` for state-machine-driven blending, the underlying interpolation and blending logic remains consistent and deterministic.

## The Three-Layer Animation Architecture

Godot's animation workflow operates through tightly coupled layers that separate data storage from playback logic:

| Layer | Role | Core Class | Main Responsibilities |
|-------|------|------------|----------------------|
| **Animation Resource** | Stores raw keyframe data | `Animation` | Manages tracks, interpolates values, applies compression, provides `_interpolate` helpers |
| **AnimationPlayer** | Plays single animations | `AnimationPlayer` (inherits `AnimationMixer`) | Manages playback state, blending, looping, seeking, dispatches values to scene |
| **AnimationTree** | Graph-based blending | `AnimationTree` (inherits `AnimationMixer`) | Traverses animation nodes, builds property maps, writes blended values |

When processed each frame, the system computes current time and speed, determines the keyframe range based on loop mode, fetches relevant keys from compressed or uncompressed storage, interpolates using the specified method, blends multiple animations if necessary, and writes results to target properties.

## Low-Level Keyframe Interpolation in the Animation Resource

All raw keyframe data lives in [`scene/resources/animation.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/animation.cpp). The central helper that transforms key pairs into values is the templated `_interpolate` method.

### The Interpolation Pipeline

The heavy lifting occurs in `Animation::_interpolate` (lines 2457-2510), which selects routines based on track type:

- **Vector3 tracks** – Use `Animation::_interpolate(const Vector3 &a, const Vector3 &b, real_t c)`
- **Quaternion tracks** – Use spherical interpolation for rotation data
- **Variant tracks** – Generic fallback forwarding to scalar interpolation

For key vector processing, the system uses:

```cpp
template <class K>
K Animation::_interpolate(const LocalVector<TKey<K>> &p_keys,
                          double p_time,
                          InterpolationType p_interp,
                          bool p_loop_wrap,
                          bool *p_ok,
                          bool p_backward) const;

```

This implementation (lines 2524-2540 in [`animation.cpp`](https://github.com/godotengine/godot/blob/main/animation.cpp)) finds surrounding keys via `_find`, then calls scalar `_interpolate` or `_cubic_interpolate_in_time` depending on the interpolation mode.

### Compression and Continuous Updates

Compressed tracks utilize specialized methods like `_pos_scale_interpolate_compressed` and `_rotation_interpolate_compressed` (lines 5303-5326). For continuous value tracks, `UPDATE_CONTINUOUS` disables discrete snapping to guarantee smooth motion between keyframes.

## AnimationPlayer: Single Animation Playback Engine

`AnimationPlayer` orchestrates per-frame processing of individual animations through [`scene/animation/animation_player.cpp`](https://github.com/godotengine/godot/blob/main/scene/animation/animation_player.cpp).

### Frame Processing Entry Point

The pipeline begins with `_blend_pre_process` (line 64):

```cpp
bool AnimationPlayer::_blend_pre_process(double p_delta, int p_track_count, const AHashMap<NodePath, int> &p_track_map) {
    // ...
    _process_playback_data(c.current, p_delta, get_current_blend_amount(),
                           seeked, internal_seeked, p_started, true);
}

```

This builds a `PlaybackInfo` struct containing delta time, blend weight, and flags indicating whether the animation was just started or seeked.

### Core Playback Logic

`_process_playback_data` (lines 59-86) handles the main computation:

```cpp
void AnimationPlayer::_process_playback_data(PlaybackData &cd,
                                            double p_delta,
                                            float p_blend,
                                            bool p_seeked,
                                            bool p_internal_seeked,
                                            bool p_started,
                                            bool p_is_current) {
    double speed = speed_scale * cd.speed_scale;
    bool backwards = std::signbit(speed);
    double delta = p_started ? 0 : p_delta * speed;
    double next_pos = cd.pos + delta;
    
    // Loop mode handling
    switch (p_from->animation->get_loop_mode()) { /* ... */ }
    
    // Build PlaybackInfo and create animation instance
    PlaybackInfo pi;
    // ... populate pi ...
    make_animation_instance(cd.animation_name, pi);
}

```

The method implements loop strategies through a switch on `get_loop_mode()`: clamp for `LOOP_NONE`, modulo for `LOOP_LINEAR`, and ping-pong for `LOOP_PINGPONG`. The `make_animation_instance` call creates an animation instance that invokes `Animation::_process_animation`, which internally calls the `_interpolate` functions.

### Blending Multiple Animations

When blending is active, `_blend_playback_data` iterates over the `c.blend` list, reduces each blend's remaining time, and processes them using `_process_playback_data`. Final values sum according to their blend weights (lines 85-100 in [`animation_player.cpp`](https://github.com/godotengine/godot/blob/main/animation_player.cpp)).

## AnimationTree: Graph-Based Keyframe Blending

`AnimationTree` adds a node-graph layer on top of `AnimationPlayer` functionality through [`scene/animation/animation_tree.cpp`](https://github.com/godotengine/godot/blob/main/scene/animation/animation_tree.cpp).

### Graph Pre-Processing

The tree processes all nodes before applying any values. `_blend_pre_process` (lines 40-70) initializes the traversal:

```cpp
bool AnimationTree::_blend_pre_process(double p_delta,
                                       int p_track_count,
                                       const AHashMap<NodePath, int> &p_track_map) {
    _update_properties(); // rebuild property map if needed
    process_state = AnimationNode::ProcessState();
    process_state.tree = this;
    process_state.valid = true;
    
    // Initialize root node weights
    root_animation_node->node_state.track_weights.resize(p_track_count);
    for (int i = 0; i < p_track_count; i++) {
        root_animation_node->node_state.track_weights[i] = 1.0;
    }
    
    // Start recursive walk
    PlaybackInfo pi;
    if (started) { pi.seeked = true; pi.delta = p_delta; }
    root_animation_node->_pre_process(&process_state, pi, false);
    return process_state.valid;
}

```

The `process_state` holds a property map (`property_map`) receiving all interpolated values from every node. Each node's `node_state.track_weights` determines how much of each animation track contributes to the final output.

### Node Processing Pipeline

`AnimationNode::_pre_process` (lines 146-149) handles individual nodes:

```cpp
AnimationNode::NodeTimeInfo AnimationNode::_pre_process(ProcessState *p_process_state,
                                                       const PlaybackInfo &p_playback_info,
                                                       bool p_test_only) {
    process_state = p_process_state;
    NodeTimeInfo nti = process(p_playback_info, p_test_only);
    process_state = nullptr;
    return nti;
}

```

Concrete node types like `AnimationNodeBlendTree` or `AnimationNodeStateMachine` override the virtual `process` method. During processing, nodes read and write entries in `process_state->property_map` using helpers like `set_property` and `get_property`.

### Final Application

After the graph walk completes, `AnimationTree::_process_animation` (inherited from `AnimationMixer`) writes the collected property values to scene nodes. This path mirrors `AnimationPlayer`'s `make_animation_instance`, ultimately calling the engine's `Object::set` on the targeted `NodePath`.

## Execution Order: From Keyframe to Scene Update

The complete processing pipeline follows this deterministic order each frame:

| Step | Called By | Operation |
|------|-----------|-----------|
| 1 | Engine `_process(delta)` | Invokes `_blend_pre_process` on active mixer |
| 2 | `_blend_pre_process` | Creates `PlaybackInfo`, updates loop/seek state, initializes blend weights |
| 3 | `AnimationPlayer` → `_process_playback_data` or `AnimationTree` → root `_pre_process` | Queries `Animation::_process_animation` for track values at current time |
| 4 | `Animation` → `_interpolate` | Finds surrounding keys, applies interpolation (nearest/linear/cubic) |
| 5 | `AnimationPlayer` → blend handling or `AnimationTree` → property map merging | Combines multiple animations according to blend weights |
| 6 | Engine property application | Writes final values via `Object::set(property_path, value)` to target nodes |

This architecture ensures frame-rate independence through `speed_scale` and `delta` time calculations while maintaining deterministic playback across all platforms.

## Practical Code Examples

### Playing a Simple Animation with AnimationPlayer

```gdscript
func _ready():
    var player = $AnimationPlayer
    # Load and register animation

    var anim = preload("res://character_walk.tres")
    player.add_animation("walk", anim)
    
    # Start playback - triggers _blend_pre_process each frame

    player.play("walk")
    player.speed_scale = 1.5  # Affects delta calculation in _process_playback_data

```

Behind the scenes, `play()` initiates the pipeline: `_blend_pre_process` → `_process_playback_data` → `Animation::_interpolate` (see [`animation_player.cpp`](https://github.com/godotengine/godot/blob/main/animation_player.cpp) lines 59-86 and [`animation.cpp`](https://github.com/godotengine/godot/blob/main/animation.cpp) lines 2457-2524).

### Blending Animations with AnimationTree

```gdscript
func _ready():
    var tree = $AnimationTree
    tree.active = true
    
    # Access Blend2 node parameter

    tree.set("parameters/Blend2/blend_amount", 0.0)

func _on_switch_blend():
    var current = $AnimationTree.get("parameters/Blend2/blend_amount")
    $AnimationTree.set("parameters/Blend2/blend_amount", 
                      clamp(current + 0.2, 0.0, 1.0))

```

The `AnimationTree` builds a `ProcessState` with a property map ([`animation_tree.cpp`](https://github.com/godotengine/godot/blob/main/animation_tree.cpp) lines 40-70). The `Blend2` node's `process` method writes interpolated values to this map, which are then applied to the scene after the graph traversal completes.

### Adding Keyframes at Runtime in C++

```cpp
Ref<Animation> anim = memnew(Animation);
int track = anim->add_track(Animation::TYPE_POSITION_3D);
anim->track_set_path(track, NodePath("Character:transform/origin"));

// Insert keyframes that will be interpolated each frame
anim->position_track_insert_key(track, 0.0, Vector3(0, 0, 0));
anim->position_track_insert_key(track, 1.0, Vector3(5, 0, 0));

// Attach to player
animation_player->add_animation("run", anim);
animation_player->play("run");

```

This creates an `Animation` resource with position keys that the engine interpolates using `Animation::_interpolate` (see [`animation.cpp`](https://github.com/godotengine/godot/blob/main/animation.cpp) lines 954-961 and 2524-2540).

## Key Source Files

| File | Purpose | Key Functions |
|------|---------|---------------|
| [`scene/resources/animation.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/animation.cpp) | Core animation data structure and interpolation | `_interpolate`, `_cubic_interpolate_in_time`, `position_track_insert_key` |
| [`scene/animation/animation_player.cpp`](https://github.com/godotengine/godot/blob/main/scene/animation/animation_player.cpp) | Single animation playback and blending | `_blend_pre_process`, `_process_playback_data`, `make_animation_instance` |
| [`scene/animation/animation_tree.cpp`](https://github.com/godotengine/godot/blob/main/scene/animation/animation_tree.cpp) | Graph-based animation blending | `_blend_pre_process`, `ProcessState` management, property map building |
| [`scene/animation/animation_node.h`](https://github.com/godotengine/godot/blob/main/scene/animation/animation_node.h) | Base class for animation nodes | `_pre_process`, `process`, track weight handling |

These files constitute the complete pipeline that transforms stored keyframe data into animated property changes each engine tick.

## Summary

- **Godot animation system keyframes** are stored in the `Animation` resource class, which provides type-specific interpolation via `_interpolate` methods for Vector3, Quaternion, and Variant data types.
- **AnimationPlayer** handles single-animation playback by calculating time deltas, applying loop modes (none, linear, ping-pong), and dispatching interpolated values to scene nodes through `_process_playback_data`.
- **AnimationTree** extends this with graph-based blending, using a `ProcessState` property map to accumulate contributions from multiple `AnimationNode` instances before applying the final result.
- All interpolation supports multiple modes including nearest, linear, and cubic, with compressed tracks handled through specialized decompression routines.
- The entire pipeline is frame-rate independent, using `speed_scale` and delta time calculations to ensure consistent playback across different hardware.

## Frequently Asked Questions

### How does Godot interpolate between keyframes?

Godot uses the `_interpolate` template method in [`scene/resources/animation.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/animation.cpp) (lines 2524-2540) to find surrounding keys and calculate intermediate values. The system supports **nearest**, **linear**, and **cubic** interpolation modes, with specialized handling for Vector3 positions and Quaternion rotations to ensure proper spatial and rotational blending.

### What is the difference between AnimationPlayer and AnimationTree processing?

**AnimationPlayer** processes a single animation through `_process_playback_data`, calculating time progression and applying values directly to nodes. **AnimationTree** uses `_blend_pre_process` to traverse a graph of `AnimationNode` instances, building a property map that accumulates weighted contributions from multiple animations before writing the final blended values to the scene.

### How does loop mode affect keyframe processing?

The loop mode determines how `_process_playback_data` calculates the current time position. `LOOP_NONE` clamps playback to the start/end boundaries, `LOOP_LINEAR` uses modulo arithmetic to wrap time continuously, and `LOOP_PINGPONG` reverses playback direction when reaching boundaries, creating oscillating time calculations that sample keyframes in reverse order on the return trip.

### Can I modify keyframes at runtime?

Yes, the `Animation` API exposes methods like `position_track_insert_key`, `track_insert_key`, and `track_set_key_value` (defined in [`scene/resources/animation.cpp`](https://github.com/godotengine/godot/blob/main/scene/resources/animation.cpp) lines 954-961). These modify the underlying key vectors that `_interpolate` samples each frame, allowing dynamic animation generation without restarting the engine or reloading scenes.