Skip to main content
Skeletal animation is the standard technique for animating 3D characters and articulated objects. It uses a hierarchy of bones to deform a 3D mesh, allowing for realistic character movement and animation.

Understanding Skeleton3D

The Skeleton3D node manages a hierarchy of bones used for 3D skeletal animation. Each bone has a transform relative to its parent bone, and the skeleton deforms attached meshes based on bone poses.

Key Concepts

  • Bone: A single transform in the skeleton hierarchy
  • Rest Pose: The default/initial pose of each bone
  • Pose: The current animated transform of each bone
  • Skinning: The process of deforming a mesh based on bone transforms
  • Skin: A resource that maps mesh vertices to bones

Setting Up a Skeleton

Skeletons are typically imported from 3D modeling software, but you can also create and manipulate them at runtime.

Accessing Bones

# Get bone count
var bone_count = $Skeleton3D.get_bone_count()
print("Total bones: ", bone_count)

# Find bone by name
var head_bone = $Skeleton3D.find_bone("Head")
if head_bone != -1:
    print("Found head bone at index: ", head_bone)

# Get bone name
var bone_name = $Skeleton3D.get_bone_name(0)
print("First bone: ", bone_name)

# Get bone parent
var parent_idx = $Skeleton3D.get_bone_parent(head_bone)
if parent_idx != -1:
    print("Parent bone: ", $Skeleton3D.get_bone_name(parent_idx))

Bone Hierarchy

# Get all children of a bone
var spine_bone = $Skeleton3D.find_bone("Spine")
var children = $Skeleton3D.get_bone_children(spine_bone)
for child_idx in children:
    print("Child bone: ", $Skeleton3D.get_bone_name(child_idx))

# Get root bones (bones without parents)
var root_bones = $Skeleton3D.get_parentless_bones()
for root_idx in root_bones:
    print("Root bone: ", $Skeleton3D.get_bone_name(root_idx))

Bone Poses

Bones have two key transforms: rest (the default pose) and pose (the current animated state).

Rest Pose

The rest pose defines the initial bone configuration:
# Get rest transform
var bone_idx = $Skeleton3D.find_bone("UpperArm")
var rest = $Skeleton3D.get_bone_rest(bone_idx)
print("Rest transform: ", rest)

# Get global rest (relative to skeleton)
var global_rest = $Skeleton3D.get_bone_global_rest(bone_idx)

# Set rest transform
var new_rest = Transform3D()
new_rest.origin = Vector3(0, 1, 0)
$Skeleton3D.set_bone_rest(bone_idx, new_rest)

# Reset all bones to rest pose
$Skeleton3D.reset_bone_poses()

Current Pose

The pose is the animated transform, typically set by animations:
var bone_idx = $Skeleton3D.find_bone("Head")

# Get local pose (relative to parent bone)
var pose = $Skeleton3D.get_bone_pose(bone_idx)
print("Current pose: ", pose)

# Get global pose (relative to skeleton)
var global_pose = $Skeleton3D.get_bone_global_pose(bone_idx)

# Set local pose
var new_pose = Transform3D()
new_pose.origin = Vector3(0, 0.1, 0)
$Skeleton3D.set_bone_pose(bone_idx, new_pose)

# Set global pose
$Skeleton3D.set_bone_global_pose(bone_idx, global_pose)

Pose Components

Access individual position, rotation, and scale components:
var bone_idx = $Skeleton3D.find_bone("RightHand")

# Get components
var position = $Skeleton3D.get_bone_pose_position(bone_idx)
var rotation = $Skeleton3D.get_bone_pose_rotation(bone_idx)  # Quaternion
var scale = $Skeleton3D.get_bone_pose_scale(bone_idx)

# Set components
$Skeleton3D.set_bone_pose_position(bone_idx, Vector3(0, 1, 0))
$Skeleton3D.set_bone_pose_rotation(bone_idx, Quaternion(Vector3.UP, PI / 4))
$Skeleton3D.set_bone_pose_scale(bone_idx, Vector3(1.2, 1.2, 1.2))

Skinning

Skinning is the process of deforming a mesh based on bone transforms. The Skin resource defines how mesh vertices are influenced by bones.

Creating and Registering Skins

# Create a skin from current rest transforms
var skin = $Skeleton3D.create_skin_from_rest_transforms()

# Register skin with skeleton
var skin_ref = $Skeleton3D.register_skin(skin)

# Apply skin to MeshInstance3D
$MeshInstance3D.skin = skin

Bone Attachments

Attach objects (weapons, accessories) to specific bones using BoneAttachment3D:
# Create a sword attached to the hand
var bone_attachment = BoneAttachment3D.new()
bone_attachment.bone_name = "RightHand"
$Skeleton3D.add_child(bone_attachment)

# Add sword model as child of attachment
var sword = preload("res://sword.tscn").instantiate()
bone_attachment.add_child(sword)
The BoneAttachment3D automatically follows the bone’s transform.

Inverse Kinematics (IK)

IK allows you to position bones by specifying an end goal (like placing a hand on an object) rather than rotating each joint manually.

Using SkeletonIK3D (Deprecated)

While SkeletonIK3D is available, Godot 4+ recommends using SkeletonModifier3D with custom IK implementations or third-party solutions.

Custom IK Implementation Example

# Simple two-bone IK (e.g., for an arm)
func solve_two_bone_ik(skeleton: Skeleton3D, root_bone: String, 
                       mid_bone: String, tip_bone: String, target_pos: Vector3):
    var root_idx = skeleton.find_bone(root_bone)
    var mid_idx = skeleton.find_bone(mid_bone)
    var tip_idx = skeleton.find_bone(tip_bone)
    
    # Get bone lengths
    var upper_length = skeleton.get_bone_rest(mid_idx).origin.length()
    var lower_length = skeleton.get_bone_rest(tip_idx).origin.length()
    
    # Get root position in global space
    var root_global = skeleton.get_bone_global_pose(root_idx)
    var root_pos = root_global.origin
    
    # Calculate target direction and distance
    var target_dir = (target_pos - root_pos).normalized()
    var target_dist = root_pos.distance_to(target_pos)
    
    # Clamp distance to reachable range
    var max_reach = upper_length + lower_length
    target_dist = min(target_dist, max_reach * 0.99)
    
    # Law of cosines for angles
    var upper_angle = acos(clamp(
        (target_dist * target_dist + upper_length * upper_length - lower_length * lower_length) / 
        (2 * target_dist * upper_length), -1, 1))
    
    var mid_angle = acos(clamp(
        (upper_length * upper_length + lower_length * lower_length - target_dist * target_dist) / 
        (2 * upper_length * lower_length), -1, 1))
    
    # Apply rotations (simplified - real implementation needs proper axis calculation)
    # This is a basic example - production code needs more robust math
For production IK, consider using third-party addons like “Godot IK” or implementing FABRIK (Forward And Backward Reaching Inverse Kinematics) for multi-bone chains.

Skeleton Modifiers

Godot 4 uses SkeletonModifier3D nodes for procedural bone manipulation:
# Example: Custom modifier to make character look at target
extends SkeletonModifier3D

var target_position: Vector3
var head_bone: String = "Head"

func _process_modification():
    var skeleton = get_skeleton()
    var head_idx = skeleton.find_bone(head_bone)
    
    if head_idx == -1:
        return
    
    # Get head global position
    var head_global = skeleton.get_bone_global_pose(head_idx)
    var look_dir = (target_position - head_global.origin).normalized()
    
    # Calculate rotation to look at target
    var look_transform = head_global.looking_at(target_position, Vector3.UP)
    
    # Apply rotation (with limits)
    skeleton.set_bone_global_pose(head_idx, look_transform)

Importing Skeletal Animations

When importing 3D models with skeletal animations:
1

Export from 3D Software

Export your model with armature/skeleton in formats like GLTF, FBX, or Collada.
2

Import to Godot

Drag the file into your Godot project. The import dialog appears.
3

Configure Import Settings

  • Enable “Import Animations”
  • Set bone name conventions
  • Configure retargeting if needed
4

Verify Skeleton

Open the imported scene and check the Skeleton3D node to ensure bones are correct.

Bone Name Conventions

Godot recognizes common bone naming conventions:
  • Mixamo: mixamorig:Hips, mixamorig:Spine, etc.
  • Humanoid: Hips, Spine, Head, LeftUpperArm, etc.
You can remap bone names in the import settings.

Animation Retargeting

Retarget animations from one skeleton to another:
# Retarget animation from source to target skeleton
func retarget_animation(source_skeleton: Skeleton3D, target_skeleton: Skeleton3D, 
                        animation: Animation):
    var bone_map = {}  # Map source bone names to target bone names
    
    # Build bone mapping
    for i in range(source_skeleton.get_bone_count()):
        var source_name = source_skeleton.get_bone_name(i)
        var target_idx = target_skeleton.find_bone(source_name)
        if target_idx != -1:
            bone_map[i] = target_idx
    
    # Retarget animation tracks...

Performance Optimization

Motion Scale

Adjust the scale of position animations:
# Scale all position animations (useful for different character sizes)
$Skeleton3D.motion_scale = 1.5  # 150% of original motion

Show Rest Only

Disable animation processing for debugging:
# Display rest pose only (no animation processing)
$Skeleton3D.show_rest_only = true

Bone Enabling

# Disable bones you don't need to animate
var finger_bones = ["LeftPinky", "LeftRing", "LeftMiddle"]
for bone_name in finger_bones:
    var bone_idx = $Skeleton3D.find_bone(bone_name)
    if bone_idx != -1:
        $Skeleton3D.set_bone_enabled(bone_idx, false)

# Check if bone is enabled
var is_enabled = $Skeleton3D.is_bone_enabled(bone_idx)

Signals

Skeleton3D provides signals for monitoring changes:
func _ready():
    $Skeleton3D.pose_updated.connect(_on_pose_updated)
    $Skeleton3D.skeleton_updated.connect(_on_skeleton_updated)
    $Skeleton3D.bone_list_changed.connect(_on_bone_list_changed)

func _on_pose_updated():
    print("Pose updated")

func _on_skeleton_updated():
    print("Skeleton final pose calculated")

func _on_bone_list_changed():
    print("Bone hierarchy changed")

Complete Example: Procedural Animation

extends Node3D

var skeleton: Skeleton3D
var time: float = 0.0

func _ready():
    skeleton = $Skeleton3D

func _process(delta):
    time += delta
    
    # Procedural head bobbing
    var head_idx = skeleton.find_bone("Head")
    if head_idx != -1:
        var bob_offset = Vector3(0, sin(time * 5.0) * 0.02, 0)
        var rest_pose = skeleton.get_bone_rest(head_idx)
        var new_pose = rest_pose.translated(bob_offset)
        skeleton.set_bone_pose(head_idx, new_pose)
    
    # Rotate spine based on mouse position
    var spine_idx = skeleton.find_bone("Spine")
    if spine_idx != -1:
        var mouse_pos = get_viewport().get_mouse_position()
        var viewport_size = get_viewport().get_visible_rect().size
        var normalized = (mouse_pos - viewport_size / 2) / viewport_size
        
        var rotation = Quaternion(Vector3.UP, normalized.x * 0.5) * \
                       Quaternion(Vector3.RIGHT, -normalized.y * 0.3)
        
        var current_pose = skeleton.get_bone_pose(spine_idx)
        current_pose.basis = Basis(rotation)
        skeleton.set_bone_pose(spine_idx, current_pose)

Next Steps

AnimationTree

Use AnimationTree for complex skeletal animation blending

Animation Overview

Review fundamental animation concepts

Build docs developers (and LLMs) love