OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / bone_read_probe.gd

181 lines · gd ref:
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