Quick answer: Per-foot raycast down. Drive SkeletonModifier3D with hit position+normal. Lower pelvis by max(foot_offsets).

Walking up stairs, feet float above some steps and clip into others. Default animation has fixed-step heights; IK needs to follow surface.

The Fix

extends SkeletonModifier3D

@export var foot_l_bone: String = "FootL"
@export var foot_r_bone: String = "FootR"

func _process_modification():
    var sk = get_skeleton()
    _solve_foot(sk, foot_l_bone)
    _solve_foot(sk, foot_r_bone)

func _solve_foot(sk: Skeleton3D, name: String):
    var idx = sk.find_bone(name)
    if idx == -1: return
    var world = sk.global_transform * sk.get_bone_global_pose(idx)
    var from = world.origin + Vector3.UP * 0.5
    var to = world.origin - Vector3.UP * 0.5
    var ray = PhysicsRayQueryParameters3D.create(from, to)
    var hit = get_world_3d().direct_space_state.intersect_ray(ray)
    if hit:
        var p = sk.global_transform.affine_inverse() * hit.position
        sk.set_bone_pose_position(idx, p)

Reorder modifiers: pelvis adjuster first, foot solver second. Set per-foot blend weight when air-borne to disable the IK in flight.

Verifying

Walk up stairs. Each foot lands on its tread. Without the modifier: feet clip into steps.

“Raycast down. Set bone position. Feet land.”

Related Issues

For Skeleton3D bone reset, see bone reset. For AnimationTree travel, see travel.

Per-foot ray. Bone pose. Stairs land.