3D Skeletal Animation in Godot: AnimationPlayer and AnimationTree

Created: 2026-02-08Last updated: 2026-07-08

A practical guide to Godot 4's Skeleton3D and bones, 3D animation playback with AnimationPlayer, directional blending with BlendSpace, and foot placement with SkeletonIK3D.

You want your 3D character to walk, run, and attack. But animation switching stutters, feet sink into slopes and steps, and state management gets complicated. 3D animation comes with problems that 2D never had.

This article focuses on the parts specific to 3D: Skeleton3D and bones, playback with AnimationPlayer, directional blending with BlendSpace, and foot placement with SkeletonIK3D. The basic thinking behind AnimationTree and state machines is the same as in 2D, so reading this alongside AnimationTree and state machines will deepen your understanding.

A conceptual image of 3D skeletal animation showing glowing bones visible inside a semi-transparent 3D humanoid character

What You'll Learn

  • The Skeleton3D bone hierarchy and how to access bones from script
  • Playback and blending with the AnimationPlayer that GLB import generates for you
  • How to mix animations by speed and direction with BlendSpace1D/2D
  • Dynamic foot placement with SkeletonIK3D (and a note on its deprecation)

Sponsored

Skeleton3D and the Bone Hierarchy

Before touching animation, let's cover how the skeleton works. Every skeletal animation is built on top of this bone hierarchy.

Skeleton3D is the node that represents a 3D character's skeletal structure. Animation emerges from moving bones, arranged hierarchically like a human skeleton, over time.

A diagram of the Skeleton3D bone hierarchy. A humanoid skeleton alongside a bone tree branching from Hips into Spine and Leg

A root bone like the hips (Hips) is the parent, with the spine, head, arms, and legs hanging below as children. Move the parent and the children follow. That parent-child relationship is the foundation for moving the whole body together.

You can list the bones in an imported model from script.

var skeleton := $MeshInstance3D.find_child("Skeleton3D", true, false) as Skeleton3D
print("Bone count: ", skeleton.get_bone_count())
for i in skeleton.get_bone_count():
    print("  ", skeleton.get_bone_name(i))

# Get a specific bone's index and current pose
var head_idx := skeleton.find_bone("Head")
if head_idx != -1:
    var pose := skeleton.get_bone_pose(head_idx)  # Transform3D
    print("Head local position: ", pose.origin)
Sponsored

Playing 3D Animation with AnimationPlayer

AnimationPlayer is the most basic animation playback node. When you import a 3D model (GLB), it is generated automatically alongside MeshInstance3D and Skeleton3D, ready to use.

A diagram of the GLB import flow generating MeshInstance3D, Skeleton3D, and AnimationPlayer with idle/walk/run clips

Basic Playback

@onready var anim_player: AnimationPlayer = $AnimationPlayer

func _ready() -> void:
    for anim_name in anim_player.get_animation_list():
        print("  ", anim_name)
    if anim_player.has_animation("idle"):
        anim_player.play("idle")

Smoothing Switches with Blend Time

That jolt at the moment of a switch happens because the change is instant, with no blending. set_blend_time() gives you a smooth transition.

# Set the blend time (in seconds) between animations ahead of time
anim_player.set_blend_time("idle", "walk", 0.2)
anim_player.set_blend_time("walk", "run", 0.3)

func change_to_walk() -> void:
    anim_player.play("walk")  # Blends from idle to walk over 0.2 seconds

Playback Speed and Signals

anim_player.play("walk", -1, 2.0)   # Double speed
anim_player.play_backwards("walk")   # Reverse playback

func _ready() -> void:
    # Useful for things like returning to idle once an attack motion ends
    anim_player.animation_finished.connect(_on_animation_finished)

func _on_animation_finished(anim_name: String) -> void:
    if anim_name == "attack":
        anim_player.play("idle")

For building out AnimationPlayer itself (method call tracks, RESET, and so on), see Advanced AnimationPlayer Techniques.

Sponsored

Managing State with AnimationTree

Once states grow to walk, run, jump, and attack, bundle them into an AnimationTree state machine. The thinking is identical to 2D : AnimationTree (the conductor) pulls clips from AnimationPlayer (the animation warehouse), and your code just reports the current state with travel().

@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")

func _ready() -> void:
    anim_tree.active = true

func _physics_process(delta: float) -> void:
    var speed := velocity.length()
    var target := "idle"
    if speed > 0.1:
        target = "run" if Input.is_action_pressed("sprint") else "walk"
    if state_machine.get_current_node() != target:  # Only when it changes
        state_machine.travel(target)

State machine design (adding nodes, transitions, Xfade Time) and the detailed split with your code are covered in AnimationTree and state machines. From here, we move on to BlendSpace and IK , which are especially useful in 3D.

Blending Speed and Direction with BlendSpace

Where a state machine switches between discrete states like "walk" and "run", BlendSpace suits continuously varying values such as speed and direction.

BlendSpace1D blends along a single value (movement speed, for example). It smoothly connects idle at 0, walk at 3, and run at 10.

# Create a BlendSpace1D in the editor and place 0.0=idle / 3.0=walk / 10.0=run
var speed := velocity.length()
anim_tree.set("parameters/movement/blend_position", speed)

BlendSpace2D blends along a 2D vector (movement direction, for example). It is ideal for eight-direction walk animation on a character who moves in all directions, like in an action RPG.

A BlendSpace2D diagram. Walk animations for each heading are placed at eight points around a square, and the blend cursor's position mixes the nearest directions
# Place walk_forward, walk_right, and so on at the corners and cardinal points
var dir := Vector2(
    Input.get_axis("move_left", "move_right"),
    Input.get_axis("move_back", "move_forward")
)
anim_tree.set("parameters/locomotion/blend_position", dir)
Sponsored

Placing Feet on the Ground with SkeletonIK3D

Animation data alone cannot make on-the-spot adjustments like matching feet to uneven terrain or reaching a hand toward a moving object. That is where IK (inverse kinematics) comes in.

A comparison diagram of no IK versus IK. Without IK the foot sinks into a step; with IK the ankle bends and the foot rests on the step

SkeletonIK3D works backward from a target position to solve the bones from the ankle down, giving you grounded feet and reaching hands. Even with a walk animation authored for flat ground, feet land properly on steps and slopes.

tips: SkeletonIK3D is on the deprecation path in Godot 4.x, with migration toward the SkeletonModifier3D family. For new projects, consider using SkeletonModifier3D. Existing projects will keep working with SkeletonIK3D, but it may be removed in a future version.

# Place a SkeletonIK3D (Root Bone: Hips / Tip Bone: FootL) under Skeleton3D,
# and assign a Node3D representing the ground position to Target
@onready var foot_ik: SkeletonIK3D = $Skeleton3D/FootIK_L
@onready var foot_target: Node3D = $FootTarget_L

func _ready() -> void:
    foot_ik.start()  # Start IK

func _process(delta: float) -> void:
    # Cast a ray below the foot and align the target to the ground height
    var space := get_world_3d().direct_space_state
    var from := foot_target.global_position + Vector3.UP
    var to := foot_target.global_position + Vector3.DOWN * 2.0
    var hit := space.intersect_ray(PhysicsRayQueryParameters3D.create(from, to))
    if hit:
        foot_target.global_position = hit.position

You can tune how strongly IK applies with interpolation (0.0 = off through 1.0 = fully applied), and turn it off temporarily with stop().

Hands-On: Combining Locomotion Blending and Foot IK

Let's build a character for a 3D action game, open world, or third-person shooter whose walk and run blend by movement speed and whose feet land properly on slopes and steps. The trick is to layer animation (the broad motion) and IK (fine-grained ground contact) as separate roles .

  • Animation side : BlendSpace and AnimationTree produce walk/run poses from speed
  • IK side : a raycast below the foot finds the ground, and SkeletonIK3D places the ankle there
A diagram of the hands-on setup. AnimationTree produces the broad motion via blend_position from speed, while SkeletonIK3D fine-tunes ground contact via an IK target from a ground raycast
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var foot_ik: SkeletonIK3D = $Skeleton3D/FootIK_L
@onready var foot_target: Node3D = $FootTarget_L

func _ready() -> void:
    anim_tree.active = true
    foot_ik.start()

func _physics_process(delta: float) -> void:
    # Animation: pass speed to the blend position (walk to run)
    anim_tree.set("parameters/locomotion/blend_position", velocity.length())

    # IK: align the target to the ground height below the foot
    var space := get_world_3d().direct_space_state
    var from := foot_target.global_position + Vector3.UP
    var to := foot_target.global_position + Vector3.DOWN * 2.0
    var hit := space.intersect_ray(PhysicsRayQueryParameters3D.create(from, to))
    if hit:
        foot_target.global_position = hit.position

There are two points to take away.

  • Animation and IK have different jobs : animation handles the broad motion of "walking," while IK handles the fine adjustment of "put that foot at the ground height under it." Keeping them separate lets the same walk animation look natural on flat ground and slopes alike.
  • IK is the expensive part : IK recalculates every frame. As shown below, turning IK off for distant or off-screen characters keeps the cost down.

Performance Optimization

Rich animation is appealing, but processing cost becomes a problem in scenes with many characters. Scale the work down in stages based on distance from the camera.

A diagram of distance-based LOD. Near distance updates every frame, mid distance skips frames, far distance stops updating

A character rendered tiny at the edge of the screen doesn't need the same update precision as the hero.

func _process(delta: float) -> void:
    var distance := global_position.distance_to(camera.global_position)

    if distance > 50.0:
        anim_tree.active = false                     # Far: stop processing
    else:
        anim_tree.active = true
        if distance > 20.0 and Engine.get_process_frames() % 2 != 0:
            return                                   # Mid: skip every other frame
        # Near: normal update

tips: Setting anim_tree.active = false stops animation processing entirely. The safe pattern is to set active to false at long distances and leave active true while skipping updates at mid distances. Turning off IK on non-player characters (foot_ik.stop()) and enabling animation compression at import time (Compression: Lossy / Optimize) also help.

Summary

  • Skeleton3D : the parent-child bone hierarchy. Moving a parent brings its children along, which is the foundation
  • AnimationPlayer : generated automatically by GLB import. Play and blend with play() and set_blend_time()
  • AnimationTree : state management works the same as in 2D (AnimationTree and state machines). In 3D, BlendSpace directional blending is especially powerful
  • SkeletonIK3D : dynamic pose adjustment such as foot placement (deprecation in progress, migrating to SkeletonModifier3D is recommended)
  • Optimization : distance-based LOD, disabling unneeded IK, animation compression

Start by loading a GLB and playing walk and run with AnimationPlayer, then layer on directional blending with BlendSpace and ground contact with SkeletonIK3D one step at a time. One-off effects like a damage flash are easier to add with Tween.