extends Node3D
## WHICH read gives the POST-modifier bone pose?
##
## foot_slope_gate produced byte-identical tables with FootIK switched OFF, which
## means its reads were the animation pose and every number it printed was
## meaningless. This probe settles which API actually sees a SkeletonModifier3D's
## writes, by measuring the instrument on a known-good input: the same foot bone,
## on a SLOPE, with foot IK on and off.
##
## The slope matters. On flat ground foot IK has nothing to do, so ON and OFF are
## legitimately identical there and the comparison would prove nothing.
##
## Run:
## "$HYPER_TPS_GODOT" --headless res://scenes/test/bone_read_probe.tscn
const SLOPE_DEG := 30.0
const SETTLE := 45
const FOOT := "DEF-foot.L"
var _player: Node = null
var _skel: Node = null
var _frames := 0
var _phase := 0 # 0 = IK on, 1 = IK off
var _results: Array = []
var _sig_pose: Vector3 = Vector3.ZERO
var _sig_seen := false
var _done := false
func _ready() -> void:
process_priority = 100
_build()
func _build() -> void:
var s: float = tan(deg_to_rad(SLOPE_DEG))
var g := StaticBody3D.new()
var cs := CollisionShape3D.new()
var f := PackedVector3Array()
var i := 0
while i < 30:
var j := 0
while j < 30:
var x0 := float(i) - 15.0
var z0 := float(j) - 15.0
var x1 := x0 + 1.0
var z1 := z0 + 1.0
var y0 := x0 * s
var y1 := x1 * s
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z1)); f.push_back(Vector3(x0, y0, z1))
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z0)); f.push_back(Vector3(x1, y1, z1))
j += 1
i += 1
var tri := ConcavePolygonShape3D.new()
tri.set_faces(f)
cs.shape = tri
g.add_child(cs)
g.name = "Ground"
add_child(g)
var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
_player = scene.instantiate()
add_child(_player)
_player.global_position = Vector3(0.0, 0.6, 0.0)
if _phase == 1:
_player.enable_foot_ik = false
_frames = 0
_sig_seen = false
_skel = null
func _physics_process(_d: float) -> void:
if _done or _player == null:
return
if _skel == null:
_skel = _find_skel(_player)
if _skel != null and _skel.has_signal("skeleton_updated"):
_skel.skeleton_updated.connect(_on_skeleton_updated)
_frames += 1
func _on_skeleton_updated() -> void:
# Read INSIDE the signal: this is the callback that fires after the skeleton
# has run its modifier stack for the frame.
if _skel == null:
return
var idx: int = _skel.find_bone(FOOT)
if idx < 0:
return
_sig_pose = (_skel.global_transform * _skel.get_bone_global_pose(idx)).origin
_sig_seen = true
func _process(_d: float) -> void:
if _done or _player == null or _frames < SETTLE:
return
var anim: Node = _find(_player, "ProceduralAnimator")
var idx: int = _skel.find_bone(FOOT) if _skel != null else -1
var fik: Variant = anim.get("_foot_ik") if anim != null else null
var route_process := Vector3.ZERO
var route_local := Vector3.ZERO
if idx >= 0:
route_process = (_skel.global_transform * _skel.get_bone_global_pose(idx)).origin
route_local = _skel.get_bone_pose_position(idx)
var offs := "n/a"
var hits := "n/a"
if fik != null:
offs = str(fik.get("_offsets"))
hits = str(fik.get("_ground_hit"))
_results.append({
"phase": _phase,
"grounded": _player.is_on_floor(),
"a_process": route_process,
"b_signal": _sig_pose,
"b_seen": _sig_seen,
"c_local": route_local,
"offsets": offs, "hits": hits,
})
_player.queue_free(); _player = null
get_node("Ground").queue_free()
_phase += 1
if _phase > 1:
_report()
_done = true
get_tree().quit(0)
return
_build()
func _report() -> void:
print("")
print("Skeleton3D has skeleton_updated signal: %s"
% str(_skel != null and _skel.has_signal("skeleton_updated")))
print("")
for r in _results:
print("--- foot IK %s ---" % ("ON" if r["phase"] == 0 else "OFF"))
print(" grounded=%s FootIK offsets=%s hits=%s" % [str(r["grounded"]), r["offsets"], r["hits"]])
print(" A get_bone_global_pose in _process : %s" % str(r["a_process"]))
print(" B same, inside skeleton_updated : %s (fired=%s)" % [str(r["b_signal"]), str(r["b_seen"])])
print(" C get_bone_pose_position (local) : %s" % str(r["c_local"]))
if _results.size() == 2:
var on: Dictionary = _results[0]
var off: Dictionary = _results[1]
print("")
print("ON vs OFF, per route — a route that shows NO difference is reading")
print("the animation pose and cannot measure foot IK:")
print(" A _process delta = %.6f m" % (on["a_process"] as Vector3).distance_to(off["a_process"]))
print(" B skeleton_updated delta = %.6f m" % (on["b_signal"] as Vector3).distance_to(off["b_signal"]))
print(" C local pose delta = %.6f m" % (on["c_local"] as Vector3).distance_to(off["c_local"]))
# The two helpers this file has always called and never defined, which is why it
# failed to parse. Plain recursive searches, matching how they are used above.
func _find_skel(root: Node) -> Skeleton3D:
if root == null:
return null
if root is Skeleton3D:
return root
for c in root.get_children():
var found := _find_skel(c)
if found != null:
return found
return null
func _find(root: Node, node_name: String) -> Node:
if root == null:
return null
if root.name == node_name:
return root
for c in root.get_children():
var found := _find(c, node_name)
if found != null:
return found
return null