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.